Claude-skill-registry interactor-workflows
Build state-machine based automation with human-in-the-loop support through Interactor. Use when implementing approval flows, multi-step processes, automated pipelines, or any workflow requiring user input at specific stages.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/interactor-workflows" ~/.claude/skills/majiayu000-claude-skill-registry-interactor-workflows && rm -rf "$T"
skills/data/interactor-workflows/SKILL.mdInteractor 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
skill)interactor-auth - 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 |
|---|---|---|
| Executes logic automatically | Runs logic, then transitions immediately |
| Pauses for external input | Waits for call with user input |
| End state | Workflow completes, no further transitions |
Note: The
property shown in terminal states (for triggering HTTP callbacks on completion) is an optional enhancement. Verify availability with your Interactor version.on_enter
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 |
|---|---|
| Actively executing (in an action state) |
| Paused, waiting for external input |
| Finished successfully (reached terminal state) |
| Terminated due to error |
| Manually cancelled |
List Instances
curl https://core.interactor.com/api/v1/workflows/instances \ -H "Authorization: Bearer <token>"
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| string | Filter by namespace |
| string | Filter by workflow |
| string | , , , , |
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 |
|---|---|---|
| integer | Max events (default: 100, max: 1000) |
| string | Pagination cursor |
| string | Filter by type: , , , , |
| ISO8601 | Events after this timestamp |
| ISO8601 | Events before this timestamp |
| string | Filter to specific thread |
| 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 |
|---|---|
| Message to display (AI-generated or static) |
| if AI-generated, if static |
| Available transitions for selection mode |
Halting Presentations (Legacy)
Note: The
format is still supported for backward compatibility. New workflows should usepresentationabove.halting_instructions
When a workflow halts, specify how to present the required input to users.
Note: The
andtitlefields shown in presentations are optional enhancements for better UX. The core API requires onlydescriptionand the type-specific fields (type,fields, oroptions).message
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
field is an optional UI hint. Client implementations may ignore it if not supported.show_progress
Field Types
| Type | Description | Additional Properties |
|---|---|---|
| Text input | , , |
| Numeric input | , , |
| Checkbox/toggle | - |
| Dropdown selection | array |
| Date picker | , |
| File upload | , |
Note: Common field properties include
,required, anddefault. Additional properties likelabel,placeholder,stepmay vary by Interactor version. Test withmaxLengthendpoint to confirm supported properties./validate
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:
- The input provided when starting or resuming the workflowinput
- Current accumulated workflow dataworkflow_data
- Additional context (namespace, instance_id, etc.)context
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
andtimeoutproperties are optional enhancements. The core API requires onlyretry,type,method, and optionallyurlandheaders.body
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 |
|---|---|---|
| Exact match | |
| Not equal | |
| Greater than | |
| Greater than or equal | |
| Less than | |
| Less than or equal | |
| String contains | |
| Value in array | |
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
module from theMyApp.Interactor.Clientskill. See that skill for the HTTP client implementation.interactor-auth
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: string, userId: string, input: Record<string, any> ): Promise<WorkflowInstance> { return this.client.request('POST', `/workflows/${workflowName}/instances`, { namespace: `user_${userId}`, input }); } async getInstance(instanceId: string): Promise<WorkflowInstance> { return this.client.request('GET', `/workflows/instances/${instanceId}`); } async listInstances(filters?: { userId?: string; workflowName?: string; status?: InstanceStatus; }): Promise<WorkflowInstance[]> { const params = new URLSearchParams(); if (filters?.userId) params.set('namespace', `user_${filters.userId}`); if (filters?.workflowName) params.set('workflow_name', filters.workflowName); if (filters?.status) params.set('status', filters.status); const query = params.toString(); const result = await this.client.request<{ instances: WorkflowInstance[] }>( 'GET', `/workflows/instances${query ? '?' + query : ''}` ); return result.instances; } async resumeInstance( instanceId: string, input: Record<string, any> ): Promise<WorkflowInstance> { return this.client.request('POST', `/workflows/instances/${instanceId}/resume`, { input }); } async cancelInstance(instanceId: string): Promise<void> { await this.client.request('POST', `/workflows/instances/${instanceId}/cancel`); } // ============ Threads ============ async listThreads(instanceId: string): Promise<WorkflowThread[]> { const result = await this.client.request<{ threads: WorkflowThread[] }>( 'GET', `/workflows/instances/${instanceId}/threads` ); return result.threads; } async resumeThread( instanceId: string, threadId: string, input: Record<string, any> ): Promise<WorkflowInstance> { return this.client.request( 'POST', `/workflows/instances/${instanceId}/threads/${threadId}/resume`, { input } ); } // ============ Helpers ============ async waitForCompletion( instanceId: string, timeoutMs: number = 300000, pollIntervalMs: number = 2000 ): Promise<WorkflowInstance> { const startTime = Date.now(); while (Date.now() - startTime < timeoutMs) { const instance = await this.getInstance(instanceId); if (instance.status === 'completed') { return instance; } if (instance.status === 'failed') { throw new Error(`Workflow failed: ${instance.error}`); } if (instance.status === 'cancelled') { throw new Error('Workflow was cancelled'); } if (instance.status === 'halted') { // Workflow is waiting for input return instance; } await new Promise(resolve => setTimeout(resolve, pollIntervalMs)); } throw new Error('Workflow completion timed out'); } } // Types interface WorkflowDefinition { name: string; initial_state: string; ai_guidance?: string; states: Record<string, StateDefinition>; } interface StateDefinition { type: 'action' | 'halting' | 'terminal'; logic?: LogicDefinition; presentation?: PresentationDefinition; transitions?: TransitionDefinition[]; on_enter?: LogicDefinition; // Optional - verify availability with your Interactor version } interface LogicDefinition { type: 'script' | 'http'; code?: string; // For script type method?: string; // For http type url?: string; // For http type headers?: Record<string, string>; // For http type body?: any; // For http type timeout?: number; // Optional - for http type retry?: { attempts: number; backoff: 'exponential' | 'linear' }; // Optional - for http type } interface PresentationDefinition { type: 'form' | 'choice' | 'message'; title?: string; // Optional - for better UX description?: string; // Optional - for better UX message?: string; // For choice and message types fields?: FieldDefinition[]; // For form type options?: OptionDefinition[]; // For choice type show_progress?: boolean; // Optional - for message type (UI hint) } interface FieldDefinition { name: string; type: 'string' | 'number' | 'boolean' | 'select' | 'date' | 'file'; label: string; required?: boolean; default?: any; // String type properties multiline?: boolean; placeholder?: string; maxLength?: number; // Number type properties min?: number; max?: number; step?: number; // Select type properties options?: OptionDefinition[]; // Date type properties minDate?: string; maxDate?: string; // File type properties accept?: string; maxSize?: number; } interface OptionDefinition { value: string; label: string; description?: string; } interface TransitionDefinition { target: string; condition?: ConditionDefinition; } interface ConditionDefinition { field?: string; operator?: 'equals' | 'not_equals' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains' | 'in'; equals?: any; not_equals?: any; value?: any; and?: ConditionDefinition[]; or?: ConditionDefinition[]; } interface Workflow { name: string; latest_version_id: string; published_version_id?: string; created_at: string; } interface WorkflowVersion { version_id: string; status: 'draft' | 'published'; created_at: string; published_at?: string; } interface ValidationResult { valid: boolean; errors?: Array<{ path: string; message: string }>; } type InstanceStatus = 'running' | 'halted' | 'completed' | 'failed' | 'cancelled'; interface WorkflowInstance { id: string; workflow_name: string; version_id: string; namespace: string; status: InstanceStatus; current_state: string; workflow_data: Record<string, any>; halting_presentation?: PresentationDefinition; threads: WorkflowThread[]; history: HistoryEntry[]; error?: string; created_at: string; } interface WorkflowThread { id: string; status: 'running' | 'halted' | 'completed'; current_state: string; } interface HistoryEntry { state: string; entered_at: string; exited_at?: string; transition?: string; }
Usage Example
const workflowManager = new WorkflowManager(interactorClient); // Create and publish a workflow const version = await workflowManager.createWorkflow(purchaseApprovalDefinition); await workflowManager.publishVersion('purchase_approval', version.version_id); // Start a new instance const instance = await workflowManager.createInstance( 'purchase_approval', 'user_123', { id: 'PO-2026-001', amount: 5500, requester: 'john@example.com', description: 'Development laptop' } ); console.log(`Workflow started: ${instance.id}`); console.log(`Current state: ${instance.current_state}`); console.log(`Status: ${instance.status}`); if (instance.status === 'halted') { console.log('Waiting for approval...'); console.log('Presentation:', instance.halting_presentation); // Simulate manager approval const resumed = await workflowManager.resumeInstance(instance.id, { approved: true, comment: 'Approved for Q1 budget' }); console.log(`New status: ${resumed.status}`); console.log(`New state: ${resumed.current_state}`); }
Error Handling
Workflow-Specific Errors
Common workflow errors and their resolutions:
| Error Code | HTTP Status | Description | Resolution |
|---|---|---|---|
| 404 | Workflow definition doesn't exist | Check workflow name |
| 400 | No published version available | Publish a version first |
| 404 | Version doesn't exist | Check version ID |
| 404 | Instance doesn't exist | Check instance ID |
| 400 | Cannot resume - not halted | Check instance status |
| 400 | Input doesn't match any condition | Check transition conditions |
| 500 | Error executing workflow script | Check script syntax |
| 500 | HTTP action failed | Check endpoint and auth |
See Also: The API Reference section contains the complete canonical error table with all endpoint-specific error codes.
Webhook Events & Subscription Management
Available Events
| Event | Description | Triggered When |
|---|---|---|
| New instance started | Instance created via API |
| Waiting for input | Instance reaches halting state |
| Instance resumed | Resume called with input |
| Finished successfully | Instance reaches terminal state |
| Terminated with error | Script/HTTP error or invalid transition |
| Manually cancelled | Cancel endpoint called |
Create Webhook Subscription
curl -X POST https://core.interactor.com/api/v1/webhooks/subscriptions \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "url": "https://yourapp.com/api/webhooks/workflows", "events": [ "workflow.instance.halted", "workflow.instance.completed", "workflow.instance.failed" ], "secret": "whsec_your_webhook_secret_min_32_chars", "namespace_filter": "user_123" }'
Response:
{ "data": { "id": "whsub_abc123", "url": "https://yourapp.com/api/webhooks/workflows", "events": ["workflow.instance.halted", "workflow.instance.completed", "workflow.instance.failed"], "namespace_filter": "user_123", "status": "active", "created_at": "2026-01-20T12:00:00Z" } }
List Webhook Subscriptions
curl https://core.interactor.com/api/v1/webhooks/subscriptions \ -H "Authorization: Bearer <token>"
Delete Webhook Subscription
curl -X DELETE https://core.interactor.com/api/v1/webhooks/subscriptions/whsub_abc123 \ -H "Authorization: Bearer <token>"
Webhook Delivery Headers
Every webhook delivery includes these headers:
| Header | Description | Example |
|---|---|---|
| Event type | |
| HMAC-SHA256 signature | |
| Unique delivery ID | |
| Unix timestamp | |
| Retry attempt (0-based) | |
| Always JSON | |
Webhook Payload Example (workflow.instance.halted
)
workflow.instance.halted{ "event": "workflow.instance.halted", "delivery_id": "del_01F8B6XY...", "timestamp": "2026-01-20T12:00:01Z", "data": { "instance_id": "inst_xyz", "workflow_name": "approval_workflow", "version_id": "v_abc123", "namespace": "user_123", "current_state": "await_approval", "workflow_data": { "request_id": "req_456", "amount": 5000, "status": "pending" }, "halting_presentation": { "type": "form", "title": "Approval Required", "fields": [ {"name": "approved", "type": "boolean", "label": "Approve this request?"}, {"name": "comment", "type": "string", "label": "Comment"} ] } } }
Webhook Payload Example (workflow.instance.completed
)
workflow.instance.completed{ "event": "workflow.instance.completed", "delivery_id": "del_02G9C7ZW...", "timestamp": "2026-01-20T12:05:00Z", "data": { "instance_id": "inst_xyz", "workflow_name": "approval_workflow", "version_id": "v_abc123", "namespace": "user_123", "current_state": "approved", "workflow_data": { "request_id": "req_456", "amount": 5000, "status": "approved", "approved_at": "2026-01-20T12:05:00Z" } } }
Webhook Signature Verification
Always verify webhook signatures to ensure requests are from Interactor.
Node.js/TypeScript:
import crypto from 'crypto'; function verifyWebhookSignature( payload: string | Buffer, signature: string, secret: string, timestamp: string ): boolean { // Protect against replay attacks (reject if older than 5 minutes) const timestampAge = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10); if (timestampAge > 300) { return false; } // Compute expected signature const signedPayload = `${timestamp}.${payload}`; const expectedSignature = 'sha256=' + crypto .createHmac('sha256', secret) .update(signedPayload) .digest('hex'); // Constant-time comparison to prevent timing attacks return crypto.timingSafeEqual( Buffer.from(expectedSignature), Buffer.from(signature) ); } // Express middleware example app.post('/webhooks/workflows', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-interactor-signature'] as string; const timestamp = req.headers['x-interactor-timestamp'] as string; if (!verifyWebhookSignature(req.body, signature, process.env.WEBHOOK_SECRET!, timestamp)) { return res.status(401).json({ error: 'Invalid signature' }); } const event = JSON.parse(req.body.toString()); // Process event... res.status(200).json({ received: true }); });
Elixir/Phoenix:
defmodule MyAppWeb.WebhookController do use MyAppWeb, :controller @webhook_secret Application.compile_env(:my_app, :webhook_secret) @max_age_seconds 300 def handle(conn, _params) do signature = get_req_header(conn, "x-interactor-signature") |> List.first() timestamp = get_req_header(conn, "x-interactor-timestamp") |> List.first() {:ok, body, conn} = read_body(conn) case verify_signature(body, signature, timestamp) do :ok -> event = Jason.decode!(body) process_event(event) json(conn, %{received: true}) {:error, reason} -> conn |> put_status(401) |> json(%{error: reason}) end end defp verify_signature(payload, signature, timestamp) do with :ok <- verify_timestamp(timestamp), :ok <- verify_hmac(payload, signature, timestamp) do :ok end end defp verify_timestamp(timestamp) do timestamp_int = String.to_integer(timestamp) age = System.system_time(:second) - timestamp_int if age <= @max_age_seconds do :ok else {:error, "Timestamp too old"} end end defp verify_hmac(payload, signature, timestamp) do signed_payload = "#{timestamp}.#{payload}" expected = "sha256=" <> Base.encode16( :crypto.mac(:hmac, :sha256, @webhook_secret, signed_payload), case: :lower ) if Plug.Crypto.secure_compare(expected, signature) do :ok else {:error, "Invalid signature"} end end defp process_event(%{"event" => "workflow.instance.halted"} = event) do # Handle halted workflow - notify user, etc. IO.inspect(event, label: "Workflow halted") end defp process_event(%{"event" => "workflow.instance.completed"} = event) do # Handle completed workflow IO.inspect(event, label: "Workflow completed") end defp process_event(event) do IO.inspect(event, label: "Unknown event") end end
Webhook Retry Policy
| Attempt | Delay | Cumulative Time |
|---|---|---|
| 1 | Immediate | 0s |
| 2 | 30 seconds | 30s |
| 3 | 2 minutes | 2m 30s |
| 4 | 8 minutes | 10m 30s |
| 5 | 30 minutes | 40m 30s |
| 6 | 2 hours | 2h 40m 30s |
- Webhooks are retried up to 6 times with exponential backoff
- Return
status to acknowledge receipt2xx - Non-2xx responses or timeouts (30s) trigger retries
- After all retries fail, the event is moved to a dead-letter queue
- Use the webhook dashboard to replay failed events
See
interactor-webhooks skill for complete webhook management.
Authentication & Authorization
Required OAuth Scopes
| Scope | Description | Required For |
|---|---|---|
| Read workflow definitions and instances | GET endpoints |
| Create/modify workflows and instances | POST/PUT/DELETE endpoints |
| Create and resume instances | Instance operations |
| Manage webhook subscriptions | Webhook endpoints |
Token Format
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Token Refresh
Tokens expire after 1 hour. Refresh before expiry:
curl -X POST https://auth.interactor.com/oauth/token \ -H "Content-Type: application/json" \ -d '{ "grant_type": "refresh_token", "refresh_token": "<refresh_token>", "client_id": "<client_id>" }'
Namespace Authorization
- Instances are isolated by
namespace - Tokens can only access instances in namespaces they own
- Use
convention for user-specific workflowsuser_{user_id} - Use
convention for organization-wide workflowsorg_{org_id} - Service accounts can access all namespaces within their account
See
interactor-auth skill for complete authentication setup.
Idempotency & Concurrency
Idempotency Keys
Use
Idempotency-Key header to prevent duplicate operations:
curl -X POST https://core.interactor.com/api/v1/workflows/approval_workflow/instances \ -H "Authorization: Bearer <token>" \ -H "Idempotency-Key: order_123_approval_v1" \ -H "Content-Type: application/json" \ -d '{ "namespace": "user_123", "input": {"order_id": "order_123", "amount": 5000} }'
Behavior:
- If the same
is used within 24 hours, the original response is returnedIdempotency-Key - Keys are scoped to the authenticated account
- Use deterministic keys based on business identifiers (e.g.,
){order_id}_approval
Supported Endpoints:
(create instance)POST /workflows/{name}/instances
(resume instance)POST /workflows/instances/{id}/resume
(resume thread)POST /workflows/instances/{id}/threads/{thread_id}/resume
Concurrent Resume Handling
When multiple resume requests arrive simultaneously:
| Scenario | Behavior |
|---|---|
| Same instance, same input | Second request returns same result (idempotent) |
| Same instance, different input | First request wins, second gets |
| Different threads, same instance | Both processed (parallel execution) |
Conflict Response:
{ "error": { "code": "concurrent_modification", "message": "Instance was modified by another request", "details": { "current_state": "approved", "expected_state": "await_approval" }, "request_id": "req_01F8B6..." } }
Limits & Quotas
Workflow Definition Limits
| Limit | Value | Notes |
|---|---|---|
| Max states per workflow | 100 | Including terminal states |
| Max transitions per state | 20 | Evaluated in order |
| Max workflow name length | 64 chars | Alphanumeric, underscores, hyphens |
| Max script code size | 64 KB | Per script logic block |
| Max presentation fields | 50 | Per halting state |
| Max workflow definition size | 1 MB | Total JSON size |
Instance Limits
| Limit | Value | Notes |
|---|---|---|
Max size | 256 KB | Accumulated across states |
| Max input payload size | 64 KB | Per resume/create call |
| Max concurrent threads | 10 | Per instance |
| Instance TTL (running) | 30 days | Auto-cancelled after |
| Instance TTL (halted) | 90 days | Auto-cancelled after |
| Max instances per namespace | 10,000 | Active instances |
File Upload Limits (for file
field type)
file| Limit | Value |
|---|---|
| Max file size | 10 MB |
| Allowed MIME types | Configurable per field |
| Max files per field | 5 |
| File retention | 7 days after instance completion |
Rate Limits
| Endpoint Category | Limit | Window |
|---|---|---|
| Read operations | 1000 req | per minute |
| Write operations | 100 req | per minute |
| Instance creation | 50 req | per minute |
| Webhook deliveries | 1000 events | per minute per subscription |
Rate Limit Headers:
X-RateLimit-Limit: 100 X-RateLimit-Remaining: 95 X-RateLimit-Reset: 1705752060
Script Execution Environment
Runtime Specification
| Property | Value |
|---|---|
| Runtime | JavaScript (ES2020) |
| Engine | QuickJS sandbox |
| Execution timeout | 5 seconds |
| Memory limit | 16 MB |
| Network access | Disabled (use HTTP logic instead) |
Available Globals
// Available in script context input // Object: Input from create/resume call workflow_data // Object: Accumulated workflow data context // Object: { namespace, instance_id, workflow_name, state_name } // Standard JavaScript JSON // JSON.parse, JSON.stringify Date // Date constructor and methods Math // Math utilities console // console.log (for debugging, logged to instance history) Array // Array methods Object // Object methods String // String methods Number // Number methods Boolean // Boolean type RegExp // Regular expressions // NOT available (for security) fetch // Use HTTP logic instead require // No module imports eval // Disabled Function // Constructor disabled setTimeout // Async not supported setInterval // Async not supported
Accessing Secrets in Scripts
Secrets are accessed via the
secrets object (read-only):
// In script logic const apiKey = secrets.MY_API_KEY; return { ...workflow_data, api_key_present: !!apiKey };
Security: Secrets are injected at runtime and never logged. Use HTTP logic for external calls requiring secrets.
Script Best Practices
- Keep scripts simple and fast (<100ms recommended)
- Avoid loops over large datasets
- Delegate heavy computation to HTTP endpoints
- Use
sparingly (logs are stored in instance history)console.log - Return plain objects (no functions or circular references)
Observability & Tracing
Correlation IDs
Every API request returns a unique request ID:
X-Request-Id: req_01F8B6XY9Z...
Include this ID when contacting support or debugging issues.
Propagating Trace Context
Pass trace context to correlate across services:
curl -X POST https://core.interactor.com/api/v1/workflows/approval_workflow/instances \ -H "Authorization: Bearer <token>" \ -H "X-Request-Id: your-correlation-id-123" \ -H "traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
The
traceparent header (W3C Trace Context) is propagated to HTTP logic actions.
Instance History & Logs
Each instance maintains a detailed execution history:
{ "history": [ { "state": "request", "entered_at": "2026-01-20T12:00:00Z", "exited_at": "2026-01-20T12:00:01Z", "transition": "await_approval", "logs": ["Processing request req_456"], "duration_ms": 45 }, { "state": "await_approval", "entered_at": "2026-01-20T12:00:01Z", "input_received": {"approved": true, "comment": "LGTM"}, "resumed_at": "2026-01-20T12:05:00Z", "resumed_by": "user_789" } ] }
Metrics Available
| Metric | Description |
|---|---|
| Counter: instances created |
| Counter: instances completed |
| Counter: instances failed |
| Histogram: time in each state |
| Histogram: script execution time |
| Histogram: HTTP action duration |
Access metrics via the Interactor dashboard or export to your observability platform.
Best Practices
DO
- Start simple - Begin with linear workflows, add complexity as needed
- Use meaningful state names -
overawait_manager_approvalstate_3 - Validate early - Use
endpoint during development/validate - Version carefully - Publish new versions rather than modifying existing ones
- Handle all paths - Ensure every state has a valid transition or is terminal
- Use namespaces - Isolate workflow instances per user
- Add AI guidance - Help AI assistants understand your workflow's purpose
DON'T
- Don't modify published versions - Create new versions instead
- Don't create orphan states - Every state should be reachable
- Don't forget error handling - Add appropriate error states
- Don't use complex scripts - Keep logic simple, move complexity to HTTP endpoints
Output Format
When implementing workflows, provide this summary:
## Workflow Implementation Report **Date**: YYYY-MM-DD **Workflow**: purchase_approval ### Definition | Property | Value | |----------|-------| | Name | purchase_approval | | Version | v_abc123 | | Status | Published | | States | 7 | | Halting States | 2 | ### State Flow
submit → manager_approval → [vp_approval] → approved → notify → complete ↘ rejected → notify → complete
### Implementation Checklist - [ ] Workflow definition created - [ ] Validation passed - [ ] Version published - [ ] Instance creation tested - [ ] Resume functionality tested - [ ] All transitions verified - [ ] Webhook handlers configured - [ ] Error handling implemented ### Test Scenarios | Scenario | Input | Expected Path | Status | |----------|-------|---------------|--------| | Auto-approve | amount: 500 | submit → auto_approved → complete | ✓ | | Manager only | amount: 5000 | submit → manager → approved → complete | ✓ | | VP required | amount: 15000 | submit → manager → vp → approved → complete | ✓ | | Rejected | amount: 5000, approved: false | submit → manager → rejected → complete | ✓ |
API Reference
Endpoint Summary
| Method | Endpoint | Auth | Success | Description |
|---|---|---|---|---|
| | | | Create workflow definition |
| | | | Validate without saving |
| | | | List all workflows |
| | | | List workflow versions |
| | | | Publish a version |
| | | | Create instance |
| | | | List instances |
| | | | Get instance details |
| | | | Resume halted instance |
| | | | Cancel instance |
| | | | List threads |
| | | | Resume thread |
| | | | Create webhook |
| | | | List webhooks |
| | | | Delete webhook |
Error Response Format
All errors follow this standardized format:
{ "error": { "code": "workflow_not_found", "message": "Workflow 'invalid_workflow' not found", "details": null, "request_id": "req_01F8B6XY9Z..." } }
Error Codes by Endpoint
| Endpoint | Error Code | HTTP | Description |
|---|---|---|---|
| All | | 401 | Missing or invalid token |
| All | | 403 | Insufficient scopes |
| All | | 429 | Rate limit exceeded |
| All | | 500 | Server error |
| | 400 | Schema validation failed |
| | 409 | Name already taken |
| | 404 | Workflow doesn't exist |
| | 404 | Version doesn't exist |
| | 400 | Version already published |
| | 400 | No published version |
| | 429 | Too many instances |
| | 404 | Instance doesn't exist |
| | 400 | Instance not in halted state |
| | 400 | Input doesn't match conditions |
| | 409 | Race condition |
| | 400 | Already completed/failed |
| Script execution | | 500 | Runtime error in script |
| Script execution | | 500 | Script exceeded 5s limit |
| HTTP logic | | 500 | External request failed |
| HTTP logic | | 500 | External request timed out |
React Component Example
Render
halting_presentation in React applications:
import React from 'react'; interface Field { name: string; type: 'string' | 'number' | 'boolean' | 'select' | 'date' | 'file'; label: string; required?: boolean; default?: any; multiline?: boolean; placeholder?: string; options?: { value: string; label: string }[]; min?: number; max?: number; } interface Option { value: string; label: string; description?: string; } interface Presentation { type: 'form' | 'choice' | 'message'; title?: string; description?: string; message?: string; fields?: Field[]; options?: Option[]; show_progress?: boolean; } interface WorkflowFormProps { presentation: Presentation; onSubmit: (input: Record<string, any>) => void; onCancel?: () => void; isSubmitting?: boolean; } export function WorkflowForm({ presentation, onSubmit, onCancel, isSubmitting = false }: WorkflowFormProps) { const [formData, setFormData] = React.useState<Record<string, any>>(() => { // Initialize with defaults const defaults: Record<string, any> = {}; presentation.fields?.forEach(field => { if (field.default !== undefined) { defaults[field.name] = field.default; } }); return defaults; }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); onSubmit(formData); }; const handleChoice = (value: string) => { onSubmit({ choice: value }); }; const updateField = (name: string, value: any) => { setFormData(prev => ({ ...prev, [name]: value })); }; return ( <div className="workflow-form bg-white rounded-2xl shadow-md p-6"> {presentation.title && ( <h2 className="text-xl font-semibold mb-2">{presentation.title}</h2> )} {presentation.description && ( <p className="text-gray-600 mb-4">{presentation.description}</p> )} {presentation.type === 'form' && ( <form onSubmit={handleSubmit} className="space-y-4"> {presentation.fields?.map(field => ( <FormField key={field.name} field={field} value={formData[field.name]} onChange={(value) => updateField(field.name, value)} /> ))} <div className="flex gap-3 pt-4"> <button type="submit" disabled={isSubmitting} className="px-6 py-2 bg-[#4CD964] hover:bg-[#3DBF55] text-white rounded-full font-medium disabled:opacity-50" > {isSubmitting ? 'Submitting...' : 'Submit'} </button> {onCancel && ( <button type="button" onClick={onCancel} className="px-6 py-2 border border-gray-300 rounded-full font-medium hover:bg-gray-50" > Cancel </button> )} </div> </form> )} {presentation.type === 'choice' && ( <div className="space-y-3"> {presentation.message && ( <p className="font-medium">{presentation.message}</p> )} <div className="flex flex-wrap gap-2"> {presentation.options?.map(option => ( <button key={option.value} onClick={() => handleChoice(option.value)} disabled={isSubmitting} className="px-4 py-2 bg-[#4CD964] hover:bg-[#3DBF55] text-white rounded-full disabled:opacity-50" title={option.description} > {option.label} </button> ))} </div> </div> )} {presentation.type === 'message' && ( <div className="text-center py-4"> <p>{presentation.message}</p> {presentation.show_progress && ( <div className="mt-4"> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#4CD964] mx-auto" /> </div> )} </div> )} </div> ); } function FormField({ field, value, onChange }: { field: Field; value: any; onChange: (value: any) => void; }) { const baseInputClass = "w-full border rounded-lg p-2 focus:ring-2 focus:ring-[#4CD964] focus:border-transparent"; return ( <div className="space-y-1"> <label className="block font-medium text-gray-700"> {field.label} {field.required && <span className="text-red-500 ml-1">*</span>} </label> {field.type === 'string' && !field.multiline && ( <input type="text" value={value || ''} onChange={(e) => onChange(e.target.value)} placeholder={field.placeholder} required={field.required} className={baseInputClass} /> )} {field.type === 'string' && field.multiline && ( <textarea value={value || ''} onChange={(e) => onChange(e.target.value)} placeholder={field.placeholder} required={field.required} rows={4} className={baseInputClass} /> )} {field.type === 'number' && ( <input type="number" value={value ?? ''} onChange={(e) => onChange(e.target.valueAsNumber)} min={field.min} max={field.max} required={field.required} className={baseInputClass} /> )} {field.type === 'boolean' && ( <input type="checkbox" checked={value || false} onChange={(e) => onChange(e.target.checked)} className="h-5 w-5 text-[#4CD964] rounded focus:ring-[#4CD964]" /> )} {field.type === 'select' && ( <select value={value || ''} onChange={(e) => onChange(e.target.value)} required={field.required} className={baseInputClass} > <option value="">Select...</option> {field.options?.map(opt => ( <option key={opt.value} value={opt.value}>{opt.label}</option> ))} </select> )} {field.type === 'date' && ( <input type="date" value={value || ''} onChange={(e) => onChange(e.target.value)} required={field.required} className={baseInputClass} /> )} {field.type === 'file' && ( <input type="file" onChange={(e) => onChange(e.target.files?.[0])} required={field.required} className={baseInputClass} /> )} </div> ); } // Usage example function ApprovalPage({ instanceId }: { instanceId: string }) { const [instance, setInstance] = React.useState<any>(null); const [isSubmitting, setIsSubmitting] = React.useState(false); React.useEffect(() => { fetch(`/api/workflows/instances/${instanceId}`) .then(res => res.json()) .then(data => setInstance(data.data)); }, [instanceId]); const handleSubmit = async (input: Record<string, any>) => { setIsSubmitting(true); try { const res = await fetch(`/api/workflows/instances/${instanceId}/resume`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ input }) }); const updated = await res.json(); setInstance(updated.data); } finally { setIsSubmitting(false); } }; if (!instance) return <div>Loading...</div>; if (instance.status !== 'halted') return <div>Workflow not awaiting input</div>; return ( <WorkflowForm presentation={instance.halting_presentation} onSubmit={handleSubmit} isSubmitting={isSubmitting} /> ); }
JSON Schema Appendix
Workflow Definition Schema
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://core.interactor.com/schemas/workflow-definition.json", "title": "Workflow Definition", "type": "object", "required": ["name", "initial_state", "states"], "properties": { "name": { "type": "string", "pattern": "^[a-z][a-z0-9_-]{0,63}$", "description": "Unique workflow identifier" }, "initial_state": { "type": "string", "description": "Starting state name" }, "ai_guidance": { "type": "string", "maxLength": 1000, "description": "Instructions for AI assistants" }, "states": { "type": "object", "additionalProperties": { "$ref": "#/$defs/state" }, "minProperties": 1, "maxProperties": 100 } }, "$defs": { "state": { "type": "object", "required": ["type"], "properties": { "type": { "enum": ["action", "halting", "terminal"] }, "logic": { "$ref": "#/$defs/logic" }, "presentation": { "$ref": "#/$defs/presentation" }, "transitions": { "type": "array", "items": { "$ref": "#/$defs/transition" }, "maxItems": 20 }, "on_enter": { "$ref": "#/$defs/logic", "description": "Optional (v2.0.0+): Logic to execute when entering this state" } } }, "logic": { "type": "object", "required": ["type"], "oneOf": [ { "properties": { "type": { "const": "script" }, "code": { "type": "string", "maxLength": 65536 } }, "required": ["type", "code"] }, { "properties": { "type": { "const": "http" }, "method": { "enum": ["GET", "POST", "PUT", "PATCH", "DELETE"] }, "url": { "type": "string", "format": "uri" }, "headers": { "type": "object" }, "body": {}, "timeout": { "type": "integer", "minimum": 1000, "maximum": 30000 }, "retry": { "type": "object", "properties": { "attempts": { "type": "integer", "minimum": 1, "maximum": 5 }, "backoff": { "enum": ["linear", "exponential"] } } } }, "required": ["type", "method", "url"] } ] }, "presentation": { "type": "object", "required": ["type"], "oneOf": [ { "properties": { "type": { "const": "form" }, "title": { "type": "string" }, "description": { "type": "string" }, "fields": { "type": "array", "items": { "$ref": "#/$defs/field" }, "maxItems": 50 } }, "required": ["type", "fields"] }, { "properties": { "type": { "const": "choice" }, "title": { "type": "string" }, "message": { "type": "string" }, "options": { "type": "array", "items": { "$ref": "#/$defs/option" }, "minItems": 2, "maxItems": 20 } }, "required": ["type", "options"] }, { "properties": { "type": { "const": "message" }, "title": { "type": "string" }, "message": { "type": "string" }, "show_progress": { "type": "boolean" } }, "required": ["type", "message"] } ] }, "field": { "type": "object", "required": ["name", "type", "label"], "properties": { "name": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" }, "type": { "enum": ["string", "number", "boolean", "select", "date", "file"] }, "label": { "type": "string" }, "required": { "type": "boolean", "default": false }, "default": {}, "placeholder": { "type": "string" }, "multiline": { "type": "boolean" }, "maxLength": { "type": "integer" }, "min": { "type": "number" }, "max": { "type": "number" }, "step": { "type": "number" }, "minDate": { "type": "string", "format": "date" }, "maxDate": { "type": "string", "format": "date" }, "accept": { "type": "string" }, "maxSize": { "type": "integer" }, "options": { "type": "array", "items": { "$ref": "#/$defs/option" } } } }, "option": { "type": "object", "required": ["value", "label"], "properties": { "value": { "type": "string" }, "label": { "type": "string" }, "description": { "type": "string" } } }, "transition": { "type": "object", "required": ["target"], "properties": { "target": { "type": "string" }, "condition": { "$ref": "#/$defs/condition" } } }, "condition": { "type": "object", "oneOf": [ { "properties": { "field": { "type": "string" }, "equals": {} }, "required": ["field", "equals"] }, { "properties": { "field": { "type": "string" }, "not_equals": {} }, "required": ["field", "not_equals"] }, { "properties": { "field": { "type": "string" }, "operator": { "enum": ["gt", "gte", "lt", "lte", "contains", "in"] }, "value": {} }, "required": ["field", "operator", "value"] }, { "properties": { "and": { "type": "array", "items": { "$ref": "#/$defs/condition" } } }, "required": ["and"] }, { "properties": { "or": { "type": "array", "items": { "$ref": "#/$defs/condition" } } }, "required": ["or"] } ] } } }
Instance Response Schema
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://core.interactor.com/schemas/workflow-instance.json", "title": "Workflow Instance", "type": "object", "required": ["id", "workflow_name", "status", "current_state", "created_at"], "properties": { "id": { "type": "string", "pattern": "^inst_[a-z0-9]+$" }, "workflow_name": { "type": "string" }, "version_id": { "type": "string" }, "namespace": { "type": "string" }, "status": { "enum": ["running", "halted", "completed", "failed", "cancelled"] }, "current_state": { "type": "string" }, "workflow_data": { "type": "object" }, "halting_presentation": { "$ref": "workflow-definition.json#/$defs/presentation" }, "threads": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "status": { "enum": ["running", "halted", "completed"] }, "current_state": { "type": "string" } } } }, "history": { "type": "array", "items": { "type": "object", "properties": { "state": { "type": "string" }, "entered_at": { "type": "string", "format": "date-time" }, "exited_at": { "type": "string", "format": "date-time" }, "transition": { "type": "string" }, "duration_ms": { "type": "integer" } } } }, "error": { "type": "string" }, "created_at": { "type": "string", "format": "date-time" } } }
Quick Reference
Common cURL Commands
# Create and publish a workflow curl -X POST https://core.interactor.com/api/v1/workflows \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d @workflow.json curl -X POST https://core.interactor.com/api/v1/workflows/my_workflow/versions/v_abc123/publish \ -H "Authorization: Bearer $TOKEN" # Start a workflow instance curl -X POST https://core.interactor.com/api/v1/workflows/my_workflow/instances \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: unique-key-123" \ -H "Content-Type: application/json" \ -d '{"namespace": "user_123", "input": {"key": "value"}}' # Check instance status curl https://core.interactor.com/api/v1/workflows/instances/inst_xyz \ -H "Authorization: Bearer $TOKEN" # Resume a halted instance curl -X POST https://core.interactor.com/api/v1/workflows/instances/inst_xyz/resume \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: resume-unique-key" \ -H "Content-Type: application/json" \ -d '{"input": {"approved": true, "comment": "LGTM"}}' # List halted instances for a user curl "https://core.interactor.com/api/v1/workflows/instances?namespace=user_123&status=halted" \ -H "Authorization: Bearer $TOKEN" # Cancel an instance curl -X POST https://core.interactor.com/api/v1/workflows/instances/inst_xyz/cancel \ -H "Authorization: Bearer $TOKEN"
Elixir Quick Start
# Start instance {:ok, instance} = MyApp.Interactor.Workflows.create_instance("approval", user_id, %{amount: 5000}) # Resume when halted {:ok, resumed} = MyApp.Interactor.Workflows.resume_instance(instance["id"], %{approved: true}) # Poll for completion {:ok, final} = MyApp.Interactor.Workflows.wait_for_completion(instance["id"])
TypeScript Quick Start
const workflow = new WorkflowManager(client); // Start instance const instance = await workflow.createInstance('approval', 'user_123', { amount: 5000 }); // Resume when halted if (instance.status === 'halted') { await workflow.resumeInstance(instance.id, { approved: true }); } // Wait for completion const final = await workflow.waitForCompletion(instance.id);
FAQ
Common Issues & Solutions
Q: My workflow is stuck in "running" status
- A: Check that all action states have transitions. A state without transitions will halt the workflow engine. Add a transition to a terminal state or fix the logic.
Q: Transitions are not firing as expected
- A: Transitions are evaluated in order. The first matching condition wins. Ensure your conditions are ordered from most specific to least specific. The last transition should typically have no condition (default path).
Q: Script logic is timing out
- A: Scripts have a 5-second limit. Move complex computations to HTTP endpoints. Use scripts only for simple data transformations and routing decisions.
Q: Form field validation fails on the client but succeeds on the server
- A: Client-side validation should match server expectations. Use the JSON Schema to generate client validators. Test with the
endpoint during development./validate
Q: Webhook events are not being delivered
- A: Check: (1) Subscription is active, (2) URL is publicly accessible, (3) Endpoint returns 2xx within 30s, (4) Signature verification is correct. Use webhook dashboard to see delivery logs and retry failed events.
Q: I get "concurrent_modification" errors
- A: Multiple requests tried to resume the same instance. Use idempotency keys to prevent duplicates. Design your UI to disable buttons after submission. Check instance status before resuming.
Q: How do I handle long-running external operations?
- A: Use a halting state that waits for a webhook callback. Start the operation via HTTP logic, then transition to a halting state. When your external system completes, call the resume endpoint via webhook.
Best Practices Checklist
- Use
endpoint during development/validate - Test all transition paths before publishing
- Implement idempotency keys for create/resume calls
- Verify webhook signatures in your handler
- Use namespaces to isolate user data
- Keep scripts under 100ms execution time
- Monitor rate limit headers and implement backoff
- Store
for debugging and support ticketsrequest_id
Version Compatibility
| Feature | Introduced | Notes |
|---|---|---|
| Core workflow API | v1.0.0 | Stable |
| Webhook subscriptions | v1.1.0 | Stable |
for terminal states | v2.0.0 | Optional |
| Idempotency keys | v1.2.0 | Recommended |
| Thread parallelism | v1.3.0 | Stable |
| File field type | v2.1.0 | Beta |
Note: The
property for terminal states is available in Interactor v2.0.0+. Check your version before using this feature.on_enter
Related Skills
- interactor-auth: Setup authentication (prerequisite)
- interactor-credentials: Use credentials in workflow HTTP actions
- interactor-agents: Combine AI agents with workflows
- interactor-webhooks: Real-time workflow status updates