Claude-skill-registry interactor-workflows

Build state-machine based automation with human-in-the-loop support through Interactor. Use when implementing approval flows, multi-step processes, automated pipelines, or any workflow requiring user input at specific stages.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/interactor-workflows" ~/.claude/skills/majiayu000-claude-skill-registry-interactor-workflows && rm -rf "$T"
manifest: skills/data/interactor-workflows/SKILL.md
source content

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:

ComponentDescription
StatesSteps in your process (action, halting, terminal)
TransitionsRules for moving between states
InstancesRunning executions of a workflow
ThreadsParallel 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

TypeDescriptionBehavior
action
Executes logic automaticallyRuns logic, then transitions immediately
halting
Pauses for external inputWaits for
resume
call with user input
terminal
End stateWorkflow 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

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

ParameterTypeDescription
namespace
stringFilter by namespace
workflow_name
stringFilter 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:

ParameterTypeDescription
limit
integerMax events (default: 100, max: 1000)
cursor
stringPagination cursor
types
stringFilter by type:
transition
,
step
,
halt
,
error
,
lifecycle
since
ISO8601Events after this timestamp
until
ISO8601Events before this timestamp
thread
stringFilter to specific thread
include_data
booleanInclude 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
  }
}
FieldDescription
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

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

OperatorDescriptionExample
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 CodeHTTP StatusDescriptionResolution
workflow_not_found
404Workflow definition doesn't existCheck workflow name
workflow_not_published
400No published version availablePublish a version first
version_not_found
404Version doesn't existCheck version ID
instance_not_found
404Instance doesn't existCheck instance ID
instance_not_halted
400Cannot resume - not haltedCheck instance status
invalid_transition
400Input doesn't match any conditionCheck transition conditions
script_error
500Error executing workflow scriptCheck script syntax
http_error
500HTTP action failedCheck 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

EventDescriptionTriggered When
workflow.instance.created
New instance startedInstance created via API
workflow.instance.halted
Waiting for inputInstance reaches halting state
workflow.instance.resumed
Instance resumedResume called with input
workflow.instance.completed
Finished successfullyInstance reaches terminal state
workflow.instance.failed
Terminated with errorScript/HTTP error or invalid transition
workflow.instance.cancelled
Manually cancelledCancel 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:

HeaderDescriptionExample
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

AttemptDelayCumulative Time
1Immediate0s
230 seconds30s
32 minutes2m 30s
48 minutes10m 30s
530 minutes40m 30s
62 hours2h 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

ScopeDescriptionRequired For
workflows:read
Read workflow definitions and instancesGET endpoints
workflows:write
Create/modify workflows and instancesPOST/PUT/DELETE endpoints
workflows:execute
Create and resume instancesInstance operations
webhooks:manage
Manage webhook subscriptionsWebhook 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:

ScenarioBehavior
Same instance, same inputSecond request returns same result (idempotent)
Same instance, different inputFirst request wins, second gets
409 Conflict
Different threads, same instanceBoth 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

LimitValueNotes
Max states per workflow100Including terminal states
Max transitions per state20Evaluated in order
Max workflow name length64 charsAlphanumeric, underscores, hyphens
Max script code size64 KBPer script logic block
Max presentation fields50Per halting state
Max workflow definition size1 MBTotal JSON size

Instance Limits

LimitValueNotes
Max
workflow_data
size
256 KBAccumulated across states
Max input payload size64 KBPer resume/create call
Max concurrent threads10Per instance
Instance TTL (running)30 daysAuto-cancelled after
Instance TTL (halted)90 daysAuto-cancelled after
Max instances per namespace10,000Active instances

File Upload Limits (for
file
field type)

LimitValue
Max file size10 MB
Allowed MIME typesConfigurable per field
Max files per field5
File retention7 days after instance completion

Rate Limits

Endpoint CategoryLimitWindow
Read operations1000 reqper minute
Write operations100 reqper minute
Instance creation50 reqper minute
Webhook deliveries1000 eventsper minute per subscription

Rate Limit Headers:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1705752060

Script Execution Environment

Runtime Specification

PropertyValue
RuntimeJavaScript (ES2020)
EngineQuickJS sandbox
Execution timeout5 seconds
Memory limit16 MB
Network accessDisabled (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

MetricDescription
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

MethodEndpointAuthSuccessDescription
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

EndpointError CodeHTTPDescription
All
unauthorized
401Missing or invalid token
All
forbidden
403Insufficient scopes
All
rate_limited
429Rate limit exceeded
All
internal_error
500Server error
POST /workflows
invalid_workflow
400Schema validation failed
POST /workflows
workflow_exists
409Name already taken
GET /workflows/{name}/*
workflow_not_found
404Workflow doesn't exist
POST /.../publish
version_not_found
404Version doesn't exist
POST /.../publish
already_published
400Version already published
POST /.../instances
workflow_not_published
400No published version
POST /.../instances
namespace_quota_exceeded
429Too many instances
GET /instances/{id}
instance_not_found
404Instance doesn't exist
POST /.../resume
instance_not_halted
400Instance not in halted state
POST /.../resume
invalid_transition
400Input doesn't match conditions
POST /.../resume
concurrent_modification
409Race condition
POST /.../cancel
instance_not_active
400Already completed/failed
Script execution
script_error
500Runtime error in script
Script execution
script_timeout
500Script exceeded 5s limit
HTTP logic
http_error
500External request failed
HTTP logic
http_timeout
500External 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

FeatureIntroducedNotes
Core workflow APIv1.0.0Stable
Webhook subscriptionsv1.1.0Stable
on_enter
for terminal states
v2.0.0Optional
Idempotency keysv1.2.0Recommended
Thread parallelismv1.3.0Stable
File field typev2.1.0Beta

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