Interactor Workflows Skill
Build state-machine based automations with human-in-the-loop support for multi-step business processes.
When to Use
- Approval Flows: Multi-level approval processes (expense reports, purchase orders)
- Onboarding Workflows: Step-by-step user or employee onboarding
- Order Processing: Order fulfillment with status tracking
- Support Escalation: Ticket routing with human handoffs
- Document Processing: Review and approval pipelines
- Any Multi-Step Process: Processes requiring conditional logic and user input
Prerequisites
- Interactor authentication configured (see
interactor-authskill) - Understanding of state machines and workflow concepts
- Webhook endpoint for workflow notifications (recommended)
Overview
Workflows consist of:
| Component | Description |
|---|---|
| States | Steps in your process (action, halting, terminal) |
| Transitions | Rules for moving between states |
| Instances | Running executions of a workflow |
| Threads | Parallel execution paths within an instance |
Instructions
Step 1: Create a Workflow Definition
curl -X POST https://core.interactor.com/api/v1/workflows \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "approval_workflow",
"initial_state": "request",
"ai_guidance": "This workflow handles approval requests. Route based on amount thresholds.",
"states": {
"request": {
"type": "action",
"logic": {
"type": "script",
"code": "return { request_id: input.id, amount: input.amount, status: \"pending\", submitted_at: new Date().toISOString() }"
},
"transitions": [
{ "target": "await_approval" }
]
},
"await_approval": {
"type": "halting",
"presentation": {
"type": "form",
"title": "Approval Required",
"description": "Please review and approve or reject this request.",
"fields": [
{ "name": "approved", "type": "boolean", "label": "Approve this request?" },
{ "name": "comment", "type": "string", "label": "Comment (optional)", "multiline": true }
]
},
"transitions": [
{ "target": "approved", "condition": { "field": "approved", "equals": true } },
{ "target": "rejected" }
]
},
"approved": {
"type": "terminal",
"on_enter": {
"type": "http",
"method": "POST",
"url": "https://yourapp.com/api/webhooks/approval-complete",
"body": { "request_id": "${workflow_data.request_id}", "status": "approved" }
}
},
"rejected": {
"type": "terminal",
"on_enter": {
"type": "http",
"method": "POST",
"url": "https://yourapp.com/api/webhooks/approval-complete",
"body": { "request_id": "${workflow_data.request_id}", "status": "rejected" }
}
}
}
}'
Response:
{
"data": {
"name": "approval_workflow",
"version_id": "v_abc123",
"status": "draft",
"created_at": "2026-01-20T12:00:00Z"
}
}
State Types
| Type | Description | Behavior |
|---|---|---|
action |
Executes logic automatically | Runs logic, then transitions immediately |
halting |
Pauses for external input | Waits for resume call with user input |
terminal |
End state | Workflow completes, no further transitions |
Note: The
on_enterproperty shown in terminal states (for triggering HTTP callbacks on completion) is an optional enhancement. Verify availability with your Interactor version.
Step 2: Validate Without Saving
Test a workflow definition before creating it:
curl -X POST https://core.interactor.com/api/v1/workflows/validate \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "my_workflow",
"initial_state": "start",
"states": {
"start": {
"type": "action",
"logic": { "type": "script", "code": "return { message: \"Hello\" }" },
"transitions": [{ "target": "end" }]
},
"end": {
"type": "terminal"
}
}
}'
Response (success):
{
"data": {
"valid": true
}
}
Response (error):
{
"data": {
"valid": false,
"errors": [
{
"path": "states.start.transitions[0].target",
"message": "Target state 'nonexistent' does not exist"
}
]
}
}
Step 3: List Workflows
curl https://core.interactor.com/api/v1/workflows \
-H "Authorization: Bearer <token>"
Response:
{
"data": {
"workflows": [
{
"name": "approval_workflow",
"latest_version_id": "v_abc123",
"published_version_id": "v_abc123",
"created_at": "2026-01-20T12:00:00Z"
},
{
"name": "onboarding_workflow",
"latest_version_id": "v_def456",
"published_version_id": null,
"created_at": "2026-01-19T10:00:00Z"
}
]
}
}
Step 4: List Versions
curl https://core.interactor.com/api/v1/workflows/approval_workflow/versions \
-H "Authorization: Bearer <token>"
Response:
{
"data": {
"versions": [
{
"version_id": "v_abc123",
"status": "draft",
"created_at": "2026-01-20T12:00:00Z"
},
{
"version_id": "v_def456",
"status": "published",
"created_at": "2026-01-19T10:00:00Z"
}
]
}
}
Step 5: Publish a Version
Workflows must be published before they can be executed:
curl -X POST https://core.interactor.com/api/v1/workflows/approval_workflow/versions/v_abc123/publish \
-H "Authorization: Bearer <token>"
Response:
{
"data": {
"version_id": "v_abc123",
"status": "published",
"published_at": "2026-01-20T12:05:00Z"
}
}
Workflow Instances
Instances are running executions of a workflow.
Create Instance
Start a new workflow execution:
curl -X POST https://core.interactor.com/api/v1/workflows/approval_workflow/instances \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"namespace": "user_123",
"input": {
"id": "req_456",
"amount": 5000,
"requester": "john@example.com",
"description": "New laptop for development"
}
}'
Response:
{
"data": {
"id": "inst_xyz",
"workflow_name": "approval_workflow",
"version_id": "v_abc123",
"namespace": "user_123",
"status": "halted",
"current_state": "await_approval",
"workflow_data": {
"request_id": "req_456",
"amount": 5000,
"status": "pending",
"submitted_at": "2026-01-20T12:00:00Z"
},
"created_at": "2026-01-20T12:00:00Z"
}
}
Instance Status Values
| Status | Description |
|---|---|
running |
Actively executing (in an action state) |
halted |
Paused, waiting for external input |
completed |
Finished successfully (reached terminal state) |
failed |
Terminated due to error |
cancelled |
Manually cancelled |
List Instances
curl https://core.interactor.com/api/v1/workflows/instances \
-H "Authorization: Bearer <token>"
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
namespace |
string | Filter by namespace |
workflow_name |
string | Filter by workflow |
status |
string | running, halted, completed, failed, cancelled |
Example - List halted instances for a user:
curl "https://core.interactor.com/api/v1/workflows/instances?namespace=user_123&status=halted" \
-H "Authorization: Bearer <token>"
Response:
{
"data": {
"instances": [
{
"id": "inst_xyz",
"workflow_name": "approval_workflow",
"status": "halted",
"current_state": "await_approval",
"created_at": "2026-01-20T12:00:00Z"
},
{
"id": "inst_abc",
"workflow_name": "onboarding_workflow",
"status": "halted",
"current_state": "verify_email",
"created_at": "2026-01-19T15:30:00Z"
}
]
}
}
Get Instance
curl https://core.interactor.com/api/v1/workflows/instances/inst_xyz \
-H "Authorization: Bearer <token>"
Response:
{
"data": {
"id": "inst_xyz",
"workflow_name": "approval_workflow",
"version_id": "v_abc123",
"namespace": "user_123",
"status": "halted",
"current_state": "await_approval",
"workflow_data": {
"request_id": "req_456",
"amount": 5000,
"status": "pending"
},
"halting_presentation": {
"type": "form",
"title": "Approval Required",
"description": "Please review and approve or reject this request.",
"fields": [
{ "name": "approved", "type": "boolean", "label": "Approve this request?" },
{ "name": "comment", "type": "string", "label": "Comment (optional)", "multiline": true }
]
},
"threads": [
{
"id": "thread_main",
"status": "halted",
"current_state": "await_approval"
}
],
"history": [
{
"state": "request",
"entered_at": "2026-01-20T12:00:00Z",
"exited_at": "2026-01-20T12:00:01Z",
"transition": "await_approval"
},
{
"state": "await_approval",
"entered_at": "2026-01-20T12:00:01Z"
}
],
"created_at": "2026-01-20T12:00:00Z"
}
}
Resuming Workflows
When a workflow reaches a halting state, it waits for external input.
Resume with Input
curl -X POST https://core.interactor.com/api/v1/workflows/instances/inst_xyz/resume \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"input": {
"approved": true,
"comment": "Looks good, approved for Q1 budget"
}
}'
Response:
{
"data": {
"id": "inst_xyz",
"status": "completed",
"current_state": "approved",
"workflow_data": {
"request_id": "req_456",
"amount": 5000,
"status": "pending",
"approved": true,
"comment": "Looks good, approved for Q1 budget"
}
}
}
The workflow continues execution based on the input and transition conditions.
Cancel Instance
curl -X POST https://core.interactor.com/api/v1/workflows/instances/inst_xyz/cancel \
-H "Authorization: Bearer <token>"
Response:
{
"data": {
"id": "inst_xyz",
"status": "cancelled",
"cancelled_at": "2026-01-20T12:30:00Z"
}
}
Threads
Workflows can have parallel execution paths (threads).
List Threads
curl https://core.interactor.com/api/v1/workflows/instances/inst_xyz/threads \
-H "Authorization: Bearer <token>"
Response:
{
"data": {
"threads": [
{
"id": "thread_main",
"status": "halted",
"current_state": "await_approval"
},
{
"id": "thread_finance",
"status": "completed",
"current_state": "finance_approved"
}
]
}
}
Resume Specific Thread
For workflows with multiple parallel threads:
curl -X POST https://core.interactor.com/api/v1/workflows/instances/inst_xyz/threads/thread_1/resume \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"input": {
"department_approved": true
}
}'
Response:
{
"data": {
"id": "inst_xyz",
"status": "running",
"threads": [
{
"id": "thread_1",
"status": "completed",
"current_state": "department_approved"
},
{
"id": "thread_2",
"status": "halted",
"current_state": "await_finance_approval"
}
]
}
}
History API
Query workflow execution history for debugging, monitoring, and audit.
List History Events
curl https://core.interactor.com/api/v1/workflows/instances/inst_xyz/history \
-H "Authorization: Bearer <token>"
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
limit |
integer | Max events (default: 100, max: 1000) |
cursor |
string | Pagination cursor |
types |
string | Filter by type: transition, step, halt, error, lifecycle |
since |
ISO8601 | Events after this timestamp |
until |
ISO8601 | Events before this timestamp |
thread |
string | Filter to specific thread |
include_data |
boolean | Include workflow_data snapshots |
Response:
{
"data": {
"instance_id": "inst_xyz",
"workflow_id": "wf_abc",
"status": "completed",
"events": [
{
"id": "evt_01HX...",
"type": "lifecycle",
"subtype": "created",
"timestamp": "2026-01-20T12:00:00Z",
"initial_state": "request"
},
{
"id": "evt_01HX...",
"type": "transition",
"subtype": "state_change",
"from_state": "request",
"to_state": "processing",
"trigger": "automatic",
"changes": {
"updated": {"status": {"from": "pending", "to": "processing"}}
}
}
],
"pagination": {"has_more": false, "next_cursor": null}
}
}
Get Single Event
curl https://core.interactor.com/api/v1/workflows/instances/inst_xyz/events/evt_01HX... \
-H "Authorization: Bearer <token>"
Add ?include_data=true to include the workflow_data snapshot at that point.
Error Dashboard
Query errors across all workflows:
curl "https://core.interactor.com/api/v1/workflows/errors?since=2026-01-20T00:00:00Z" \
-H "Authorization: Bearer <token>"
Halting Instructions
When a workflow halts, you can configure how the halting message is generated and presented to users.
AI-Generated Instructions
Use AI to dynamically generate contextual messages based on workflow data:
{
"await_approval": {
"type": "halting",
"halting_instructions": {
"type": "ai",
"config": {
"prompt": "Summarize this order and ask the user to approve or reject it.",
"model": "claude-3-haiku-20240307",
"include_data_paths": ["order", "customer", "risk_score"]
}
},
"transition_mode": "selection",
"transitions": [
{"key": "approve", "to": "approved", "description": "Approve the order"},
{"key": "reject", "to": "rejected", "description": "Reject the order"}
]
}
}
Simple format - treats instruction as an AI prompt:
{
"halting_instructions": {
"instruction": "Tell the user the strategy is ready for review. Highlight key metrics and risks.",
"include_data": ["strategy", "benchmarks", "risk_assessment"]
}
}
Static Message Instructions
For static messages without AI generation:
{
"halting_instructions": {
"type": "message",
"config": {
"title": "Approval Required",
"message": "This order exceeds the automatic approval threshold and requires manual review."
}
}
}
Halted Response
When halted, the API response includes halted_options:
{
"status": "halted",
"halted_at_state": "await_approval",
"halted_options": {
"instruction": "Order #123 for $150.00 from Acme Corp is ready. Risk score: Low (23).",
"include_data": ["order", "customer"],
"transition_mode": "selection",
"choices": [
{"key": "approve", "description": "Approve the order", "to": "approved"},
{"key": "reject", "description": "Reject the order", "to": "rejected"}
],
"generated": true
}
}
| Field | Description |
|---|---|
instruction |
Message to display (AI-generated or static) |
generated |
true if AI-generated, false if static |
choices |
Available transitions for selection mode |
Halting Presentations (Legacy)
Note: The
presentationformat is still supported for backward compatibility. New workflows should usehalting_instructionsabove.
When a workflow halts, specify how to present the required input to users.
Note: The
titleanddescriptionfields shown in presentations are optional enhancements for better UX. The core API requires onlytypeand the type-specific fields (fields,options, ormessage).
Form Presentation
{
"type": "form",
"title": "Approval Required",
"description": "Please review the request details and provide your decision.",
"fields": [
{
"name": "approved",
"type": "boolean",
"label": "Approve this request?",
"required": true
},
{
"name": "amount",
"type": "number",
"label": "Approved Amount",
"default": "${workflow_data.amount}",
"min": 0,
"max": 100000
},
{
"name": "notes",
"type": "string",
"label": "Notes",
"multiline": true,
"placeholder": "Add any notes or conditions..."
},
{
"name": "priority",
"type": "select",
"label": "Priority",
"options": [
{ "value": "low", "label": "Low" },
{ "value": "medium", "label": "Medium" },
{ "value": "high", "label": "High" }
],
"default": "medium"
}
]
}
Choice Presentation
{
"type": "choice",
"title": "Select Action",
"message": "How would you like to proceed with this request?",
"options": [
{
"value": "approve",
"label": "Approve",
"description": "Approve the request as submitted"
},
{
"value": "reject",
"label": "Reject",
"description": "Reject the request"
},
{
"value": "escalate",
"label": "Escalate to Manager",
"description": "Send to manager for review"
},
{
"value": "request_info",
"label": "Request More Information",
"description": "Ask the requester for additional details"
}
]
}
Message Presentation
{
"type": "message",
"title": "Processing",
"message": "Waiting for external system response. This may take a few minutes.",
"show_progress": true
}
Note: The
show_progressfield is an optional UI hint. Client implementations may ignore it if not supported.
Field Types
| Type | Description | Additional Properties |
|---|---|---|
string |
Text input | multiline, placeholder, maxLength |
number |
Numeric input | min, max, step |
boolean |
Checkbox/toggle | - |
select |
Dropdown selection | options array |
date |
Date picker | minDate, maxDate |
file |
File upload | accept, maxSize |
Note: Common field properties include
required,default, andlabel. Additional properties likeplaceholder,step,maxLengthmay vary by Interactor version. Test with/validateendpoint to confirm supported properties.
Workflow Logic
Script Logic
Execute JavaScript code in action states:
{
"type": "script",
"code": "const total = input.items.reduce((sum, item) => sum + item.price, 0); const needsApproval = total > 1000; return { ...workflow_data, total, needs_approval: needsApproval, calculated_at: new Date().toISOString() };"
}
Available Variables:
input- The input provided when starting or resuming the workflowworkflow_data- Current accumulated workflow datacontext- Additional context (namespace, instance_id, etc.)
HTTP Logic
Make external API calls:
{
"type": "http",
"method": "POST",
"url": "https://api.yourservice.com/process",
"headers": {
"Authorization": "Bearer ${secrets.API_KEY}",
"Content-Type": "application/json"
},
"body": {
"order_id": "${workflow_data.order_id}",
"amount": "${workflow_data.amount}",
"customer_email": "${workflow_data.customer_email}"
},
"timeout": 30000,
"retry": {
"attempts": 3,
"backoff": "exponential"
}
}
Note: The
timeoutandretryproperties are optional enhancements. The core API requires onlytype,method,url, and optionallyheadersandbody.
Transition Conditions
Define conditions for state transitions:
{
"transitions": [
{
"target": "high_value_approval",
"condition": {
"field": "amount",
"operator": "gt",
"value": 10000
}
},
{
"target": "manager_approval",
"condition": {
"field": "amount",
"operator": "gt",
"value": 1000
}
},
{
"target": "auto_approve"
}
]
}
Operators:
| Operator | Description | Example |
|---|---|---|
equals |
Exact match | { "field": "status", "equals": "approved" } |
not_equals |
Not equal | { "field": "status", "not_equals": "rejected" } |
gt |
Greater than | { "field": "amount", "operator": "gt", "value": 1000 } |
gte |
Greater than or equal | { "field": "amount", "operator": "gte", "value": 1000 } |
lt |
Less than | { "field": "amount", "operator": "lt", "value": 100 } |
lte |
Less than or equal | { "field": "amount", "operator": "lte", "value": 100 } |
contains |
String contains | { "field": "email", "operator": "contains", "value": "@company.com" } |
in |
Value in array | { "field": "category", "operator": "in", "value": ["A", "B", "C"] } |
Complex Conditions
Use and / or for complex conditions:
{
"transitions": [
{
"target": "vp_approval",
"condition": {
"and": [
{ "field": "approved", "equals": true },
{ "field": "amount", "operator": "gt", "value": 10000 }
]
}
},
{
"target": "approved",
"condition": {
"or": [
{ "field": "amount", "operator": "lte", "value": 1000 },
{
"and": [
{ "field": "approved", "equals": true },
{ "field": "amount", "operator": "lte", "value": 10000 }
]
}
]
}
},
{
"target": "rejected"
}
]
}
Complete Example: Multi-Level Approval
{
"name": "purchase_approval",
"initial_state": "submit",
"ai_guidance": "Multi-level purchase approval workflow. Amount thresholds: <$1000 auto-approve, $1000-$10000 manager, >$10000 VP required.",
"states": {
"submit": {
"type": "action",
"logic": {
"type": "script",
"code": "return { ...input, submitted_at: new Date().toISOString(), status: 'pending' }"
},
"transitions": [
{
"target": "auto_approved",
"condition": { "field": "amount", "operator": "lte", "value": 1000 }
},
{
"target": "manager_approval",
"condition": { "field": "amount", "operator": "lte", "value": 10000 }
},
{ "target": "manager_approval" }
]
},
"manager_approval": {
"type": "halting",
"presentation": {
"type": "form",
"title": "Manager Approval Required",
"description": "Purchase request for ${workflow_data.description} - $${workflow_data.amount}",
"fields": [
{ "name": "approved", "type": "boolean", "label": "Approve?", "required": true },
{ "name": "comment", "type": "string", "label": "Comment", "multiline": true }
]
},
"transitions": [
{
"target": "vp_approval",
"condition": {
"and": [
{ "field": "approved", "equals": true },
{ "field": "amount", "operator": "gt", "value": 10000 }
]
}
},
{
"target": "approved",
"condition": { "field": "approved", "equals": true }
},
{ "target": "rejected" }
]
},
"vp_approval": {
"type": "halting",
"presentation": {
"type": "form",
"title": "VP Approval Required",
"description": "High-value purchase: ${workflow_data.description} - $${workflow_data.amount}",
"fields": [
{ "name": "approved", "type": "boolean", "label": "VP Approval", "required": true },
{ "name": "budget_code", "type": "string", "label": "Budget Code" },
{ "name": "comment", "type": "string", "label": "Comment", "multiline": true }
]
},
"transitions": [
{
"target": "approved",
"condition": { "field": "approved", "equals": true }
},
{ "target": "rejected" }
]
},
"auto_approved": {
"type": "action",
"logic": {
"type": "script",
"code": "return { ...workflow_data, status: 'approved', approved_by: 'auto', approved_at: new Date().toISOString() }"
},
"transitions": [
{ "target": "notify_requester" }
]
},
"approved": {
"type": "action",
"logic": {
"type": "script",
"code": "return { ...workflow_data, status: 'approved', approved_at: new Date().toISOString() }"
},
"transitions": [
{ "target": "notify_requester" }
]
},
"rejected": {
"type": "action",
"logic": {
"type": "script",
"code": "return { ...workflow_data, status: 'rejected', rejected_at: new Date().toISOString() }"
},
"transitions": [
{ "target": "notify_requester" }
]
},
"notify_requester": {
"type": "action",
"logic": {
"type": "http",
"method": "POST",
"url": "https://yourapp.com/api/notifications",
"body": {
"type": "purchase_decision",
"email": "${workflow_data.requester}",
"status": "${workflow_data.status}",
"amount": "${workflow_data.amount}"
}
},
"transitions": [
{ "target": "complete" }
]
},
"complete": {
"type": "terminal"
}
}
}
Implementation Examples
Elixir Implementation (Phoenix)
Prerequisite: This module requires the
MyApp.Interactor.Clientmodule from theinteractor-authskill. See that skill for the HTTP client implementation.
defmodule MyApp.Interactor.Workflows do
@moduledoc """
Interactor Workflow management for state-machine based automations.
Requires MyApp.Interactor.Client from interactor-auth skill.
"""
alias MyApp.Interactor.Client
# ============ Workflow Definitions ============
@doc """
Create a new workflow definition.
"""
def create_workflow(definition) do
Client.post("/workflows", definition)
end
@doc """
Validate a workflow definition without saving.
"""
def validate_workflow(definition) do
Client.post("/workflows/validate", definition)
end
@doc """
List all workflows.
"""
def list_workflows do
case Client.get("/workflows") do
{:ok, %{"workflows" => workflows}} -> {:ok, workflows}
error -> error
end
end
@doc """
List versions for a workflow.
"""
def list_versions(workflow_name) do
case Client.get("/workflows/#{workflow_name}/versions") do
{:ok, %{"versions" => versions}} -> {:ok, versions}
error -> error
end
end
@doc """
Publish a workflow version.
"""
def publish_version(workflow_name, version_id) do
Client.post("/workflows/#{workflow_name}/versions/#{version_id}/publish", %{})
end
# ============ Instances ============
@doc """
Create a new workflow instance.
"""
def create_instance(workflow_name, user_id, input) do
Client.post("/workflows/#{workflow_name}/instances", %{
namespace: "user_#{user_id}",
input: input
})
end
@doc """
Get a workflow instance by ID.
"""
def get_instance(instance_id) do
Client.get("/workflows/instances/#{instance_id}")
end
@doc """
List workflow instances with optional filters.
"""
def list_instances(filters \\ %{}) do
query_params =
filters
|> Enum.map(fn
{:user_id, id} -> {"namespace", "user_#{id}"}
{:workflow_name, name} -> {"workflow_name", name}
{:status, status} -> {"status", status}
end)
|> URI.encode_query()
path = if query_params == "", do: "/workflows/instances", else: "/workflows/instances?#{query_params}"
case Client.get(path) do
{:ok, %{"instances" => instances}} -> {:ok, instances}
error -> error
end
end
@doc """
Resume a halted workflow instance with input.
"""
def resume_instance(instance_id, input) do
Client.post("/workflows/instances/#{instance_id}/resume", %{input: input})
end
@doc """
Cancel a workflow instance.
"""
def cancel_instance(instance_id) do
Client.post("/workflows/instances/#{instance_id}/cancel", %{})
end
# ============ Threads ============
@doc """
List threads for an instance.
"""
def list_threads(instance_id) do
case Client.get("/workflows/instances/#{instance_id}/threads") do
{:ok, %{"threads" => threads}} -> {:ok, threads}
error -> error
end
end
@doc """
Resume a specific thread.
"""
def resume_thread(instance_id, thread_id, input) do
Client.post(
"/workflows/instances/#{instance_id}/threads/#{thread_id}/resume",
%{input: input}
)
end
# ============ Helpers ============
@doc """
Wait for a workflow to complete or halt.
Returns {:ok, instance} when completed/halted, {:error, reason} on failure/timeout.
"""
def wait_for_completion(instance_id, opts \\ []) do
timeout_ms = Keyword.get(opts, :timeout, 300_000)
poll_interval_ms = Keyword.get(opts, :poll_interval, 2_000)
deadline = System.monotonic_time(:millisecond) + timeout_ms
do_wait_for_completion(instance_id, deadline, poll_interval_ms)
end
defp do_wait_for_completion(instance_id, deadline, poll_interval_ms) do
if System.monotonic_time(:millisecond) >= deadline do
{:error, :timeout}
else
case get_instance(instance_id) do
{:ok, %{"status" => "completed"} = instance} ->
{:ok, instance}
{:ok, %{"status" => "halted"} = instance} ->
{:ok, instance}
{:ok, %{"status" => "failed", "error" => error}} ->
{:error, {:workflow_failed, error}}
{:ok, %{"status" => "cancelled"}} ->
{:error, :cancelled}
{:ok, %{"status" => "running"}} ->
Process.sleep(poll_interval_ms)
do_wait_for_completion(instance_id, deadline, poll_interval_ms)
{:error, _} = error ->
error
end
end
end
end
Elixir Usage Example
alias MyApp.Interactor.Workflows
# Create and publish a workflow
{:ok, version} = Workflows.create_workflow(purchase_approval_definition)
{:ok, _published} = Workflows.publish_version("purchase_approval", version["version_id"])
# Start a new instance
{:ok, instance} = Workflows.create_instance(
"purchase_approval",
"user_123",
%{
id: "PO-2026-001",
amount: 5500,
requester: "john@example.com",
description: "Development laptop"
}
)
IO.puts("Workflow started: #{instance["id"]}")
IO.puts("Current state: #{instance["current_state"]}")
IO.puts("Status: #{instance["status"]}")
# Handle halted state
case instance["status"] do
"halted" ->
IO.puts("Waiting for approval...")
IO.inspect(instance["halting_presentation"], label: "Presentation")
# Simulate manager approval
{:ok, resumed} = Workflows.resume_instance(instance["id"], %{
approved: true,
comment: "Approved for Q1 budget"
})
IO.puts("New status: #{resumed["status"]}")
IO.puts("New state: #{resumed["current_state"]}")
_ ->
:ok
end
Elixir LiveView Integration
First, create a component to render workflow presentations dynamically:
defmodule MyAppWeb.WorkflowComponents do
use Phoenix.Component
@doc """
Renders a workflow form based on the halting presentation.
"""
attr :presentation, :map, required: true
attr :form, :any, required: true
def workflow_form(assigns) do
~H"""
<.form for={@form} phx-submit="submit_input" class="space-y-4">
<%= if @presentation["title"] do %>
<h2 class="text-xl font-semibold"><%= @presentation["title"] %></h2>
<% end %>
<%= if @presentation["description"] do %>
<p class="text-gray-600"><%= @presentation["description"] %></p>
<% end %>
<%= case @presentation["type"] do %>
<% "form" -> %>
<%= for field <- @presentation["fields"] || [] do %>
<.workflow_field field={field} form={@form} />
<% end %>
<% "choice" -> %>
<p class="font-medium"><%= @presentation["message"] %></p>
<div class="flex flex-wrap gap-2">
<%= for option <- @presentation["options"] || [] do %>
<button
type="submit"
name="input[choice]"
value={option["value"]}
class="px-4 py-2 bg-[#4CD964] hover:bg-[#3DBF55] text-white rounded-full"
>
<%= option["label"] %>
</button>
<% end %>
</div>
<% "message" -> %>
<p><%= @presentation["message"] %></p>
<% end %>
<%= if @presentation["type"] == "form" do %>
<button type="submit" class="px-6 py-2 bg-[#4CD964] hover:bg-[#3DBF55] text-white rounded-full">
Submit
</button>
<% end %>
</.form>
"""
end
attr :field, :map, required: true
attr :form, :any, required: true
defp workflow_field(assigns) do
~H"""
<div class="space-y-1">
<label class="block font-medium">
<%= @field["label"] %>
<%= if @field["required"], do: "*" %>
</label>
<%= case @field["type"] do %>
<% "string" -> %>
<%= if @field["multiline"] do %>
<textarea
name={"input[#{@field["name"]}]"}
class="w-full border rounded-lg p-2"
placeholder={@field["placeholder"]}
><%= @field["default"] %></textarea>
<% else %>
<input
type="text"
name={"input[#{@field["name"]}]"}
value={@field["default"]}
placeholder={@field["placeholder"]}
class="w-full border rounded-lg p-2"
/>
<% end %>
<% "number" -> %>
<input
type="number"
name={"input[#{@field["name"]}]"}
value={@field["default"]}
min={@field["min"]}
max={@field["max"]}
step={@field["step"]}
class="w-full border rounded-lg p-2"
/>
<% "boolean" -> %>
<input
type="checkbox"
name={"input[#{@field["name"]}]"}
value="true"
checked={@field["default"] == true}
class="h-5 w-5"
/>
<% "select" -> %>
<select name={"input[#{@field["name"]}]"} class="w-full border rounded-lg p-2">
<%= for option <- @field["options"] || [] do %>
<option value={option["value"]} selected={option["value"] == @field["default"]}>
<%= option["label"] %>
</option>
<% end %>
</select>
<% "date" -> %>
<input
type="date"
name={"input[#{@field["name"]}]"}
value={@field["default"]}
min={@field["minDate"]}
max={@field["maxDate"]}
class="w-full border rounded-lg p-2"
/>
<% _ -> %>
<input
type="text"
name={"input[#{@field["name"]}]"}
value={@field["default"]}
class="w-full border rounded-lg p-2"
/>
<% end %>
</div>
"""
end
end
Then import it in your LiveView:
defmodule MyAppWeb.WorkflowLive.Show do
use MyAppWeb, :live_view
import MyAppWeb.WorkflowComponents
alias MyApp.Interactor.Workflows
@impl true
def mount(%{"id" => instance_id}, _session, socket) do
if connected?(socket) do
# Subscribe to workflow updates via PubSub
Phoenix.PubSub.subscribe(MyApp.PubSub, "workflow:#{instance_id}")
end
case Workflows.get_instance(instance_id) do
{:ok, instance} ->
{:ok, assign(socket, instance: instance, form: to_form(%{}))}
{:error, _} ->
{:ok, push_navigate(socket, to: ~p"/workflows")}
end
end
@impl true
def handle_event("submit_input", %{"input" => input}, socket) do
instance_id = socket.assigns.instance["id"]
case Workflows.resume_instance(instance_id, input) do
{:ok, updated_instance} ->
{:noreply, assign(socket, instance: updated_instance)}
{:error, reason} ->
{:noreply, put_flash(socket, :error, "Failed to resume: #{inspect(reason)}")}
end
end
@impl true
def handle_event("cancel", _params, socket) do
instance_id = socket.assigns.instance["id"]
case Workflows.cancel_instance(instance_id) do
{:ok, _} ->
{:noreply, push_navigate(socket, to: ~p"/workflows")}
{:error, reason} ->
{:noreply, put_flash(socket, :error, "Failed to cancel: #{inspect(reason)}")}
end
end
@impl true
def handle_info({:workflow_updated, instance}, socket) do
{:noreply, assign(socket, instance: instance)}
end
@impl true
def render(assigns) do
~H"""
<div class="workflow-instance">
<h1>Workflow: <%= @instance["workflow_name"] %></h1>
<p>Status: <span class={status_class(@instance["status"])}><%= @instance["status"] %></span></p>
<p>Current State: <%= @instance["current_state"] %></p>
<%= if @instance["status"] == "halted" do %>
<.workflow_form
presentation={@instance["halting_presentation"]}
form={@form}
/>
<% end %>
<%= if @instance["status"] in ["running", "halted"] do %>
<button phx-click="cancel" class="btn-secondary">Cancel Workflow</button>
<% end %>
</div>
"""
end
defp status_class("completed"), do: "text-green-600"
defp status_class("failed"), do: "text-red-600"
defp status_class("cancelled"), do: "text-gray-600"
defp status_class("halted"), do: "text-yellow-600"
defp status_class(_), do: "text-blue-600"
end
TypeScript Implementation
import { InteractorClient } from './interactor-client';
export class WorkflowManager {
private client: InteractorClient;
constructor(client: InteractorClient) {
this.client = client;
}
// ============ Workflow Definitions ============
async createWorkflow(definition: WorkflowDefinition): Promise<WorkflowVersion> {
return this.client.request('POST', '/workflows', definition);
}
async validateWorkflow(definition: WorkflowDefinition): Promise<ValidationResult> {
return this.client.request('POST', '/workflows/validate', definition);
}
async listWorkflows(): Promise<Workflow[]> {
const result = await this.client.request<{ workflows: Workflow[] }>('GET', '/workflows');
return result.workflows;
}
async listVersions(workflowName: string): Promise<WorkflowVersion[]> {
const result = await this.client.request<{ versions: WorkflowVersion[] }>(
'GET',
`/workflows/${workflowName}/versions`
);
return result.versions;
}
async publishVersion(workflowName: string, versionId: string): Promise<WorkflowVersion> {
return this.client.request(
'POST',
`/workflows/${workflowName}/versions/${versionId}/publish`
);
}
// ============ Instances ============
async createInstance(
workflowName: strin
…(truncated)