Skip to main content

Orchestrate several agents

By the end of this tutorial you will have a support-reply orchestration of three agents, each doing one job in order — proven by a run that succeeded and whose record shows the facts, the draft and the checked reply each step produced.

Five steps:

  1. Create the three agents.
  2. Describe the pipeline and validate it.
  3. Create the orchestration.
  4. Start a run.
  5. Read the run back — the proof.

Every call is shown for all three clients. The ids in the responses are examples — copy the ones your own calls return.

Prerequisites​

  • A credential exported as NATURALI_TOKEN, a project id as PROJECT and a working AI provider id as PROVIDER — from Your first agent generation.
  • A client set up as in that tutorial.
export NATURALI_TOKEN=nat_sk_...
export PROJECT=proj_V1StGXR8Z5jdHi6B
export PROVIDER=aip_V1StGXR8Z5jdHi6B
Each agent step is a run

Every agent node in a run is one generation, so this pipeline counts as three runs against your plan's monthly allowance, and its managed tokens draw on the balance. A run never starts while the project owes for usage already served — see A negative balance stops a run before it starts.

1. Create the three agents​

Each agent does one job and reads only what the step before it handed over. An agent node receives its inputs as one user message, one key: value line per input, so the instructions name the inputs they expect.

The same call three times, with a different name and instructions:

naturali create-agent \
--project-id "$PROJECT" \
--ai-provider-id "$PROVIDER" \
--name support-researcher \
--instructions 'You receive a customer question and a policy. List, as short bullet points, only the policy facts that answer the question.'

naturali create-agent \
--project-id "$PROJECT" \
--ai-provider-id "$PROVIDER" \
--name support-writer \
--instructions 'You receive a customer question and a list of facts. Write a friendly reply of at most three sentences that uses only those facts.'

naturali create-agent \
--project-id "$PROJECT" \
--ai-provider-id "$PROVIDER" \
--name support-reviewer \
--instructions 'You receive facts and a draft reply. Return the draft, corrected so it states nothing the facts do not support. Return only the reply text, without quotes.'

Each call answers with the agent; the first:

{
"id": "agent_lBSeFLo1Z4NgQlm0",
"project_id": "proj_cT9LACJi0WypPf5U",
"ai_provider_id": "aip_CLcDNRe8GpP1pBNs",
"name": "support-researcher",
"version": 1
}
export RESEARCHER=agent_lBSeFLo1Z4NgQlm0
export WRITER=agent_VOJ1cafUEXZnR8ut
export REVIEWER=agent_iKQR3K4ivAFcVSzC

2. Describe the pipeline and validate it​

A pipeline is nodes (what each step does) and edges (what follows what). Each node here is an agent node:

  • input_mapping says what the agent receives. Each value is JSON Logic over the run's state: input.question is the run's input, facts is what an earlier node wrote.
  • state_mapping says where the agent's answer goes. output.content is the agent's text reply.

So the researcher writes facts, the writer reads facts and writes draft, and the reviewer reads both and writes reply.

NODES=$(cat <<EOF
[
{
"id": "research",
"type": "agent",
"agent_id": "$RESEARCHER",
"input_mapping": {
"question": { "var": "input.question" },
"policy": { "var": "input.policy" }
},
"state_mapping": { "facts": { "var": "output.content" } }
},
{
"id": "draft",
"type": "agent",
"agent_id": "$WRITER",
"input_mapping": {
"question": { "var": "input.question" },
"facts": { "var": "facts" }
},
"state_mapping": { "draft": { "var": "output.content" } }
},
{
"id": "review",
"type": "agent",
"agent_id": "$REVIEWER",
"input_mapping": {
"facts": { "var": "facts" },
"draft": { "var": "draft" }
},
"state_mapping": { "reply": { "var": "output.content" } }
}
]
EOF
)
EDGES='[{ "from": "research", "to": "draft" }, { "from": "draft", "to": "review" }]'

POST /v1/projects/{project_id}/orchestrations/validate checks the graph without saving it — an edge to a node that does not exist, a var that no step writes, a node missing a field its type needs.

naturali validate-orchestration \
--project-id "$PROJECT" \
--nodes "$NODES" \
--edges "$EDGES"
{ "valid": true, "errors": [], "warnings": [] }

3. Create the orchestration​

Saving the graph makes it version 1. output_mapping decides what a finished run's output holds — here just the reviewed reply — so a caller reads output.reply however the steps inside change later.

naturali create-orchestration \
--project-id "$PROJECT" \
--name support-reply \
--nodes "$NODES" \
--edges "$EDGES" \
--output-mapping '{ "reply": { "var": "state.reply" } }'
{
"id": "orch_HGy16aPwsrkszumb",
"project_id": "proj_cT9LACJi0WypPf5U",
"name": "support-reply",
"version": 1,
"nodes": [
{
"id": "research",
"type": "agent",
"agent_id": "agent_lBSeFLo1Z4NgQlm0",
"input_mapping": {
"policy": { "var": "input.policy" },
"question": { "var": "input.question" }
},
"state_mapping": { "facts": { "var": "output.content" } }
}
],
"edges": [
{ "from": "research", "to": "draft" },
{ "from": "draft", "to": "review" }
],
"output_mapping": { "reply": { "var": "state.reply" } },
"created_at": "2026-10-03T08:40:17.262Z"
}

(nodes trimmed to the first.)

export ORCHESTRATION=orch_HGy16aPwsrkszumb

4. Start a run​

A run takes the input the first node reads: the customer's question and the policy to answer it from. It is background by default — the call answers 201 with a queued run at once.

naturali start-orchestration-run \
--project-id "$PROJECT" \
--orchestration-id "$ORCHESTRATION" \
--input '{
"question": "Can I get a refund on a cake I picked up yesterday?",
"policy": "Custom cakes: refunds within 48 hours of pickup, with the receipt. Bread and pastries: no refunds, same-day exchanges only. Refunds go back to the original payment method within 5 business days."
}'
{
"id": "orch_run_6edpfLYy6vJtHfgH",
"orchestration_id": "orch_HGy16aPwsrkszumb",
"orchestration_version": 1,
"status": "queued",
"active_nodes": [],
"output": null,
"started_at": "2026-10-03T08:40:25.039Z"
}
export RUN=orch_run_6edpfLYy6vJtHfgH

Pass "wait": true in the body instead to hold the request open until the run settles.

5. Read the run back​

Poll the run until status is terminal — succeeded, failed, cancelled or expired. This one took about three seconds.

naturali get-orchestration-run \
--project-id "$PROJECT" \
--orchestration-run-id "$RUN"
{
"id": "orch_run_6edpfLYy6vJtHfgH",
"status": "succeeded",
"output": {
"reply": "Yes, a cake is eligible for a refund. It must be requested within 48 hours of pickup, and a receipt is required. Please note that refunds are processed back to the original payment method."
},
"node_executions": [
{
"node_id": "research",
"node_type": "agent",
"status": "completed",
"output": {
"object": null,
"content": "* Cakes are eligible for refunds.\n* Refunds must be requested within 48 hours of pickup.\n* A receipt is required.\n* Refunds are processed back to the original payment method."
}
},
{
"node_id": "draft",
"node_type": "agent",
"status": "completed",
"input": {
"facts": "* Cakes are eligible for refunds.\n* Refunds must be requested within 48 hours of pickup.\n* A receipt is required.\n* Refunds are processed back to the original payment method.",
"question": "Can I get a refund on a cake I picked up yesterday?"
},
"output": {
"object": null,
"content": "Yes, a cake is eligible for a refund, but it must be requested within 48 hours of pickup, and a receipt is required. Please note that refunds are processed back to the original payment method."
}
},
{
"node_id": "review",
"node_type": "agent",
"status": "completed",
"output": {
"object": null,
"content": "Yes, a cake is eligible for a refund. It must be requested within 48 hours of pickup, and a receipt is required. Please note that refunds are processed back to the original payment method."
}
}
],
"usage": {
"cost_usd": 0.00007313,
"input_tokens": 319,
"output_tokens": 127
},
"trace_id": "trace_2mWssjSFsGAd5d2D",
"completed_at": "2026-10-03T08:40:28.399Z"
}

(node_executions trimmed; each also carries its input and timings.)

That is the proof:

  • status is succeeded and output.reply is the reviewed answer, shaped by output_mapping.
  • node_executions lists research, draft and review in order, each completed, with the input it received and the output it produced — the writer's input is exactly the researcher's facts.
  • usage is the cost of all three steps together, and trace_id opens the same run in traces.

What's next​