Skip to main content

Orchestrate a Sonnet

This tutorial shows how to build a poem pipeline with the Orchestrations module. Unlike Multi-Agent Sonnet with Nested Agent Calls, the agents here do not call one another. The orchestration graph invokes each agent directly, stores their outputs in typed state, assembles the final poem, and persists it through a builtin tool.

You will:

  1. Create a project, an AI provider, and a shared document for the poem.
  2. Create one fixed builtin tool that writes the finished poem to that document.
  3. Create five specialized agents: one for the title and four for the sonnet sections.
  4. Define an orchestration whose agent nodes call those agents in sequence.
  5. Run the orchestration with a theme and inspect both the persisted document and the run state.

Prerequisites

  • SOAT running locally. Follow the Quick Start guide to bring the stack up with Docker Compose.
  • New to SOAT? Read Key Concepts to understand projects, agents, tools, and runs before diving in.
  • CLI installed and configured, or SDK set up. See CLI or SDK.
  • For production hardening (secrets, env vars), see Configuration.
  • Server is at http://localhost:5047.
  • Ollama running locally with a chat model available. This tutorial uses a local Ollama provider so it can run without external credentials. To connect xAI, OpenAI, Anthropic, or Amazon Bedrock instead, see Connect Third-Party LLMs.
export SOAT_BASE_URL=http://localhost:5047

Step 1 — Log in as admin

Admin is the built-in superuser role. See Users for authentication details.

ADMIN_TOKEN=$(soat login-user --username admin --password Admin1234! | jq -r '.token')
export SOAT_TOKEN=$ADMIN_TOKEN

Step 2 — Create a project

Every resource lives inside a project. Create one for this workflow.

PROJECT_ID=$(soat create-project --name "Orchestrated Sonnet" | jq -r '.id')
echo "PROJECT_ID: $PROJECT_ID"

Step 3 — Create an AI provider

Create a local AI provider backed by Ollama.

AI_PROVIDER_ID=$(soat create-ai-provider \
--project-id "$PROJECT_ID" \
--name "Local Ollama" \
--provider "ollama" \
--default-model "qwen2.5:0.5b" | jq -r '.id')
echo "AI_PROVIDER_ID: $AI_PROVIDER_ID"

Step 4 — Create the poem document and a fixed write tool

The final poem will be persisted in a document. A fixed builtin tool will later update that document from the orchestration.

POEM_DOC_ID=$(soat create-document \
--project-id "$PROJECT_ID" \
--content "(empty - orchestration will write the poem)" \
--path "/poems/orchestrated-sonnet.txt" | jq -r '.id')
echo "POEM_DOC_ID: $POEM_DOC_ID"

WRITE_POEM_TOOL_ID=$(soat create-tool \
--project-id "$PROJECT_ID" \
--name "persist-poem" \
--type "builtin" \
--description "Write the completed poem to the shared document" \
--actions '["update-document"]' \
--preset-parameters '{"document_id": "'"$POEM_DOC_ID"'"}' | jq -r '.id')
echo "WRITE_POEM_TOOL_ID: $WRITE_POEM_TOOL_ID"

Step 5 — Create the title and stanza agents

Create five agents: one returns the title as JSON, and four return a stanza or couplet as JSON. The orchestration will call them directly in order.

TITLE_AGENT_ID=$(soat create-agent \
--project-id "$PROJECT_ID" \
--ai-provider-id "$AI_PROVIDER_ID" \
--name "Sonnet Title Agent" \
--instructions 'You receive context lines such as theme: "...". Return ONLY compact JSON matching {"title":"..."}. Create a short poetic sonnet title about the theme. No markdown, no code fences.' \
--max-steps 1 | jq -r '.id')

STANZA1_AGENT_ID=$(soat create-agent \
--project-id "$PROJECT_ID" \
--ai-provider-id "$AI_PROVIDER_ID" \
--name "Sonnet Stanza 1 Agent" \
--instructions 'You receive context lines such as theme, title, and previous stanzas. Return ONLY compact JSON matching {"stanza":"..."}. Write the first quatrain of a sonnet using ABAB rhyme. Exactly 4 lines. Do not repeat previous text.' \
--max-steps 1 | jq -r '.id')

STANZA2_AGENT_ID=$(soat create-agent \
--project-id "$PROJECT_ID" \
--ai-provider-id "$AI_PROVIDER_ID" \
--name "Sonnet Stanza 2 Agent" \
--instructions 'You receive context lines such as theme, title, and previous stanzas. Return ONLY compact JSON matching {"stanza":"..."}. Write the second quatrain of a sonnet using CDCD rhyme. Exactly 4 lines. Do not repeat previous text.' \
--max-steps 1 | jq -r '.id')

STANZA3_AGENT_ID=$(soat create-agent \
--project-id "$PROJECT_ID" \
--ai-provider-id "$AI_PROVIDER_ID" \
--name "Sonnet Stanza 3 Agent" \
--instructions 'You receive context lines such as theme, title, and previous stanzas. Return ONLY compact JSON matching {"stanza":"..."}. Write the third quatrain of a sonnet using EFEF rhyme. Exactly 4 lines. Do not repeat previous text.' \
--max-steps 1 | jq -r '.id')

STANZA4_AGENT_ID=$(soat create-agent \
--project-id "$PROJECT_ID" \
--ai-provider-id "$AI_PROVIDER_ID" \
--name "Sonnet Couplet Agent" \
--instructions 'You receive context lines such as theme, title, and previous stanzas. Return ONLY compact JSON matching {"stanza":"..."}. Write the final couplet of a sonnet using GG rhyme. Exactly 2 lines. Do not repeat previous text.' \
--max-steps 1 | jq -r '.id')

echo "TITLE_AGENT_ID: $TITLE_AGENT_ID"
echo "STANZA1_AGENT_ID: $STANZA1_AGENT_ID"
echo "STANZA2_AGENT_ID: $STANZA2_AGENT_ID"
echo "STANZA3_AGENT_ID: $STANZA3_AGENT_ID"
echo "STANZA4_AGENT_ID: $STANZA4_AGENT_ID"

Step 6 — Create the orchestration graph

This orchestration stores every agent result in state, assembles the poem with a transform node, writes it with a tool node, and exposes it again through a terminal return-poem node.

ORCH_NODES='[
{
"id": "generate-title",
"type": "agent",
"agent_id": "'"$TITLE_AGENT_ID"'",
"input_mapping": {"theme": {"var": "input.theme"}},
"output_schema": {"type": "object", "required": ["title"], "properties": {"title": {"type": "string"}}},
"state_mapping":{"state.title":{"var":"output.title"}}
},
{
"id": "generate-stanza-1",
"type": "agent",
"agent_id": "'"$STANZA1_AGENT_ID"'",
"input_mapping": {"theme": {"var": "input.theme"}, "title": {"var": "title"}},
"output_schema": {"type": "object", "required": ["stanza"], "properties": {"stanza": {"type": "string"}}},
"state_mapping":{"state.stanza1":{"var":"output.stanza"}}
},
{
"id": "generate-stanza-2",
"type": "agent",
"agent_id": "'"$STANZA2_AGENT_ID"'",
"input_mapping": {"theme": {"var": "input.theme"}, "title": {"var": "title"}, "stanza1": {"var": "stanza1"}},
"output_schema": {"type": "object", "required": ["stanza"], "properties": {"stanza": {"type": "string"}}},
"state_mapping":{"state.stanza2":{"var":"output.stanza"}}
},
{
"id": "generate-stanza-3",
"type": "agent",
"agent_id": "'"$STANZA3_AGENT_ID"'",
"input_mapping": {"theme": {"var": "input.theme"}, "title": {"var": "title"}, "stanza1": {"var": "stanza1"}, "stanza2": {"var": "stanza2"}},
"output_schema": {"type": "object", "required": ["stanza"], "properties": {"stanza": {"type": "string"}}},
"state_mapping":{"state.stanza3":{"var":"output.stanza"}}
},
{
"id": "generate-stanza-4",
"type": "agent",
"agent_id": "'"$STANZA4_AGENT_ID"'",
"input_mapping": {"theme": {"var": "input.theme"}, "title": {"var": "title"}, "stanza1": {"var": "stanza1"}, "stanza2": {"var": "stanza2"}, "stanza3": {"var": "stanza3"}},
"output_schema": {"type": "object", "required": ["stanza"], "properties": {"stanza": {"type": "string"}}},
"state_mapping":{"state.stanza4":{"var":"output.stanza"}}
},
{
"id": "assemble-poem",
"type": "transform",
"expression": {"cat": [{"var": "title"}, "\n\n", {"var": "stanza1"}, "\n\n", {"var": "stanza2"}, "\n\n", {"var": "stanza3"}, "\n\n", {"var": "stanza4"}]},
"state_mapping":{"state.poem":{"var":"output.result"}}
},
{
"id": "persist-poem",
"type": "tool",
"tool_id": "'"$WRITE_POEM_TOOL_ID"'",
"operation_id": "update-document",
"input_mapping": {"content": {"var": "poem"}}
},
{
"id": "return-poem",
"type": "transform",
"expression": {"var": "poem"}
}
]'

ORCH_EDGES='[
{"from": "generate-title", "to": "generate-stanza-1"},
{"from": "generate-stanza-1", "to": "generate-stanza-2"},
{"from": "generate-stanza-2", "to": "generate-stanza-3"},
{"from": "generate-stanza-3", "to": "generate-stanza-4"},
{"from": "generate-stanza-4", "to": "assemble-poem"},
{"from": "assemble-poem", "to": "persist-poem"},
{"from": "assemble-poem", "to": "return-poem"}
]'

ORCHESTRATION_ID=$(soat create-orchestration \
--project-id "$PROJECT_ID" \
--name "Direct Sonnet Orchestration" \
--description "Direct agent-node sonnet pipeline" \
--nodes "$ORCH_NODES" \
--edges "$ORCH_EDGES" | jq -r '.id')
echo "ORCHESTRATION_ID: $ORCHESTRATION_ID"

Step 7 — Start a run

Run the orchestration with a theme. The run output includes both terminal nodes: the persisted document update and the returned poem text.

RUN=$(soat start-orchestration-run \
--orchestration-id "$ORCHESTRATION_ID" \
--input '{"theme":"artificial intelligence"}')

printf '%s\n' "$RUN" | jq '{status, trace_id, output}'
RUN_ID=$(printf '%s\n' "$RUN" | jq -r '.id')

echo "\nFinal poem returned by the orchestration:\n"
printf '%s\n' "$RUN" | jq -r '.output["return-poem"].result'
echo "\nRUN_ID: $RUN_ID"

Expected status output:

{
"status": "succeeded",
"trace_id": "trace_example",
"output": {
"persist-poem": {
"id": "doc_example"
},
"return-poem": {
"result": "... poem text ..."
}
}
}

Step 8 — Read the persisted poem document

The document now contains the final poem written by the persist-poem tool node.

soat get-document --document-id "$POEM_DOC_ID" | jq -r '.content'

Step 9 — Inspect the run state

Use get-orchestration-run to inspect the accumulated orchestration state. This is the main difference from the nested-agent tutorial: you can see every intermediate field directly in the run record.

soat get-orchestration-run \
--orchestration-run-id "$RUN_ID" | jq '{status, state, output}'

Key fields to look for:

  • state.title
  • state.stanza1 through state.stanza4
  • state.poem
  • output["return-poem"].result

How It Works

  • agent nodes call the five agents directly; state_mapping writes each result into orchestration state (state.title, state.stanza1, …).
  • The transform node assembles the poem deterministically with JSON Logic; the tool node persists it using a fixed documentId; the terminal return-poem node exposes it in the run output.

Compared with Multi-Agent Sonnet with Nested Agent Calls, routing, sequencing, and state accumulation live in the Orchestrations engine instead of an orchestrator agent.