Skip to main content

Orchestrate a Sonnet

A poem pipeline with the Orchestrations module. Unlike Multi-Agent Sonnet with Nested Agent Calls, agents do not call one another: the graph invokes each agent directly, stores outputs in typed state, assembles the 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​

export SOAT_BASE_URL=http://localhost:5047

Step 1 — Log in as admin​

Admin is the built-in superuser role (Users).

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.

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 poem is persisted in a document; a fixed builtin tool updates it 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​

Five agents: one returns the title as JSON, four return a stanza or couplet as JSON.

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​

The orchestration stores each agent result in state, assembles the poem with a transform node, writes it with a tool node, and exposes it 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.object.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.object.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.object.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.object.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.object.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 output includes both terminal nodes: the document update and the 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 holds the 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​

get-orchestration-run exposes the accumulated orchestration state, every intermediate field included, unlike the nested-agent tutorial.

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

Key fields:

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

How It Works​

  • agent nodes call the five agents; state_mapping writes each result into state (state.title, state.stanza1, …).
  • transform assembles the poem with JSON Logic; tool persists it with a fixed documentId; return-poem exposes it in the run output.
  • Routing, sequencing and state live in the Orchestrations engine, not in an orchestrator agent as in Multi-Agent Sonnet with Nested Agent Calls.