Back to skills
extension
Category: Development & EngineeringNo API key required

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.

personAuthor: jakexiaohubgithub

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

curl -X POST https://core.interactor.com/api/v1/workflows \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "approval_workflow",
    "initial_state": "request",
    "ai_guidance": "This workflow handles approval requests. Route based on amount thresholds.",
    "states": {
      "request": {
        "type": "action",
        "logic": {
          "type": "script",
          "code": "return { request_id: input.id, amount: input.amount, status: \"pending\", submitted_at: new Date().toISOString() }"
        },
        "transitions": [
          { "target": "await_approval" }
        ]
      },
      "await_approval": {
        "type": "halting",
        "presentation": {
          "type": "form",
          "title": "Approval Required",
          "description": "Please review and approve or reject this request.",
          "fields": [
            { "name": "approved", "type": "boolean", "label": "Approve this request?" },
            { "name": "comment", "type": "string", "label": "Comment (optional)", "multiline": true }
          ]
        },
        "transitions": [
          { "target": "approved", "condition": { "field": "approved", "equals": true } },
          { "target": "rejected" }
        ]
      },
      "approved": {
        "type": "terminal",
        "on_enter": {
          "type": "http",
          "method": "POST",
          "url": "https://yourapp.com/api/webhooks/approval-complete",
          "body": { "request_id": "${workflow_data.request_id}", "status": "approved" }
        }
      },
      "rejected": {
        "type": "terminal",
        "on_enter": {
          "type": "http",
          "method": "POST",
          "url": "https://yourapp.com/api/webhooks/approval-complete",
          "body": { "request_id": "${workflow_data.request_id}", "status": "rejected" }
        }
      }
    }
  }'

Response:

{
  "data": {
    "name": "approval_workflow",
    "version_id": "v_abc123",
    "status": "draft",
    "created_at": "2026-01-20T12:00:00Z"
  }
}

State Types

| Type | Description | Behavior | |------|-------------|----------| | action | Executes logic automatically | Runs logic, then transitions immediately | | halting | Pauses for external input | Waits for resume call with user input | | terminal | End state | Workflow completes, no further transitions |

Note: The on_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:

curl -X POST https://core.interactor.com/api/v1/workflows/validate \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "my_workflow",
    "initial_state": "start",
    "states": {
      "start": {
        "type": "action",
        "logic": { "type": "script", "code": "return { message: \"Hello\" }" },
        "transitions": [{ "target": "end" }]
      },
      "end": {
        "type": "terminal"
      }
    }
  }'

Response (success):

{
  "data": {
    "valid": true
  }
}

Response (error):

{
  "data": {
    "valid": false,
    "errors": [
      {
        "path": "states.start.transitions[0].target",
        "message": "Target state 'nonexistent' does not exist"
      }
    ]
  }
}

Step 3: List Workflows

curl https://core.interactor.com/api/v1/workflows \
  -H "Authorization: Bearer <token>"

Response:

{
  "data": {
    "workflows": [
      {
        "name": "approval_workflow",
        "latest_version_id": "v_abc123",
        "published_version_id": "v_abc123",
        "created_at": "2026-01-20T12:00:00Z"
      },
      {
        "name": "onboarding_workflow",
        "latest_version_id": "v_def456",
        "published_version_id": null,
        "created_at": "2026-01-19T10:00:00Z"
      }
    ]
  }
}

Step 4: List Versions

curl https://core.interactor.com/api/v1/workflows/approval_workflow/versions \
  -H "Authorization: Bearer <token>"

Response:

{
  "data": {
    "versions": [
      {
        "version_id": "v_abc123",
        "status": "draft",
        "created_at": "2026-01-20T12:00:00Z"
      },
      {
        "version_id": "v_def456",
        "status": "published",
        "created_at": "2026-01-19T10:00:00Z"
      }
    ]
  }
}

Step 5: Publish a Version

Workflows must be published before they can be executed:

curl -X POST https://core.interactor.com/api/v1/workflows/approval_workflow/versions/v_abc123/publish \
  -H "Authorization: Bearer <token>"

Response:

{
  "data": {
    "version_id": "v_abc123",
    "status": "published",
    "published_at": "2026-01-20T12:05:00Z"
  }
}

Workflow Instances

Instances are running executions of a workflow.

Create Instance

Start a new workflow execution:

curl -X POST https://core.interactor.com/api/v1/workflows/approval_workflow/instances \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "namespace": "user_123",
    "input": {
      "id": "req_456",
      "amount": 5000,
      "requester": "john@example.com",
      "description": "New laptop for development"
    }
  }'

Response:

{
  "data": {
    "id": "inst_xyz",
    "workflow_name": "approval_workflow",
    "version_id": "v_abc123",
    "namespace": "user_123",
    "status": "halted",
    "current_state": "await_approval",
    "workflow_data": {
      "request_id": "req_456",
      "amount": 5000,
      "status": "pending",
      "submitted_at": "2026-01-20T12:00:00Z"
    },
    "created_at": "2026-01-20T12:00:00Z"
  }
}

Instance Status Values

| Status | Description | |--------|-------------| | running | Actively executing (in an action state) | | halted | Paused, waiting for external input | | completed | Finished successfully (reached terminal state) | | failed | Terminated due to error | | cancelled | Manually cancelled |

List Instances

curl https://core.interactor.com/api/v1/workflows/instances \
  -H "Authorization: Bearer <token>"

Query Parameters:

| Parameter | Type | Description | |-----------|------|-------------| | namespace | string | Filter by namespace | | workflow_name | string | Filter by workflow | | status | string | running, halted, completed, failed, cancelled |

Example - List halted instances for a user:

curl "https://core.interactor.com/api/v1/workflows/instances?namespace=user_123&status=halted" \
  -H "Authorization: Bearer <token>"

Response:

{
  "data": {
    "instances": [
      {
        "id": "inst_xyz",
        "workflow_name": "approval_workflow",
        "status": "halted",
        "current_state": "await_approval",
        "created_at": "2026-01-20T12:00:00Z"
      },
      {
        "id": "inst_abc",
        "workflow_name": "onboarding_workflow",
        "status": "halted",
        "current_state": "verify_email",
        "created_at": "2026-01-19T15:30:00Z"
      }
    ]
  }
}

Get Instance

curl https://core.interactor.com/api/v1/workflows/instances/inst_xyz \
  -H "Authorization: Bearer <token>"

Response:

{
  "data": {
    "id": "inst_xyz",
    "workflow_name": "approval_workflow",
    "version_id": "v_abc123",
    "namespace": "user_123",
    "status": "halted",
    "current_state": "await_approval",
    "workflow_data": {
      "request_id": "req_456",
      "amount": 5000,
      "status": "pending"
    },
    "halting_presentation": {
      "type": "form",
      "title": "Approval Required",
      "description": "Please review and approve or reject this request.",
      "fields": [
        { "name": "approved", "type": "boolean", "label": "Approve this request?" },
        { "name": "comment", "type": "string", "label": "Comment (optional)", "multiline": true }
      ]
    },
    "threads": [
      {
        "id": "thread_main",
        "status": "halted",
        "current_state": "await_approval"
      }
    ],
    "history": [
      {
        "state": "request",
        "entered_at": "2026-01-20T12:00:00Z",
        "exited_at": "2026-01-20T12:00:01Z",
        "transition": "await_approval"
      },
      {
        "state": "await_approval",
        "entered_at": "2026-01-20T12:00:01Z"
      }
    ],
    "created_at": "2026-01-20T12:00:00Z"
  }
}

Resuming Workflows

When a workflow reaches a halting state, it waits for external input.

Resume with Input

curl -X POST https://core.interactor.com/api/v1/workflows/instances/inst_xyz/resume \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "input": {
      "approved": true,
      "comment": "Looks good, approved for Q1 budget"
    }
  }'

Response:

{
  "data": {
    "id": "inst_xyz",
    "status": "completed",
    "current_state": "approved",
    "workflow_data": {
      "request_id": "req_456",
      "amount": 5000,
      "status": "pending",
      "approved": true,
      "comment": "Looks good, approved for Q1 budget"
    }
  }
}

The workflow continues execution based on the input and transition conditions.

Cancel Instance

curl -X POST https://core.interactor.com/api/v1/workflows/instances/inst_xyz/cancel \
  -H "Authorization: Bearer <token>"

Response:

{
  "data": {
    "id": "inst_xyz",
    "status": "cancelled",
    "cancelled_at": "2026-01-20T12:30:00Z"
  }
}

Threads

Workflows can have parallel execution paths (threads).

List Threads

curl https://core.interactor.com/api/v1/workflows/instances/inst_xyz/threads \
  -H "Authorization: Bearer <token>"

Response:

{
  "data": {
    "threads": [
      {
        "id": "thread_main",
        "status": "halted",
        "current_state": "await_approval"
      },
      {
        "id": "thread_finance",
        "status": "completed",
        "current_state": "finance_approved"
      }
    ]
  }
}

Resume Specific Thread

For workflows with multiple parallel threads:

curl -X POST https://core.interactor.com/api/v1/workflows/instances/inst_xyz/threads/thread_1/resume \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "input": {
      "department_approved": true
    }
  }'

Response:

{
  "data": {
    "id": "inst_xyz",
    "status": "running",
    "threads": [
      {
        "id": "thread_1",
        "status": "completed",
        "current_state": "department_approved"
      },
      {
        "id": "thread_2",
        "status": "halted",
        "current_state": "await_finance_approval"
      }
    ]
  }
}

History API

Query workflow execution history for debugging, monitoring, and audit.

List History Events

curl https://core.interactor.com/api/v1/workflows/instances/inst_xyz/history \
  -H "Authorization: Bearer <token>"

Query Parameters:

| Parameter | Type | Description | |-----------|------|-------------| | limit | integer | Max events (default: 100, max: 1000) | | cursor | string | Pagination cursor | | types | string | Filter by type: transition, step, halt, error, lifecycle | | since | ISO8601 | Events after this timestamp | | until | ISO8601 | Events before this timestamp | | thread | string | Filter to specific thread | | include_data | boolean | Include workflow_data snapshots |

Response:

{
  "data": {
    "instance_id": "inst_xyz",
    "workflow_id": "wf_abc",
    "status": "completed",
    "events": [
      {
        "id": "evt_01HX...",
        "type": "lifecycle",
        "subtype": "created",
        "timestamp": "2026-01-20T12:00:00Z",
        "initial_state": "request"
      },
      {
        "id": "evt_01HX...",
        "type": "transition",
        "subtype": "state_change",
        "from_state": "request",
        "to_state": "processing",
        "trigger": "automatic",
        "changes": {
          "updated": {"status": {"from": "pending", "to": "processing"}}
        }
      }
    ],
    "pagination": {"has_more": false, "next_cursor": null}
  }
}

Get Single Event

curl https://core.interactor.com/api/v1/workflows/instances/inst_xyz/events/evt_01HX... \
  -H "Authorization: Bearer <token>"

Add ?include_data=true to include the workflow_data snapshot at that point.

Error Dashboard

Query errors across all workflows:

curl "https://core.interactor.com/api/v1/workflows/errors?since=2026-01-20T00:00:00Z" \
  -H "Authorization: Bearer <token>"

Halting Instructions

When a workflow halts, you can configure how the halting message is generated and presented to users.

AI-Generated Instructions

Use AI to dynamically generate contextual messages based on workflow data:

{
  "await_approval": {
    "type": "halting",
    "halting_instructions": {
      "type": "ai",
      "config": {
        "prompt": "Summarize this order and ask the user to approve or reject it.",
        "model": "claude-3-haiku-20240307",
        "include_data_paths": ["order", "customer", "risk_score"]
      }
    },
    "transition_mode": "selection",
    "transitions": [
      {"key": "approve", "to": "approved", "description": "Approve the order"},
      {"key": "reject", "to": "rejected", "description": "Reject the order"}
    ]
  }
}

Simple format - treats instruction as an AI prompt:

{
  "halting_instructions": {
    "instruction": "Tell the user the strategy is ready for review. Highlight key metrics and risks.",
    "include_data": ["strategy", "benchmarks", "risk_assessment"]
  }
}

Static Message Instructions

For static messages without AI generation:

{
  "halting_instructions": {
    "type": "message",
    "config": {
      "title": "Approval Required",
      "message": "This order exceeds the automatic approval threshold and requires manual review."
    }
  }
}

Halted Response

When halted, the API response includes halted_options:

{
  "status": "halted",
  "halted_at_state": "await_approval",
  "halted_options": {
    "instruction": "Order #123 for $150.00 from Acme Corp is ready. Risk score: Low (23).",
    "include_data": ["order", "customer"],
    "transition_mode": "selection",
    "choices": [
      {"key": "approve", "description": "Approve the order", "to": "approved"},
      {"key": "reject", "description": "Reject the order", "to": "rejected"}
    ],
    "generated": true
  }
}

| Field | Description | |-------|-------------| | instruction | Message to display (AI-generated or static) | | generated | true if AI-generated, false if static | | choices | Available transitions for selection mode |


Halting Presentations (Legacy)

Note: The 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

{
  "type": "form",
  "title": "Approval Required",
  "description": "Please review the request details and provide your decision.",
  "fields": [
    {
      "name": "approved",
      "type": "boolean",
      "label": "Approve this request?",
      "required": true
    },
    {
      "name": "amount",
      "type": "number",
      "label": "Approved Amount",
      "default": "${workflow_data.amount}",
      "min": 0,
      "max": 100000
    },
    {
      "name": "notes",
      "type": "string",
      "label": "Notes",
      "multiline": true,
      "placeholder": "Add any notes or conditions..."
    },
    {
      "name": "priority",
      "type": "select",
      "label": "Priority",
      "options": [
        { "value": "low", "label": "Low" },
        { "value": "medium", "label": "Medium" },
        { "value": "high", "label": "High" }
      ],
      "default": "medium"
    }
  ]
}

Choice Presentation

{
  "type": "choice",
  "title": "Select Action",
  "message": "How would you like to proceed with this request?",
  "options": [
    {
      "value": "approve",
      "label": "Approve",
      "description": "Approve the request as submitted"
    },
    {
      "value": "reject",
      "label": "Reject",
      "description": "Reject the request"
    },
    {
      "value": "escalate",
      "label": "Escalate to Manager",
      "description": "Send to manager for review"
    },
    {
      "value": "request_info",
      "label": "Request More Information",
      "description": "Ask the requester for additional details"
    }
  ]
}

Message Presentation

{
  "type": "message",
  "title": "Processing",
  "message": "Waiting for external system response. This may take a few minutes.",
  "show_progress": true
}

Note: The show_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:

{
  "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:

{
  "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:

{
  "transitions": [
    {
      "target": "high_value_approval",
      "condition": {
        "field": "amount",
        "operator": "gt",
        "value": 10000
      }
    },
    {
      "target": "manager_approval",
      "condition": {
        "field": "amount",
        "operator": "gt",
        "value": 1000
      }
    },
    {
      "target": "auto_approve"
    }
  ]
}

Operators:

| Operator | Description | Example | |----------|-------------|---------| | equals | Exact match | { "field": "status", "equals": "approved" } | | not_equals | Not equal | { "field": "status", "not_equals": "rejected" } | | gt | Greater than | { "field": "amount", "operator": "gt", "value": 1000 } | | gte | Greater than or equal | { "field": "amount", "operator": "gte", "value": 1000 } | | lt | Less than | { "field": "amount", "operator": "lt", "value": 100 } | | lte | Less than or equal | { "field": "amount", "operator": "lte", "value": 100 } | | contains | String contains | { "field": "email", "operator": "contains", "value": "@company.com" } | | in | Value in array | { "field": "category", "operator": "in", "value": ["A", "B", "C"] } |

Complex Conditions

Use and / or for complex conditions:

{
  "transitions": [
    {
      "target": "vp_approval",
      "condition": {
        "and": [
          { "field": "approved", "equals": true },
          { "field": "amount", "operator": "gt", "value": 10000 }
        ]
      }
    },
    {
      "target": "approved",
      "condition": {
        "or": [
          { "field": "amount", "operator": "lte", "value": 1000 },
          {
            "and": [
              { "field": "approved", "equals": true },
              { "field": "amount", "operator": "lte", "value": 10000 }
            ]
          }
        ]
      }
    },
    {
      "target": "rejected"
    }
  ]
}

Complete Example: Multi-Level Approval

{
  "name": "purchase_approval",
  "initial_state": "submit",
  "ai_guidance": "Multi-level purchase approval workflow. Amount thresholds: <$1000 auto-approve, $1000-$10000 manager, >$10000 VP required.",
  "states": {
    "submit": {
      "type": "action",
      "logic": {
        "type": "script",
        "code": "return { ...input, submitted_at: new Date().toISOString(), status: 'pending' }"
      },
      "transitions": [
        {
          "target": "auto_approved",
          "condition": { "field": "amount", "operator": "lte", "value": 1000 }
        },
        {
          "target": "manager_approval",
          "condition": { "field": "amount", "operator": "lte", "value": 10000 }
        },
        { "target": "manager_approval" }
      ]
    },

    "manager_approval": {
      "type": "halting",
      "presentation": {
        "type": "form",
        "title": "Manager Approval Required",
        "description": "Purchase request for ${workflow_data.description} - $${workflow_data.amount}",
        "fields": [
          { "name": "approved", "type": "boolean", "label": "Approve?", "required": true },
          { "name": "comment", "type": "string", "label": "Comment", "multiline": true }
        ]
      },
      "transitions": [
        {
          "target": "vp_approval",
          "condition": {
            "and": [
              { "field": "approved", "equals": true },
              { "field": "amount", "operator": "gt", "value": 10000 }
            ]
          }
        },
        {
          "target": "approved",
          "condition": { "field": "approved", "equals": true }
        },
        { "target": "rejected" }
      ]
    },

    "vp_approval": {
      "type": "halting",
      "presentation": {
        "type": "form",
        "title": "VP Approval Required",
        "description": "High-value purchase: ${workflow_data.description} - $${workflow_data.amount}",
        "fields": [
          { "name": "approved", "type": "boolean", "label": "VP Approval", "required": true },
          { "name": "budget_code", "type": "string", "label": "Budget Code" },
          { "name": "comment", "type": "string", "label": "Comment", "multiline": true }
        ]
      },
      "transitions": [
        {
          "target": "approved",
          "condition": { "field": "approved", "equals": true }
        },
        { "target": "rejected" }
      ]
    },

    "auto_approved": {
      "type": "action",
      "logic": {
        "type": "script",
        "code": "return { ...workflow_data, status: 'approved', approved_by: 'auto', approved_at: new Date().toISOString() }"
      },
      "transitions": [
        { "target": "notify_requester" }
      ]
    },

    "approved": {
      "type": "action",
      "logic": {
        "type": "script",
        "code": "return { ...workflow_data, status: 'approved', approved_at: new Date().toISOString() }"
      },
      "transitions": [
        { "target": "notify_requester" }
      ]
    },

    "rejected": {
      "type": "action",
      "logic": {
        "type": "script",
        "code": "return { ...workflow_data, status: 'rejected', rejected_at: new Date().toISOString() }"
      },
      "transitions": [
        { "target": "notify_requester" }
      ]
    },

    "notify_requester": {
      "type": "action",
      "logic": {
        "type": "http",
        "method": "POST",
        "url": "https://yourapp.com/api/notifications",
        "body": {
          "type": "purchase_decision",
          "email": "${workflow_data.requester}",
          "status": "${workflow_data.status}",
          "amount": "${workflow_data.amount}"
        }
      },
      "transitions": [
        { "target": "complete" }
      ]
    },

    "complete": {
      "type": "terminal"
    }
  }
}

Implementation Examples

Elixir Implementation (Phoenix)

Prerequisite: This module requires the MyApp.Interactor.Client module from the interactor-auth skill. See that skill for the HTTP client implementation.

defmodule MyApp.Interactor.Workflows do
  @moduledoc """
  Interactor Workflow management for state-machine based automations.

  Requires MyApp.Interactor.Client from interactor-auth skill.
  """

  alias MyApp.Interactor.Client

  # ============ Workflow Definitions ============

  @doc """
  Create a new workflow definition.
  """
  def create_workflow(definition) do
    Client.post("/workflows", definition)
  end

  @doc """
  Validate a workflow definition without saving.
  """
  def validate_workflow(definition) do
    Client.post("/workflows/validate", definition)
  end

  @doc """
  List all workflows.
  """
  def list_workflows do
    case Client.get("/workflows") do
      {:ok, %{"workflows" => workflows}} -> {:ok, workflows}
      error -> error
    end
  end

  @doc """
  List versions for a workflow.
  """
  def list_versions(workflow_name) do
    case Client.get("/workflows/#{workflow_name}/versions") do
      {:ok, %{"versions" => versions}} -> {:ok, versions}
      error -> error
    end
  end

  @doc """
  Publish a workflow version.
  """
  def publish_version(workflow_name, version_id) do
    Client.post("/workflows/#{workflow_name}/versions/#{version_id}/publish", %{})
  end

  # ============ Instances ============

  @doc """
  Create a new workflow instance.
  """
  def create_instance(workflow_name, user_id, input) do
    Client.post("/workflows/#{workflow_name}/instances", %{
      namespace: "user_#{user_id}",
      input: input
    })
  end

  @doc """
  Get a workflow instance by ID.
  """
  def get_instance(instance_id) do
    Client.get("/workflows/instances/#{instance_id}")
  end

  @doc """
  List workflow instances with optional filters.
  """
  def list_instances(filters \\ %{}) do
    query_params =
      filters
      |> Enum.map(fn
        {:user_id, id} -> {"namespace", "user_#{id}"}
        {:workflow_name, name} -> {"workflow_name", name}
        {:status, status} -> {"status", status}
      end)
      |> URI.encode_query()

    path = if query_params == "", do: "/workflows/instances", else: "/workflows/instances?#{query_params}"

    case Client.get(path) do
      {:ok, %{"instances" => instances}} -> {:ok, instances}
      error -> error
    end
  end

  @doc """
  Resume a halted workflow instance with input.
  """
  def resume_instance(instance_id, input) do
    Client.post("/workflows/instances/#{instance_id}/resume", %{input: input})
  end

  @doc """
  Cancel a workflow instance.
  """
  def cancel_instance(instance_id) do
    Client.post("/workflows/instances/#{instance_id}/cancel", %{})
  end

  # ============ Threads ============

  @doc """
  List threads for an instance.
  """
  def list_threads(instance_id) do
    case Client.get("/workflows/instances/#{instance_id}/threads") do
      {:ok, %{"threads" => threads}} -> {:ok, threads}
      error -> error
    end
  end

  @doc """
  Resume a specific thread.
  """
  def resume_thread(instance_id, thread_id, input) do
    Client.post(
      "/workflows/instances/#{instance_id}/threads/#{thread_id}/resume",
      %{input: input}
    )
  end

  # ============ Helpers ============

  @doc """
  Wait for a workflow to complete or halt.
  Returns {:ok, instance} when completed/halted, {:error, reason} on failure/timeout.
  """
  def wait_for_completion(instance_id, opts \\ []) do
    timeout_ms = Keyword.get(opts, :timeout, 300_000)
    poll_interval_ms = Keyword.get(opts, :poll_interval, 2_000)
    deadline = System.monotonic_time(:millisecond) + timeout_ms

    do_wait_for_completion(instance_id, deadline, poll_interval_ms)
  end

  defp do_wait_for_completion(instance_id, deadline, poll_interval_ms) do
    if System.monotonic_time(:millisecond) >= deadline do
      {:error, :timeout}
    else
      case get_instance(instance_id) do
        {:ok, %{"status" => "completed"} = instance} ->
          {:ok, instance}

        {:ok, %{"status" => "halted"} = instance} ->
          {:ok, instance}

        {:ok, %{"status" => "failed", "error" => error}} ->
          {:error, {:workflow_failed, error}}

        {:ok, %{"status" => "cancelled"}} ->
          {:error, :cancelled}

        {:ok, %{"status" => "running"}} ->
          Process.sleep(poll_interval_ms)
          do_wait_for_completion(instance_id, deadline, poll_interval_ms)

        {:error, _} = error ->
          error
      end
    end
  end
end

Elixir Usage Example

alias MyApp.Interactor.Workflows

# Create and publish a workflow
{:ok, version} = Workflows.create_workflow(purchase_approval_definition)
{:ok, _published} = Workflows.publish_version("purchase_approval", version["version_id"])

# Start a new instance
{:ok, instance} = Workflows.create_instance(
  "purchase_approval",
  "user_123",
  %{
    id: "PO-2026-001",
    amount: 5500,
    requester: "john@example.com",
    description: "Development laptop"
  }
)

IO.puts("Workflow started: #{instance["id"]}")
IO.puts("Current state: #{instance["current_state"]}")
IO.puts("Status: #{instance["status"]}")

# Handle halted state
case instance["status"] do
  "halted" ->
    IO.puts("Waiting for approval...")
    IO.inspect(instance["halting_presentation"], label: "Presentation")

    # Simulate manager approval
    {:ok, resumed} = Workflows.resume_instance(instance["id"], %{
      approved: true,
      comment: "Approved for Q1 budget"
    })

    IO.puts("New status: #{resumed["status"]}")
    IO.puts("New state: #{resumed["current_state"]}")

  _ ->
    :ok
end

Elixir LiveView Integration

First, create a component to render workflow presentations dynamically:

defmodule MyAppWeb.WorkflowComponents do
  use Phoenix.Component

  @doc """
  Renders a workflow form based on the halting presentation.
  """
  attr :presentation, :map, required: true
  attr :form, :any, required: true

  def workflow_form(assigns) do
    ~H"""
    <.form for={@form} phx-submit="submit_input" class="space-y-4">
      <%= if @presentation["title"] do %>
        <h2 class="text-xl font-semibold"><%= @presentation["title"] %></h2>
      <% end %>
      <%= if @presentation["description"] do %>
        <p class="text-gray-600"><%= @presentation["description"] %></p>
      <% end %>

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

        <% "choice" -> %>
          <p class="font-medium"><%= @presentation["message"] %></p>
          <div class="flex flex-wrap gap-2">
            <%= for option <- @presentation["options"] || [] do %>
              <button
                type="submit"
                name="input[choice]"
                value={option["value"]}
                class="px-4 py-2 bg-[#4CD964] hover:bg-[#3DBF55] text-white rounded-full"
              >
                <%= option["label"] %>
              </button>
            <% end %>
          </div>

        <% "message" -> %>
          <p><%= @presentation["message"] %></p>
      <% end %>

      <%= if @presentation["type"] == "form" do %>
        <button type="submit" class="px-6 py-2 bg-[#4CD964] hover:bg-[#3DBF55] text-white rounded-full">
          Submit
        </button>
      <% end %>
    </.form>
    """
  end

  attr :field, :map, required: true
  attr :form, :any, required: true

  defp workflow_field(assigns) do
    ~H"""
    <div class="space-y-1">
      <label class="block font-medium">
        <%= @field["label"] %>
        <%= if @field["required"], do: "*" %>
      </label>

      <%= case @field["type"] do %>
        <% "string" -> %>
          <%= if @field["multiline"] do %>
            <textarea
              name={"input[#{@field["name"]}]"}
              class="w-full border rounded-lg p-2"
              placeholder={@field["placeholder"]}
            ><%= @field["default"] %></textarea>
          <% else %>
            <input
              type="text"
              name={"input[#{@field["name"]}]"}
              value={@field["default"]}
              placeholder={@field["placeholder"]}
              class="w-full border rounded-lg p-2"
            />
          <% end %>

        <% "number" -> %>
          <input
            type="number"
            name={"input[#{@field["name"]}]"}
            value={@field["default"]}
            min={@field["min"]}
            max={@field["max"]}
            step={@field["step"]}
            class="w-full border rounded-lg p-2"
          />

        <% "boolean" -> %>
          <input
            type="checkbox"
            name={"input[#{@field["name"]}]"}
            value="true"
            checked={@field["default"] == true}
            class="h-5 w-5"
          />

        <% "select" -> %>
          <select name={"input[#{@field["name"]}]"} class="w-full border rounded-lg p-2">
            <%= for option <- @field["options"] || [] do %>
              <option value={option["value"]} selected={option["value"] == @field["default"]}>
                <%= option["label"] %>
              </option>
            <% end %>
          </select>

        <% "date" -> %>
          <input
            type="date"
            name={"input[#{@field["name"]}]"}
            value={@field["default"]}
            min={@field["minDate"]}
            max={@field["maxDate"]}
            class="w-full border rounded-lg p-2"
          />

        <% _ -> %>
          <input
            type="text"
            name={"input[#{@field["name"]}]"}
            value={@field["default"]}
            class="w-full border rounded-lg p-2"
          />
      <% end %>
    </div>
    """
  end
end

Then import it in your LiveView:

defmodule MyAppWeb.WorkflowLive.Show do
  use MyAppWeb, :live_view

  import MyAppWeb.WorkflowComponents
  alias MyApp.Interactor.Workflows

  @impl true
  def mount(%{"id" => instance_id}, _session, socket) do
    if connected?(socket) do
      # Subscribe to workflow updates via PubSub
      Phoenix.PubSub.subscribe(MyApp.PubSub, "workflow:#{instance_id}")
    end

    case Workflows.get_instance(instance_id) do
      {:ok, instance} ->
        {:ok, assign(socket, instance: instance, form: to_form(%{}))}

      {:error, _} ->
        {:ok, push_navigate(socket, to: ~p"/workflows")}
    end
  end

  @impl true
  def handle_event("submit_input", %{"input" => input}, socket) do
    instance_id = socket.assigns.instance["id"]

    case Workflows.resume_instance(instance_id, input) do
      {:ok, updated_instance} ->
        {:noreply, assign(socket, instance: updated_instance)}

      {:error, reason} ->
        {:noreply, put_flash(socket, :error, "Failed to resume: #{inspect(reason)}")}
    end
  end

  @impl true
  def handle_event("cancel", _params, socket) do
    instance_id = socket.assigns.instance["id"]

    case Workflows.cancel_instance(instance_id) do
      {:ok, _} ->
        {:noreply, push_navigate(socket, to: ~p"/workflows")}

      {:error, reason} ->
        {:noreply, put_flash(socket, :error, "Failed to cancel: #{inspect(reason)}")}
    end
  end

  @impl true
  def handle_info({:workflow_updated, instance}, socket) do
    {:noreply, assign(socket, instance: instance)}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <div class="workflow-instance">
      <h1>Workflow: <%= @instance["workflow_name"] %></h1>
      <p>Status: <span class={status_class(@instance["status"])}><%= @instance["status"] %></span></p>
      <p>Current State: <%= @instance["current_state"] %></p>

      <%= if @instance["status"] == "halted" do %>
        <.workflow_form
          presentation={@instance["halting_presentation"]}
          form={@form}
        />
      <% end %>

      <%= if @instance["status"] in ["running", "halted"] do %>
        <button phx-click="cancel" class="btn-secondary">Cancel Workflow</button>
      <% end %>
    </div>
    """
  end

  defp status_class("completed"), do: "text-green-600"
  defp status_class("failed"), do: "text-red-600"
  defp status_class("cancelled"), do: "text-gray-600"
  defp status_class("halted"), do: "text-yellow-600"
  defp status_class(_), do: "text-blue-600"
end

TypeScript Implementation

import { InteractorClient } from './interactor-client';

export class WorkflowManager {
  private client: InteractorClient;

  constructor(client: InteractorClient) {
    this.client = client;
  }

  // ============ Workflow Definitions ============

  async createWorkflow(definition: WorkflowDefinition): Promise<WorkflowVersion> {
    return this.client.request('POST', '/workflows', definition);
  }

  async validateWorkflow(definition: WorkflowDefinition): Promise<ValidationResult> {
    return this.client.request('POST', '/workflows/validate', definition);
  }

  async listWorkflows(): Promise<Workflow[]> {
    const result = await this.client.request<{ workflows: Workflow[] }>('GET', '/workflows');
    return result.workflows;
  }

  async listVersions(workflowName: string): Promise<WorkflowVersion[]> {
    const result = await this.client.request<{ versions: WorkflowVersion[] }>(
      'GET',
      `/workflows/${workflowName}/versions`
    );
    return result.versions;
  }

  async publishVersion(workflowName: string, versionId: string): Promise<WorkflowVersion> {
    return this.client.request(
      'POST',
      `/workflows/${workflowName}/versions/${versionId}/publish`
    );
  }

  // ============ Instances ============

  async createInstance(
    workflowName: 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 | |------------|-------------|-------------|------------| | 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 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

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 | |--------|-------------|---------| | 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)

{
  "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)

{
  "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 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

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 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:

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 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:

{
  "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:

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 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:

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 | |--------|-------------| | 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:

## 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:

{
  "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:

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 /validate endpoint during development.

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 /validate endpoint during development
  • [ ] 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 request_id for debugging and support tickets

Version Compatibility

| Feature | Introduced | Notes | |---------|------------|-------| | Core workflow API | v1.0.0 | Stable | | Webhook subscriptions | v1.1.0 | Stable | | on_enter 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 on_enter property for terminal states is available in Interactor v2.0.0+. Check your version before using this feature.


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