Skip to main content

Choosing an Automation Model

SOAT has two ways to automate work that spans more than one step, and they are not variations of each other — they have different topologies and different lifetimes.

  • An orchestration is a pipeline that ends — a directed acyclic graph that starts, 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.

There is also a third answer, and it 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 requestA single agent — no graph
A deterministic, forward-only sequence of steps that runs and completesOrchestrations
Statuses, transitions, guards, a kanban board, or an entity that revisits statesWorkflows & Tasks

A support ticket that reopens. A lead that goes qualified → negotiating → stalled → negotiating. A kanban card dragged back a column. None of these fit a DAG — a task is the shape they need.

Neither module is a subset of the other. A workflow adds primitives a DAG cannot express; an orchestration carries almost all of the execution machinery. The two sections below say exactly which.

Step 0 — you may need neither

Both models are the graph layer, and the graph is the layer to build last. As The Layers of an Agent System puts it: harness first, loop second, graph last — and last frequently means never. A pipeline drawn around a problem the harness or the loop was going to solve anyway costs you a definition to version, a run to inspect, and a state contract to keep wired, and buys nothing.

Reach for a single agent first. Its loop already sequences work the graph layer would otherwise sequence for you, and three dials bound it:

DialWhat it does
max_stepsCaps how many reasoning steps the loop may take (default 20)
stop_conditionsEnds the loop as soon as the model calls a named tool — the has_tool_call condition, enforced alongside max_steps
output_schemaMakes the result a checked object rather than prose a downstream step must parse

Escalate to a graph when — and only when — the work needs something an agent loop cannot express:

SignalWhat only a graph gives you
Steps run in parallel and rejoinParallel execution rounds and activation_group fan-in. An agent loop is sequential
The branch must be auditable, not inferredA condition node emits a label an edge selects on, and every attempt lands in node_executions. A model choosing its own next step leaves no such record
A human signs off mid-runhuman and approval nodes park the run and resume it later
The wait outlives the requestDurable background executiondelay, poll, and the sleeping status let a run span hours or days holding no connection open
The entity revisits states over daysA workflow — see the rest of this page

None of these is a reason to throw the agent away: 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 the same way — by a client, or by a trigger that binds a starter to the target. Two of those starters answer the same question differently, and the choice is worth making deliberately:

  • An event trigger subscribes to an internal platform event (documents.ingested, agents.generation.completed, an orchestration's own emit_event) and starts work the moment it happens. Use it when the work is a reaction to something the platform already knows about and promptness is the point. Delivery is best-effort and unordered, and the reactive edge is capped by a causation depth guard so a cycle cannot run away.
  • A schedule trigger runs on a cron cadence and is recovered from the database, so a firing missed while the server was down is coalesced into one catch-up rather than lost. Use it when the work is periodic, when it must not be dropped, or as the backstop under an event trigger whose target is idempotent.

What only workflows have

CapabilityWhy a DAG cannot do it
Cyclesreview → draft, a card dragged back a columnGraph 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 queryGET /tasks?workflow_id=…&state=… is one column, with zero app-side state
Append-only transition history with principal_kind (user/api_key/automation/approval)node_executions records 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 detectionstalled_after emits tasks.stalled, re-armed on the next transitionA 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 therefore lives on the orchestration side, and a workflow reaches it by dispatching a run:

  • Every node typeagent, tool, transform, knowledge, memory_write, condition, human, approval, loop, poll, delay, emit_event, webhook, sub_orchestration — plus parallel execution rounds, activation_group fan-in, branch labels, and nested sub-graphs.
  • Durable background execution — a queue with leases and a reaper, postgres and sqs drivers, concurrency caps, queue metrics, the sleeping / awaiting_input / expired / cancelled statuses, and resume/cancel.
  • Per-node retry with fixed or exponential backoff and a delay ceiling. (A workflow's retry is a simpler per-dispatch policy for one state.)
  • The state / artifacts / nodes.<id> namespaces, input_schema and state_schema, and a node_executions 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

The two compose in both directions, and a state that dispatches a run is the normal case: 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, and none is needed:

{
"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 thing 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, so a chain of states keeps acting as whoever set it going rather than decaying to no principal at the second state. See Run identity.

Two rules for the composed edge

Keep the orchestration → workflow edge fire-and-forget. A graph that instead waits for a task to reach some state inverts the two lifetimes — a run is bounded and holds a lease and a queue slot, while a task lives for days and can move backward. Let the run end and let 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 can see: cycle detection is per-graph, and workflow cycles are deliberate. The task engine bounds it with an automation chain budget and refuses the hop that would exceed it — but the budget is a backstop, not a design. Reaching it means a loop is running unattended, which is worth an alert rather than a shrug. Bound the cycle in the graph you write; let the budget catch the case you missed.

What the two share

Both modules are built on the same platform machinery, in the same shape:

  • 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 all 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.