Skip to main content

Orchestrations

A pipeline that runs your agents and tools in a fixed order, one step's output feeding the next, and pauses for a person when it needs one.

Overview

An orchestration is a directed graph: nodes that do work, and edges that say what runs next. Starting one produces a run, which accumulates state as it goes and can be read back at any point to see where it is.

The shape is forward-only and terminating: a run walks the graph and ends. That is the whole difference from a board — a board's cards cycle review → draft → review for as long as the work needs, so a board is a state machine you push cards around, while an orchestration is a pipeline that runs once and finishes.

A node does one of several things, named by its type:

typeWhat it does
agentRuns an agent, optionally parsing structured output.
toolCalls a tool — no model in the loop.
transformComputes a value from the run's state, with no external call.
conditionRoutes onward by matching an edge's condition label.
knowledgeQueries a knowledge collection.
memory_writeWrites to an actor's memory.
humanParks the run until a person supplies input.
approvalParks the run on an approval item, with its own expiry.
loopRuns a child orchestration once per item in a collection.
pollRe-calls a tool until an exit condition is true.
delayWaits, durably — the run survives a restart.
webhookParks the run awaiting an inbound callback.
emit_eventEmits an event any webhook can subscribe to.
sub_orchestrationRuns another orchestration as one step.

A node's remaining fields depend on its type, and this API passes the graph through exactly as you wrote it rather than reshaping it. The runtime is the authority on which combination is valid — which is why POST /v1/projects/{project_id}/orchestrations/validate exists: it runs the same checks create and update enforce, without persisting anything.

See the OpenAPI spec for the full endpoint and schema reference, or browse it rendered under API Reference → Orchestrations.

Data Model

Orchestration

FieldTypeDescription
idstringPublic orchestration ID (orch_ prefix).
project_idstringThe owning project.
namestringHuman-readable label.
descriptionstring, nullable
nodesobject[]The graph's nodes; each carries at least id and type.
edgesobject[]The connections; each carries at least from and to.
state_schemaobject, nullableJSON Schema the accumulated state is validated against.
input_schemaobject, nullableJSON Schema for a run's input. Its top-level properties seed state.
created_atstring (date-time)
updated_atstring (date-time)

Edge (edges[])

FieldTypeDescription
fromstringSource node id.
tostringTarget node id.
conditionstring, optionalFor routing out of a condition node — the label to match.
activation_groupstring, optionalGroups edges so several must arrive before the target runs.
activation_conditionall, any, optionalWhether all or any edge in the group must fire.

Orchestration run

FieldTypeDescription
idstringPublic run ID (orch_run_ prefix).
orchestration_idstringThe orchestration this run is of.
project_idstringThe owning project.
statusstringSee Run lifecycle.
stateobjectThe accumulated state, as of this read.
active_nodesstring[]Node ids currently executing.
artifactsobjectNode id → the output that node produced.
errorobject, nullableStructured failure detail; null unless status is failed.
trace_idstring, nullableThe trace recording what the run's agents did.
inputobject, nullableThe input the run was started with.
outputobject, nullableThe terminal node's artifact(s), once the run has succeeded.
node_executionsobject[]One record per node attempt (see below).
usageobjectToken/cost roll-up across the run's generations. Present on a single-run read; omitted from lists.
required_actionobject, nullableWhat the run is waiting for while awaiting_input.
started_atstring (date-time), nullable
completed_atstring (date-time), nullable
created_atstring (date-time)
updated_atstring (date-time)

Node execution (node_executions[])

The orchestration analogue of a trace step — what a node received, what it produced, and whether it worked.

FieldTypeDescription
node_idstringThe node that ran.
node_typestring, nullableIts type.
attemptinteger1-based. A node with a retry policy produces one record per attempt.
statuscompleted, failed, requires_action, skipped
inputobject, nullableThe resolved input the node received.
outputobject, nullableThe artifact it produced; null when it failed.
errorobject, nullableFailure detail.
started_atstring (date-time), nullable
completed_atstring (date-time), nullable

Key Concepts

A run is durable and asynchronous by default

Starting a run returns immediately with status: "queued", and a background worker picks it up. Progress is read by polling GET /v1/projects/{project_id}/orchestration-runs/{orchestration_run_id}.

That is the mode to build on: delay and poll waits park the run without holding a worker, and it survives a restart — a run sleeping on a two-hour delay is still there afterwards, on the same node, with the same state.

Pass wait: true on start to block until the run reaches a terminal or awaiting_input state and get the settled run back in one call. It is convenient for a short pipeline and for scripts; a run with any real waiting in it should not be started that way.

Run lifecycle

statusMeaning
queuedCreated, waiting for a worker.
runningActively executing.
sleepingParked on a delay/poll wait, or a node's retry backoff. No worker held.
awaiting_inputParked on a human, approval or webhook node.
succeededFinished. output holds the terminal artifact(s).
failedA node failed terminally. error says which and why.
cancelledCancelled before reaching a terminal state.
expiredA wait passed its deadline.

The first four are non-terminal; the last four are terminal, and cancelling a run that has already reached one is a 409.

Pausing for a person: resume is not human-input

Two routes act on an awaiting_input run, and they are not interchangeable:

Reach for human-input when a person has decided something. Reach for resume essentially never, unless a run looks stuck.

required_action on the run says which of the two is wanted: its type is human_input or webhook_receive, and it carries the node_id, the prompt shown to the reviewer, and any constrained options.

Handing a run a per-user credential

A pipeline is often run for somebody — their account on a third-party API, their calendar, their orders — while the orchestration, its agents and its tools are defined once for the whole project. tool_context on POST /v1/projects/{project_id}/orchestration-runs is where that per-run difference goes: a flat map of strings, forwarded by the runtime as X-Naturali-Context-<key> request headers on every http/mcp tool call the run's agent nodes make, including the agents of any loop or sub_orchestration child run.

The point is that a tool endpoint reads the value from a header it can trust instead of from model output. Nothing about the credential is written into a prompt, so no agent can be talked into revealing or altering it.

Three properties make it usable for a credential:

  • It is stored on the run, not attached to one request, so it survives an awaiting_input pause, a durable delay, a background drive and a restart — a run that resumes tomorrow still calls its tools with it.
  • It is never returned. Any principal on the project can read the project's runs, so the bag is write-only on this API: a run read shows no tool_context, the same way a tool read shows no auth headers.
  • It can be confined. By default every key reaches every tool the agent can call; set the tool's context_keys and a credential stops egressing to the rest of the tool set.

A key becomes an HTTP header name verbatim — no character is re-cased, so ocaToken and oca_token are different keys — and may contain only letters, digits and !#$%&'*+-.^_`|~. Two keys that differ only in case are rejected, because HTTP would fold them into one header and silently drop a value.

To land a value in the header a target already expects, have the tool declare Authorization: "Bearer {{context:ocaToken}}" in its own headers — see Tools → landing the value in Authorization.

Start a run for one user:

naturali start-orchestration-run \
--project-id proj_V1StGXR8Z5jdHi6B \
--orchestration-id orch_V1StGXR8Z5jdHi6B \
--input '{"theme":"spring collection"}' \
--tool-context '{"ocaToken":"usr_token_V1StGXR8Z5jdHi6B"}'

A trigger that fires the same orchestration on a schedule carries no tool_context — a schedule has no user to be run for. Start the run directly when it needs one.

Validate before you write

A graph is rejected on create and update when it has blocking errors — a duplicate node id, an edge naming a node that does not exist, a cycle with no loop node in it, an input_mapping reference that resolves to nothing. Those come back as 400 invalid_orchestration, with the runtime's own rule named under details.upstream_code.

POST /v1/projects/{project_id}/orchestrations/validate runs exactly those checks and persists nothing, so an editor can check a graph as it is being written. It also returns non-blocking warnings — a state key only written on one branch of a condition, for instance — which create and update let through.

Running one on a schedule

Nothing here holds a clock. To run a graph nightly, point a trigger at it with target_type: orchestration — each fire starts a run, and the trigger's input becomes that run's input, so the input_schema above sees the same shape either way. The firing's result carries the orch_run_ id it started.

Listing runs is scoped by orchestration

GET /v1/projects/{project_id}/orchestration-runs requires orchestration_id. It is not an optional filter: it is what scopes the list, so ask for one orchestration's runs at a time rather than the project's.

Deleting an orchestration deletes its runs

DELETE /v1/projects/{project_id}/orchestrations/{orchestration_id} removes the definition and every run of it, including their state, artifacts and node execution records. There is no guard for runs still in flight, and no way to get the history back. The traces the run's agents produced are separate records and survive.

Examples

Create a two-step pipeline: an agent drafts, a person approves.

naturali create-orchestration \
--project-id proj_V1StGXR8Z5jdHi6B \
--name "Draft and approve" \
--nodes '[{"id":"draft","type":"agent","agent_id":"agent_V1StGXR8Z5jdHi6B","input_mapping":{"theme":{"var":"theme"}},"state_mapping":{"draft":{"var":"output.content"}}},{"id":"review","type":"human","prompt":"Publish this draft?","options":["yes","no"]}]' \
--edges '[{"from":"draft","to":"review"}]' \
--input-schema '{"type":"object","properties":{"theme":{"type":"string"}}}'

Start a run of it:

naturali start-orchestration-run \
--project-id proj_V1StGXR8Z5jdHi6B \
--orchestration-id orch_V1StGXR8Z5jdHi6B \
--input '{"theme":"spring collection"}'

Once the run reports awaiting_input, answer the human node:

naturali submit-human-input \
--project-id proj_V1StGXR8Z5jdHi6B \
--orchestration-run-id orch_run_V1StGXR8Z5jdHi6B \
--node-id review \
--output '{"choice":"yes"}'