--- name: interactor-workflows description: 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. author: Interactor Integration Guide --- # 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-auth` skill) - 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 ```bash curl -X POST https://core.interactor.com/api/v1/workflows \ -H "Authorization: Bearer " \ -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:** ```json { "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_enter` property 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: ```bash curl -X POST https://core.interactor.com/api/v1/workflows/validate \ -H "Authorization: Bearer " \ -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):** ```json { "data": { "valid": true } } ``` **Response (error):** ```json { "data": { "valid": false, "errors": [ { "path": "states.start.transitions[0].target", "message": "Target state 'nonexistent' does not exist" } ] } } ``` ### Step 3: List Workflows ```bash curl https://core.interactor.com/api/v1/workflows \ -H "Authorization: Bearer " ``` **Response:** ```json { "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 ```bash curl https://core.interactor.com/api/v1/workflows/approval_workflow/versions \ -H "Authorization: Bearer " ``` **Response:** ```json { "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: ```bash curl -X POST https://core.interactor.com/api/v1/workflows/approval_workflow/versions/v_abc123/publish \ -H "Authorization: Bearer " ``` **Response:** ```json { "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: ```bash curl -X POST https://core.interactor.com/api/v1/workflows/approval_workflow/instances \ -H "Authorization: Bearer " \ -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:** ```json { "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 ```bash curl https://core.interactor.com/api/v1/workflows/instances \ -H "Authorization: Bearer " ``` **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:** ```bash curl "https://core.interactor.com/api/v1/workflows/instances?namespace=user_123&status=halted" \ -H "Authorization: Bearer " ``` **Response:** ```json { "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 ```bash curl https://core.interactor.com/api/v1/workflows/instances/inst_xyz \ -H "Authorization: Bearer " ``` **Response:** ```json { "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 ```bash curl -X POST https://core.interactor.com/api/v1/workflows/instances/inst_xyz/resume \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "input": { "approved": true, "comment": "Looks good, approved for Q1 budget" } }' ``` **Response:** ```json { "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 ```bash curl -X POST https://core.interactor.com/api/v1/workflows/instances/inst_xyz/cancel \ -H "Authorization: Bearer " ``` **Response:** ```json { "data": { "id": "inst_xyz", "status": "cancelled", "cancelled_at": "2026-01-20T12:30:00Z" } } ``` --- ## Threads Workflows can have parallel execution paths (threads). ### List Threads ```bash curl https://core.interactor.com/api/v1/workflows/instances/inst_xyz/threads \ -H "Authorization: Bearer " ``` **Response:** ```json { "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: ```bash curl -X POST https://core.interactor.com/api/v1/workflows/instances/inst_xyz/threads/thread_1/resume \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "input": { "department_approved": true } }' ``` **Response:** ```json { "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 ```bash curl https://core.interactor.com/api/v1/workflows/instances/inst_xyz/history \ -H "Authorization: Bearer " ``` **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:** ```json { "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 ```bash curl https://core.interactor.com/api/v1/workflows/instances/inst_xyz/events/evt_01HX... \ -H "Authorization: Bearer " ``` Add `?include_data=true` to include the `workflow_data` snapshot at that point. ### Error Dashboard Query errors across all workflows: ```bash curl "https://core.interactor.com/api/v1/workflows/errors?since=2026-01-20T00:00:00Z" \ -H "Authorization: Bearer " ``` --- ## 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: ```json { "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: ```json { "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: ```json { "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`: ```json { "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 `presentation` format is still supported for backward compatibility. New workflows should use `halting_instructions` above. When a workflow halts, specify how to present the required input to users. > **Note**: The `title` and `description` fields shown in presentations are optional enhancements for better UX. The core API requires only `type` and the type-specific fields (`fields`, `options`, or `message`). ### Form Presentation ```json { "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 ```json { "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 ```json { "type": "message", "title": "Processing", "message": "Waiting for external system response. This may take a few minutes.", "show_progress": true } ``` > **Note**: The `show_progress` field 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`, and `label`. Additional properties like `placeholder`, `step`, `maxLength` may vary by Interactor version. Test with `/validate` endpoint to confirm supported properties. --- ## Workflow Logic ### Script Logic Execute JavaScript code in action states: ```json { "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 workflow - `workflow_data` - Current accumulated workflow data - `context` - Additional context (namespace, instance_id, etc.) ### HTTP Logic Make external API calls: ```json { "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 `timeout` and `retry` properties are optional enhancements. The core API requires only `type`, `method`, `url`, and optionally `headers` and `body`. ### Transition Conditions Define conditions for state transitions: ```json { "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: ```json { "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 ```json { "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.Client` module from the `interactor-auth` skill. See that skill for the HTTP client implementation. ```elixir 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 ```elixir 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: ```elixir 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 %>

<%= @presentation["title"] %>

<% end %> <%= if @presentation["description"] do %>

<%= @presentation["description"] %>

<% end %> <%= case @presentation["type"] do %> <% "form" -> %> <%= for field <- @presentation["fields"] || [] do %> <.workflow_field field={field} form={@form} /> <% end %> <% "choice" -> %>

<%= @presentation["message"] %>

<%= for option <- @presentation["options"] || [] do %> <% end %>
<% "message" -> %>

<%= @presentation["message"] %>

<% end %> <%= if @presentation["type"] == "form" do %> <% end %> """ end attr :field, :map, required: true attr :form, :any, required: true defp workflow_field(assigns) do ~H"""
<%= case @field["type"] do %> <% "string" -> %> <%= if @field["multiline"] do %> <% else %> <% end %> <% "number" -> %> <% "boolean" -> %> <% "select" -> %> <% "date" -> %> <% _ -> %> <% end %>
""" end end ``` Then import it in your LiveView: ```elixir 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"""

Workflow: <%= @instance["workflow_name"] %>

Status: <%= @instance["status"] %>

Current State: <%= @instance["current_state"] %>

<%= if @instance["status"] == "halted" do %> <.workflow_form presentation={@instance["halting_presentation"]} form={@form} /> <% end %> <%= if @instance["status"] in ["running", "halted"] do %> <% end %>
""" 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 ```typescript import { InteractorClient } from './interactor-client'; export class WorkflowManager { private client: InteractorClient; constructor(client: InteractorClient) { this.client = client; } // ============ Workflow Definitions ============ async createWorkflow(definition: WorkflowDefinition): Promise { return this.client.request('POST', '/workflows', definition); } async validateWorkflow(definition: WorkflowDefinition): Promise { return this.client.request('POST', '/workflows/validate', definition); } async listWorkflows(): Promise { const result = await this.client.request<{ workflows: Workflow[] }>('GET', '/workflows'); return result.workflows; } async listVersions(workflowName: string): Promise { const result = await this.client.request<{ versions: WorkflowVersion[] }>( 'GET', `/workflows/${workflowName}/versions` ); return result.versions; } async publishVersion(workflowName: string, versionId: string): Promise { return this.client.request( 'POST', `/workflows/${workflowName}/versions/${versionId}/publish` ); } // ============ Instances ============ async createInstance( workflowName: string, userId: string, input: Record ): Promise { return this.client.request('POST', `/workflows/${workflowName}/instances`, { namespace: `user_${userId}`, input }); } async getInstance(instanceId: string): Promise { return this.client.request('GET', `/workflows/instances/${instanceId}`); } async listInstances(filters?: { userId?: string; workflowName?: string; status?: InstanceStatus; }): Promise { 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 ): Promise { return this.client.request('POST', `/workflows/instances/${instanceId}/resume`, { input }); } async cancelInstance(instanceId: string): Promise { await this.client.request('POST', `/workflows/instances/${instanceId}/cancel`); } // ============ Threads ============ async listThreads(instanceId: string): Promise { const result = await this.client.request<{ threads: WorkflowThread[] }>( 'GET', `/workflows/instances/${instanceId}/threads` ); return result.threads; } async resumeThread( instanceId: string, threadId: string, input: Record ): Promise { return this.client.request( 'POST', `/workflows/instances/${instanceId}/threads/${threadId}/resume`, { input } ); } // ============ Helpers ============ async waitForCompletion( instanceId: string, timeoutMs: number = 300000, pollIntervalMs: number = 2000 ): Promise { 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; } 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; // 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; 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 ```typescript 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 | |------------|-------------|-------------|------------| | `workflow_not_found` | 404 | Workflow definition doesn't exist | Check workflow name | | `workflow_not_published` | 400 | No published version available | Publish a version first | | `version_not_found` | 404 | Version doesn't exist | Check version ID | | `instance_not_found` | 404 | Instance doesn't exist | Check instance ID | | `instance_not_halted` | 400 | Cannot resume - not halted | Check instance status | | `invalid_transition` | 400 | Input doesn't match any condition | Check transition conditions | | `script_error` | 500 | Error executing workflow script | Check script syntax | | `http_error` | 500 | HTTP action failed | Check endpoint and auth | > **See Also**: The [API Reference](#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 | |-------|-------------|----------------| | `workflow.instance.created` | New instance started | Instance created via API | | `workflow.instance.halted` | Waiting for input | Instance reaches halting state | | `workflow.instance.resumed` | Instance resumed | Resume called with input | | `workflow.instance.completed` | Finished successfully | Instance reaches terminal state | | `workflow.instance.failed` | Terminated with error | Script/HTTP error or invalid transition | | `workflow.instance.cancelled` | Manually cancelled | Cancel endpoint called | ### Create Webhook Subscription ```bash curl -X POST https://core.interactor.com/api/v1/webhooks/subscriptions \ -H "Authorization: Bearer " \ -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:** ```json { "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 ```bash curl https://core.interactor.com/api/v1/webhooks/subscriptions \ -H "Authorization: Bearer " ``` ### Delete Webhook Subscription ```bash curl -X DELETE https://core.interactor.com/api/v1/webhooks/subscriptions/whsub_abc123 \ -H "Authorization: Bearer " ``` ### Webhook Delivery Headers Every webhook delivery includes these headers: | Header | Description | Example | |--------|-------------|---------| | `X-Interactor-Event` | Event type | `workflow.instance.halted` | | `X-Interactor-Signature` | HMAC-SHA256 signature | `sha256=abc123...` | | `X-Interactor-Delivery` | Unique delivery ID | `del_01F8B6XY...` | | `X-Interactor-Timestamp` | Unix timestamp | `1705752000` | | `X-Interactor-Retry-Count` | Retry attempt (0-based) | `0` | | `Content-Type` | Always JSON | `application/json` | ### Webhook Payload Example (`workflow.instance.halted`) ```json { "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`) ```json { "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:** ```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:** ```elixir 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 `2xx` status to acknowledge receipt - 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 | |-------|-------------|--------------| | `workflows:read` | Read workflow definitions and instances | GET endpoints | | `workflows:write` | Create/modify workflows and instances | POST/PUT/DELETE endpoints | | `workflows:execute` | Create and resume instances | Instance operations | | `webhooks:manage` | Manage webhook subscriptions | Webhook endpoints | ### Token Format ```http Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9... ``` ### Token Refresh Tokens expire after 1 hour. Refresh before expiry: ```bash curl -X POST https://auth.interactor.com/oauth/token \ -H "Content-Type: application/json" \ -d '{ "grant_type": "refresh_token", "refresh_token": "", "client_id": "" }' ``` ### Namespace Authorization - Instances are isolated by `namespace` - Tokens can only access instances in namespaces they own - Use `user_{user_id}` convention for user-specific workflows - Use `org_{org_id}` convention for organization-wide workflows - 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: ```bash curl -X POST https://core.interactor.com/api/v1/workflows/approval_workflow/instances \ -H "Authorization: Bearer " \ -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 `Idempotency-Key` is used within 24 hours, the original response is returned - Keys are scoped to the authenticated account - Use deterministic keys based on business identifiers (e.g., `{order_id}_approval`) **Supported Endpoints:** - `POST /workflows/{name}/instances` (create instance) - `POST /workflows/instances/{id}/resume` (resume instance) - `POST /workflows/instances/{id}/threads/{thread_id}/resume` (resume thread) ### 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 `409 Conflict` | | Different threads, same instance | Both processed (parallel execution) | **Conflict Response:** ```json { "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 `workflow_data` 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) | 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:** ```http 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 ```javascript // 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): ```javascript // 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 `console.log` sparingly (logs are stored in instance history) - Return plain objects (no functions or circular references) --- ## Observability & Tracing ### Correlation IDs Every API request returns a unique request ID: ```http X-Request-Id: req_01F8B6XY9Z... ``` Include this ID when contacting support or debugging issues. ### Propagating Trace Context Pass trace context to correlate across services: ```bash curl -X POST https://core.interactor.com/api/v1/workflows/approval_workflow/instances \ -H "Authorization: Bearer " \ -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: ```json { "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 | |--------|-------------| | `workflow.instance.created` | Counter: instances created | | `workflow.instance.completed` | Counter: instances completed | | `workflow.instance.failed` | Counter: instances failed | | `workflow.state.duration` | Histogram: time in each state | | `workflow.script.duration` | Histogram: script execution time | | `workflow.http.duration` | 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** - `await_manager_approval` over `state_3` - **Validate early** - Use `/validate` endpoint during development - **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: ```markdown ## 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 | |--------|----------|------|---------|-------------| | `POST` | `/workflows` | `workflows:write` | `201` | Create workflow definition | | `POST` | `/workflows/validate` | `workflows:read` | `200` | Validate without saving | | `GET` | `/workflows` | `workflows:read` | `200` | List all workflows | | `GET` | `/workflows/{name}/versions` | `workflows:read` | `200` | List workflow versions | | `POST` | `/workflows/{name}/versions/{id}/publish` | `workflows:write` | `200` | Publish a version | | `POST` | `/workflows/{name}/instances` | `workflows:execute` | `201` | Create instance | | `GET` | `/workflows/instances` | `workflows:read` | `200` | List instances | | `GET` | `/workflows/instances/{id}` | `workflows:read` | `200` | Get instance details | | `POST` | `/workflows/instances/{id}/resume` | `workflows:execute` | `200` | Resume halted instance | | `POST` | `/workflows/instances/{id}/cancel` | `workflows:execute` | `200` | Cancel instance | | `GET` | `/workflows/instances/{id}/threads` | `workflows:read` | `200` | List threads | | `POST` | `/workflows/instances/{id}/threads/{tid}/resume` | `workflows:execute` | `200` | Resume thread | | `POST` | `/webhooks/subscriptions` | `webhooks:manage` | `201` | Create webhook | | `GET` | `/webhooks/subscriptions` | `webhooks:manage` | `200` | List webhooks | | `DELETE` | `/webhooks/subscriptions/{id}` | `webhooks:manage` | `204` | Delete webhook | ### Error Response Format All errors follow this standardized format: ```json { "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 | `unauthorized` | 401 | Missing or invalid token | | All | `forbidden` | 403 | Insufficient scopes | | All | `rate_limited` | 429 | Rate limit exceeded | | All | `internal_error` | 500 | Server error | | `POST /workflows` | `invalid_workflow` | 400 | Schema validation failed | | `POST /workflows` | `workflow_exists` | 409 | Name already taken | | `GET /workflows/{name}/*` | `workflow_not_found` | 404 | Workflow doesn't exist | | `POST /.../publish` | `version_not_found` | 404 | Version doesn't exist | | `POST /.../publish` | `already_published` | 400 | Version already published | | `POST /.../instances` | `workflow_not_published` | 400 | No published version | | `POST /.../instances` | `namespace_quota_exceeded` | 429 | Too many instances | | `GET /instances/{id}` | `instance_not_found` | 404 | Instance doesn't exist | | `POST /.../resume` | `instance_not_halted` | 400 | Instance not in halted state | | `POST /.../resume` | `invalid_transition` | 400 | Input doesn't match conditions | | `POST /.../resume` | `concurrent_modification` | 409 | Race condition | | `POST /.../cancel` | `instance_not_active` | 400 | Already completed/failed | | Script execution | `script_error` | 500 | Runtime error in script | | Script execution | `script_timeout` | 500 | Script exceeded 5s limit | | HTTP logic | `http_error` | 500 | External request failed | | HTTP logic | `http_timeout` | 500 | External request timed out | --- ## React Component Example Render `halting_presentation` in React applications: ```tsx 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) => void; onCancel?: () => void; isSubmitting?: boolean; } export function WorkflowForm({ presentation, onSubmit, onCancel, isSubmitting = false }: WorkflowFormProps) { const [formData, setFormData] = React.useState>(() => { // Initialize with defaults const defaults: Record = {}; 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 (
{presentation.title && (

{presentation.title}

)} {presentation.description && (

{presentation.description}

)} {presentation.type === 'form' && (
{presentation.fields?.map(field => ( updateField(field.name, value)} /> ))}
{onCancel && ( )}
)} {presentation.type === 'choice' && (
{presentation.message && (

{presentation.message}

)}
{presentation.options?.map(option => ( ))}
)} {presentation.type === 'message' && (

{presentation.message}

{presentation.show_progress && (
)}
)}
); } 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 (
{field.type === 'string' && !field.multiline && ( onChange(e.target.value)} placeholder={field.placeholder} required={field.required} className={baseInputClass} /> )} {field.type === 'string' && field.multiline && (