Choosing an Automation Model
SOAT has two ways to automate work that spans more than one step, with different topologies and lifetimes:
- An orchestration is a pipeline that ends — a directed acyclic graph that flows forward through its nodes and terminates.
- A workflow is a state graph a task lives in — a long-lived entity that moves between named states over days or weeks, and can move backward.
The third answer is the most common one:
| You want… | Use |
|---|---|
| Steps you cannot enumerate in advance, with no fan-out, no human gate, and no wait that outlives the request | A single agent — no graph |
| A deterministic, forward-only sequence of steps that runs and completes | Orchestrations |
| Statuses, transitions, guards, a kanban board, or an entity that revisits states | Workflows & Tasks |
A support ticket that reopens, a lead that goes qualified → negotiating → stalled → negotiating, a kanban card dragged back a column: none fit a DAG. Neither module is a
subset of the other.
Step 0 — you may need neither
Both models are the graph layer, the layer to build last (The Layers of an Agent System). Reach for a single agent first; three dials bound its loop:
| Dial | What it does |
|---|---|
max_steps | Caps how many reasoning steps the loop may take (default 20) |
stop_conditions | Ends the loop as soon as the model calls a named tool — the has_tool_call condition, enforced alongside max_steps |
output_schema | Makes the result a checked object rather than prose a downstream step must parse |
Escalate to a graph only when the work needs something an agent loop cannot express:
| Signal | What only a graph gives you |
|---|---|
| Steps run in parallel and rejoin | Parallel execution rounds and activation_group fan-in. An agent loop is sequential |
| The branch must be auditable, not inferred | A condition node emits a label an edge selects on, and every attempt lands in a node execution record. A model choosing its own next step leaves no such record |
| A human signs off mid-run | human and approval nodes park the run and resume it later |
| The wait outlives the request | Durable background execution — delay, poll, and the sleeping status let a run span hours or days holding no connection open |
| The entity revisits states over days | A workflow — see the rest of this page |
An agent is a node type, so a graph wraps the loop you already have rather than replacing it.
What starts them
Both are started by a client, or by a trigger:
- An
eventtrigger subscribes to an internal platform event (documents.ingested,generations.completed, an orchestration's ownemit_event). Delivery is best-effort and unordered, and the reactive edge is capped by a causation depth guard. Use it when the work is a reaction and promptness is the point. - A
scheduletrigger runs on a cron cadence and is recovered from the database: a firing missed while the server was down is coalesced into one catch-up. Use it when the work is periodic, must not be dropped, or as the backstop under an event trigger whose target is idempotent.
What only workflows have
| Capability | Why a DAG cannot do it |
|---|---|
Cycles — review → draft, a card dragged back a column | Graph validation rejects a cycle by design |
A long-lived entity — a task never terminates on its own (status: open / closed) | A run always drives toward a terminal node |
Named transitions as the only mutation path, with guards over {task, transition, principal} | A run advances by the edges the engine picks; there is nothing to fire |
A board query — GET /tasks?workflow_id=…&state=… is one column, with zero app-side state | — |
Append-only transition history with principal_type (user/api_key/automation/approval) | Node executions record execution, not who moved what |
Caller-owned mutable payload (shallow-merged on PATCH, validated by payload_schema) | A run's state is engine-owned |
Approval-gated transitions (requires_approval parks the move itself) | An approval node gates a tool call, not a state change |
Stall detection — stalled_after emits tasks.stalled, re-armed on the next transition | A run has no "sat here too long" concept |
| Alternate entry points — create a task directly in a named state | — |
What only orchestrations have
A workflow state's on_enter dispatches at most one thing — one agent generation or
one orchestration run. Everything below lives on the orchestration side, and a workflow
reaches it by dispatching a run:
- Every node type —
agent,tool,transform,knowledge,condition,human,approval,loop,poll,delay,emit_event,webhook,sub_orchestration— plus parallel execution rounds,activation_groupfan-in, branch labels, and nested sub-graphs. - Durable background execution — a queue with leases and a reaper,
postgresandsqsdrivers, concurrency caps, queue metrics, thesleeping/awaiting_input/expired/cancelledstatuses, and resume/cancel. - Per-node retry with
fixedorexponentialbackoff and a delay ceiling. (A workflow'sretryis a simpler per-dispatch policy for one state.) - The
state/artifacts/nodes.<id>namespaces,input_schemaandstate_schema, and a node execution record per attempt. - Usage roll-up — tokens and cost summed across every metered generation in the run, plus a linked trace.
- Guardrail interception on tool nodes.
How they compose
A workflow never replaces a run; it drives one.
Workflow → orchestration. A state's on_enter names an orchestration_id and
resolves the run input from the task with an input_mapping. See
Per-state automation.
Orchestration → workflow. A graph moves a task on with an ordinary tool node bound
to a builtin tool for create-task or transition-task.
There is no dedicated node type:
{
"id": "advance",
"type": "tool",
"tool_id": "tool_transition",
"operation_id": "transition-task",
"input_mapping": {
"task_id": { "var": "input.task_id" },
"transition": "finish"
}
}
A dispatched agent does the same with a builtin tool of its own.
Both kinds of dispatch act as the principal that started the chain (the person or key that created the task or fired the transition), and each automated hop inherits that identity. See Run identity.
Two rules for the composed edge
Keep the orchestration → workflow edge fire-and-forget. A run is bounded and holds a lease and a queue slot; a task lives for days and can move backward. Let the run end and the task's own state machine carry things forward.
Bound the cycle yourself. A state whose orchestration transitions the task back into that same state is a cycle no validator sees (cycle detection is per-graph; workflow cycles are deliberate). The task engine's automation chain budget refuses the hop that would exceed it; reaching it means a loop is running unattended and deserves an alert.
What the two share
- Versioning with instance pinning — a definition write archives a version, and a task or run executes the version it entered on. See workflow versioning and orchestration versioning.
- Declarative deployment — both are formation resource types, so a workflow, the orchestrations its states dispatch, and the agents those graphs call deploy as one stack.
- JSON Logic expressions for
input_mapping, guards, and conditions. - Webhook events for lifecycle changes, and the same project-scoped IAM enforcement.