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.
Build state-machine based automation with human-in-the-loop support through Interactor. Use when implementing approval flows, multi-step processes, automated pipelines, or any workflow requiring user input at specific stages.
author
Interactor Integration Guide
Interactor Workflows Skill
Build state-machine based automations with human-in-the-loop support for multi-step business processes.
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.
{"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.
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
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
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)
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..."}}
// 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 JavaScriptJSON// JSON.parse, JSON.stringifyDate// Date constructor and methodsMath// Math utilitiesconsole// console.log (for debugging, logged to instance history)Array// Array methodsObject// Object methodsString// String methodsNumber// Number methodsBoolean// Boolean typeRegExp// Regular expressions// NOT available (for security)
fetch // Use HTTP logic insteadrequire// No module importseval// DisabledFunction// Constructor disabledsetTimeout// Async not supportedsetInterval// Async not supported
Accessing Secrets in Scripts
Secrets are accessed via the secrets object (read-only):
{"$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"},
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다.GitHub에서 보기