Deploy a Multi-Agent App with Agent Formation
Deploy the pipeline from Multi-Agent Sonnet with Nested Agent Calls (an orchestrator delegating stanzas to four sub-agents) with a single Agent Formation template: write a template for all 14 resources, validate and preview it, deploy, run the orchestrator, update, and delete.
Prerequisites
- SOAT running locally at
http://localhost:5047(Quick Start); Key Concepts; Configuration. - Multi-Agent Sonnet with Nested Agent Calls for the same pipeline built step by step.
- CLI or SDK.
- Ollama with
qwen2.5:0.5bpulled (ollama pull qwen2.5:0.5b).
- CLI
- SDK
- curl
export SOAT_BASE_URL=http://localhost:5047
import { createConfig, SoatClient } from '@soat/sdk';
const config = createConfig({
baseUrl: 'http://localhost:5047',
auth: '',
});
const adminSoat = new SoatClient(config);
export SOAT_URL=http://localhost:5047
Step 1 — Log in as admin
See Users.
- CLI
- SDK
- curl
ADMIN_TOKEN=$(soat login-user --username admin --password Admin1234! | jq -r '.token')
export SOAT_TOKEN=$ADMIN_TOKEN
const { data: session } = await adminSoat.users.loginUser({
body: { username: 'admin', password: 'Admin1234!' },
});
const ADMIN_TOKEN = session.token;
const authConfig = createConfig({
baseUrl: 'http://localhost:5047',
auth: ADMIN_TOKEN,
});
const authClient = new SoatClient(authConfig);
ADMIN_TOKEN=$(curl -s -X POST "$SOAT_URL/api/v1/users/login" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"Admin1234!"}' | jq -r '.token')
Step 2 — Create a project
All resources are scoped to a project.
- CLI
- SDK
- curl
PROJECT_ID=$(soat create-project --name 'Sonnet Workshop' | jq -r '.id')
echo "PROJECT_ID: $PROJECT_ID"
const { data: project } = await authClient.projects.createProject({
body: { name: 'Sonnet Workshop' },
});
const PROJECT_ID = project.id;
PROJECT_ID=$(curl -s -X POST "$SOAT_URL/api/v1/projects" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Sonnet Workshop"}' | jq -r '.id')
echo "PROJECT_ID: $PROJECT_ID"
Step 3 — Write the formation template
A formation template is a JSON object with a resources map and an optional outputs map. This one declares 14 resources: an Ollama provider, a shared poem document, read/write tools, four stanza agents, five orchestrator tools, and the orchestrator. { "ref": "logicalId" } expressions resolve in dependency order, wiring tool_bindings[].tool_id, ai_provider_id, and nested preset_parameters.agentId. For xAI, OpenAI, Anthropic, or Amazon Bedrock instead of Ollama, see Connect Third-Party LLMs.
- CLI
- SDK
- curl
cat > formation.json << 'EOF'
{
"resources": {
"provider": {
"type": "ai_provider",
"properties": {
"name": "Sonnet Ollama",
"provider": "ollama",
"default_model": "qwen2.5:0.5b"
}
},
"poemDoc": {
"type": "document",
"properties": {
"content": "(empty - will be overwritten by stanza agents)",
"path": "/poems/sonnet.txt"
}
},
"poemReadTool": {
"type": "tool",
"properties": {
"name": "poem-read",
"type": "builtin",
"description": "Read the shared poem document",
"actions": ["get-document"],
"preset_parameters": { "document_id": { "ref": "poemDoc" } }
}
},
"poemWriteTool": {
"type": "tool",
"properties": {
"name": "poem-write",
"type": "builtin",
"description": "Update the shared poem document",
"actions": ["update-document"],
"preset_parameters": { "document_id": { "ref": "poemDoc" } }
}
},
"stanza1Agent": {
"type": "agent",
"properties": {
"name": "Stanza 1 - First Quatrain",
"ai_provider_id": { "ref": "provider" },
"instructions": "You are deterministic stanza worker 1. Do exactly two tool calls: first poem-read, then poem-write. Never ask follow-up questions. Write the poem title on the first line, add a blank line, then write the FIRST quatrain (4 lines) using ABAB. In poem-write, set content to the full poem-so-far including your stanza.",
"tool_bindings": [{ "tool_id": { "ref": "poemReadTool" } }, { "tool_id": { "ref": "poemWriteTool" } }],
"step_rules": [
{ "step": 1, "tool_choice": { "type": "tool", "tool_name": "poem-read_get-document" } },
{ "step": 2, "tool_choice": { "type": "tool", "tool_name": "poem-write_update-document" } }
],
"max_steps": 5
}
},
"stanza2Agent": {
"type": "agent",
"properties": {
"name": "Stanza 2 - Second Quatrain",
"ai_provider_id": { "ref": "provider" },
"instructions": "You are deterministic stanza worker 2. Do exactly two tool calls: first poem-read, then poem-write. Never ask follow-up questions. Write the SECOND quatrain (4 lines) using CDCD. In poem-write, set content to the full poem-so-far including your stanza.",
"tool_bindings": [{ "tool_id": { "ref": "poemReadTool" } }, { "tool_id": { "ref": "poemWriteTool" } }],
"step_rules": [
{ "step": 1, "tool_choice": { "type": "tool", "tool_name": "poem-read_get-document" } },
{ "step": 2, "tool_choice": { "type": "tool", "tool_name": "poem-write_update-document" } }
],
"max_steps": 5
}
},
"stanza3Agent": {
"type": "agent",
"properties": {
"name": "Stanza 3 - Third Quatrain",
"ai_provider_id": { "ref": "provider" },
"instructions": "You are deterministic stanza worker 3. Do exactly two tool calls: first poem-read, then poem-write. Never ask follow-up questions. Write the THIRD quatrain (4 lines) using EFEF. In poem-write, set content to the full poem-so-far including your stanza.",
"tool_bindings": [{ "tool_id": { "ref": "poemReadTool" } }, { "tool_id": { "ref": "poemWriteTool" } }],
"step_rules": [
{ "step": 1, "tool_choice": { "type": "tool", "tool_name": "poem-read_get-document" } },
{ "step": 2, "tool_choice": { "type": "tool", "tool_name": "poem-write_update-document" } }
],
"max_steps": 5
}
},
"stanza4Agent": {
"type": "agent",
"properties": {
"name": "Stanza 4 - Final Couplet",
"ai_provider_id": { "ref": "provider" },
"instructions": "You are deterministic stanza worker 4. Do exactly two tool calls: first poem-read, then poem-write. Never ask follow-up questions. Write the FINAL couplet (2 lines) using GG. In poem-write, set content to the full poem-so-far including your couplet.",
"tool_bindings": [{ "tool_id": { "ref": "poemReadTool" } }, { "tool_id": { "ref": "poemWriteTool" } }],
"step_rules": [
{ "step": 1, "tool_choice": { "type": "tool", "tool_name": "poem-read_get-document" } },
{ "step": 2, "tool_choice": { "type": "tool", "tool_name": "poem-write_update-document" } }
],
"max_steps": 5
}
},
"callStanza1Tool": {
"type": "tool",
"properties": {
"name": "call-stanza-1",
"type": "builtin",
"description": "Call stanza 1 agent",
"actions": ["create-agent-generation"],
"preset_parameters": {
"agent_id": { "ref": "stanza1Agent" },
"messages": [{ "role": "user", "content": "Theme: artificial intelligence. Write stanza 1 with title + first quatrain." }]
}
}
},
"callStanza2Tool": {
"type": "tool",
"properties": {
"name": "call-stanza-2",
"type": "builtin",
"description": "Call stanza 2 agent",
"actions": ["create-agent-generation"],
"preset_parameters": {
"agent_id": { "ref": "stanza2Agent" },
"messages": [{ "role": "user", "content": "Theme: artificial intelligence. Write stanza 2 (second quatrain)." }]
}
}
},
"callStanza3Tool": {
"type": "tool",
"properties": {
"name": "call-stanza-3",
"type": "builtin",
"description": "Call stanza 3 agent",
"actions": ["create-agent-generation"],
"preset_parameters": {
"agent_id": { "ref": "stanza3Agent" },
"messages": [{ "role": "user", "content": "Theme: artificial intelligence. Write stanza 3 (third quatrain)." }]
}
}
},
"callStanza4Tool": {
"type": "tool",
"properties": {
"name": "call-stanza-4",
"type": "builtin",
"description": "Call stanza 4 agent",
"actions": ["create-agent-generation"],
"preset_parameters": {
"agent_id": { "ref": "stanza4Agent" },
"messages": [{ "role": "user", "content": "Theme: artificial intelligence. Write stanza 4 (final couplet)." }]
}
}
},
"readFinalPoemTool": {
"type": "tool",
"properties": {
"name": "read-final-poem",
"type": "builtin",
"description": "Read the final poem from the shared document",
"actions": ["get-document"],
"preset_parameters": { "document_id": { "ref": "poemDoc" } }
}
},
"orchestrator": {
"type": "agent",
"properties": {
"name": "Sonnet Orchestrator",
"ai_provider_id": { "ref": "provider" },
"instructions": "Call tools in this exact order: call-stanza-1, call-stanza-2, call-stanza-3, call-stanza-4, then read-final-poem. Do not ask follow-up questions. Return ONLY the poem text.",
"tool_bindings": [
{ "tool_id": { "ref": "callStanza1Tool" } },
{ "tool_id": { "ref": "callStanza2Tool" } },
{ "tool_id": { "ref": "callStanza3Tool" } },
{ "tool_id": { "ref": "callStanza4Tool" } },
{ "tool_id": { "ref": "readFinalPoemTool" } }
],
"step_rules": [
{ "step": 1, "tool_choice": { "type": "tool", "tool_name": "call-stanza-1_create-agent-generation" } },
{ "step": 2, "tool_choice": { "type": "tool", "tool_name": "call-stanza-2_create-agent-generation" } },
{ "step": 3, "tool_choice": { "type": "tool", "tool_name": "call-stanza-3_create-agent-generation" } },
{ "step": 4, "tool_choice": { "type": "tool", "tool_name": "call-stanza-4_create-agent-generation" } },
{ "step": 5, "tool_choice": { "type": "tool", "tool_name": "read-final-poem_get-document" } }
],
"max_steps": 8
}
}
},
"outputs": {
"orchestrator_id": { "ref": "orchestrator" },
"poem_doc_id": { "ref": "poemDoc" }
}
}
EOF
TEMPLATE=$(cat formation.json)
const template = {
resources: {
provider: {
type: 'ai_provider',
properties: {
name: 'Sonnet Ollama',
provider: 'ollama',
default_model: 'qwen2.5:0.5b',
},
},
poemDoc: {
type: 'document',
properties: {
content: '(empty - will be overwritten by stanza agents)',
path: '/poems/sonnet.txt',
},
},
poemReadTool: {
type: 'tool',
properties: {
name: 'poem-read',
type: 'soat',
description: 'Read the shared poem document',
actions: ['get-document'],
preset_parameters: { documentId: { ref: 'poemDoc' } },
},
},
poemWriteTool: {
type: 'tool',
properties: {
name: 'poem-write',
type: 'soat',
description: 'Update the shared poem document',
actions: ['update-document'],
preset_parameters: { documentId: { ref: 'poemDoc' } },
},
},
stanza1Agent: {
type: 'agent',
properties: {
name: 'Stanza 1 - First Quatrain',
ai_provider_id: { ref: 'provider' },
instructions:
'You are deterministic stanza worker 1. Do exactly two tool calls: first poem-read, then poem-write. Never ask follow-up questions. Write the poem title on the first line, add a blank line, then write the FIRST quatrain (4 lines) using ABAB. In poem-write, set content to the full poem-so-far including your stanza.',
tool_bindings: [{ tool_id: { ref: 'poemReadTool' } }, { tool_id: { ref: 'poemWriteTool' } }],
step_rules: [
{
step: 1,
tool_choice: { type: 'tool', tool_name: 'poem-read_get-document' },
},
{
step: 2,
tool_choice: {
type: 'tool',
tool_name: 'poem-write_update-document',
},
},
],
max_steps: 5,
},
},
stanza2Agent: {
type: 'agent',
properties: {
name: 'Stanza 2 - Second Quatrain',
ai_provider_id: { ref: 'provider' },
instructions:
'You are deterministic stanza worker 2. Do exactly two tool calls: first poem-read, then poem-write. Never ask follow-up questions. Write the SECOND quatrain (4 lines) using CDCD. In poem-write, set content to the full poem-so-far including your stanza.',
tool_bindings: [{ tool_id: { ref: 'poemReadTool' } }, { tool_id: { ref: 'poemWriteTool' } }],
step_rules: [
{
step: 1,
tool_choice: { type: 'tool', tool_name: 'poem-read_get-document' },
},
{
step: 2,
tool_choice: {
type: 'tool',
tool_name: 'poem-write_update-document',
},
},
],
max_steps: 5,
},
},
stanza3Agent: {
type: 'agent',
properties: {
name: 'Stanza 3 - Third Quatrain',
ai_provider_id: { ref: 'provider' },
instructions:
'You are deterministic stanza worker 3. Do exactly two tool calls: first poem-read, then poem-write. Never ask follow-up questions. Write the THIRD quatrain (4 lines) using EFEF. In poem-write, set content to the full poem-so-far including your stanza.',
tool_bindings: [{ tool_id: { ref: 'poemReadTool' } }, { tool_id: { ref: 'poemWriteTool' } }],
step_rules: [
{
step: 1,
tool_choice: { type: 'tool', tool_name: 'poem-read_get-document' },
},
{
step: 2,
tool_choice: {
type: 'tool',
tool_name: 'poem-write_update-document',
},
},
],
max_steps: 5,
},
},
stanza4Agent: {
type: 'agent',
properties: {
name: 'Stanza 4 - Final Couplet',
ai_provider_id: { ref: 'provider' },
instructions:
'You are deterministic stanza worker 4. Do exactly two tool calls: first poem-read, then poem-write. Never ask follow-up questions. Write the FINAL couplet (2 lines) using GG. In poem-write, set content to the full poem-so-far including your couplet.',
tool_bindings: [{ tool_id: { ref: 'poemReadTool' } }, { tool_id: { ref: 'poemWriteTool' } }],
step_rules: [
{
step: 1,
tool_choice: { type: 'tool', tool_name: 'poem-read_get-document' },
},
{
step: 2,
tool_choice: {
type: 'tool',
tool_name: 'poem-write_update-document',
},
},
],
max_steps: 5,
},
},
callStanza1Tool: {
type: 'tool',
properties: {
name: 'call-stanza-1',
type: 'soat',
description: 'Call stanza 1 agent',
actions: ['create-agent-generation'],
preset_parameters: {
agentId: { ref: 'stanza1Agent' },
messages: [
{
role: 'user',
content:
'Theme: artificial intelligence. Write stanza 1 with title + first quatrain.',
},
],
},
},
},
callStanza2Tool: {
type: 'tool',
properties: {
name: 'call-stanza-2',
type: 'soat',
description: 'Call stanza 2 agent',
actions: ['create-agent-generation'],
preset_parameters: {
agentId: { ref: 'stanza2Agent' },
messages: [
{
role: 'user',
content:
'Theme: artificial intelligence. Write stanza 2 (second quatrain).',
},
],
},
},
},
callStanza3Tool: {
type: 'tool',
properties: {
name: 'call-stanza-3',
type: 'soat',
description: 'Call stanza 3 agent',
actions: ['create-agent-generation'],
preset_parameters: {
agentId: { ref: 'stanza3Agent' },
messages: [
{
role: 'user',
content:
'Theme: artificial intelligence. Write stanza 3 (third quatrain).',
},
],
},
},
},
callStanza4Tool: {
type: 'tool',
properties: {
name: 'call-stanza-4',
type: 'soat',
description: 'Call stanza 4 agent',
actions: ['create-agent-generation'],
preset_parameters: {
agentId: { ref: 'stanza4Agent' },
messages: [
{
role: 'user',
content:
'Theme: artificial intelligence. Write stanza 4 (final couplet).',
},
],
},
},
},
readFinalPoemTool: {
type: 'tool',
properties: {
name: 'read-final-poem',
type: 'soat',
description: 'Read the final poem from the shared document',
actions: ['get-document'],
preset_parameters: { documentId: { ref: 'poemDoc' } },
},
},
orchestrator: {
type: 'agent',
properties: {
name: 'Sonnet Orchestrator',
ai_provider_id: { ref: 'provider' },
instructions:
'Call tools in this exact order: call-stanza-1, call-stanza-2, call-stanza-3, call-stanza-4, then read-final-poem. Do not ask follow-up questions. Return ONLY the poem text.',
tool_bindings: [
{ tool_id: { ref: 'callStanza1Tool' } },
{ tool_id: { ref: 'callStanza2Tool' } },
{ tool_id: { ref: 'callStanza3Tool' } },
{ tool_id: { ref: 'callStanza4Tool' } },
{ tool_id: { ref: 'readFinalPoemTool' } }
],
step_rules: [
{
step: 1,
tool_choice: {
type: 'tool',
tool_name: 'call-stanza-1_create-agent-generation',
},
},
{
step: 2,
tool_choice: {
type: 'tool',
tool_name: 'call-stanza-2_create-agent-generation',
},
},
{
step: 3,
tool_choice: {
type: 'tool',
tool_name: 'call-stanza-3_create-agent-generation',
},
},
{
step: 4,
tool_choice: {
type: 'tool',
tool_name: 'call-stanza-4_create-agent-generation',
},
},
{
step: 5,
tool_choice: {
type: 'tool',
tool_name: 'read-final-poem_get-document',
},
},
],
max_steps: 8,
},
},
},
outputs: {
orchestrator_id: { ref: 'orchestrator' },
poem_doc_id: { ref: 'poemDoc' },
},
};
cat > formation.json << 'EOF'
{
"resources": {
"provider": {
"type": "ai_provider",
"properties": {
"name": "Sonnet Ollama",
"provider": "ollama",
"default_model": "qwen2.5:0.5b"
}
},
"poemDoc": {
"type": "document",
"properties": {
"content": "(empty - will be overwritten by stanza agents)",
"path": "/poems/sonnet.txt"
}
},
"poemReadTool": {
"type": "tool",
"properties": {
"name": "poem-read",
"type": "builtin",
"description": "Read the shared poem document",
"actions": ["get-document"],
"preset_parameters": { "document_id": { "ref": "poemDoc" } }
}
},
"poemWriteTool": {
"type": "tool",
"properties": {
"name": "poem-write",
"type": "builtin",
"description": "Update the shared poem document",
"actions": ["update-document"],
"preset_parameters": { "document_id": { "ref": "poemDoc" } }
}
},
"stanza1Agent": {
"type": "agent",
"properties": {
"name": "Stanza 1 - First Quatrain",
"ai_provider_id": { "ref": "provider" },
"instructions": "You are deterministic stanza worker 1. Do exactly two tool calls: first poem-read, then poem-write. Never ask follow-up questions. Write the poem title on the first line, add a blank line, then write the FIRST quatrain (4 lines) using ABAB. In poem-write, set content to the full poem-so-far including your stanza.",
"tool_bindings": [{ "tool_id": { "ref": "poemReadTool" } }, { "tool_id": { "ref": "poemWriteTool" } }],
"step_rules": [
{ "step": 1, "tool_choice": { "type": "tool", "tool_name": "poem-read_get-document" } },
{ "step": 2, "tool_choice": { "type": "tool", "tool_name": "poem-write_update-document" } }
],
"max_steps": 5
}
},
"stanza2Agent": {
"type": "agent",
"properties": {
"name": "Stanza 2 - Second Quatrain",
"ai_provider_id": { "ref": "provider" },
"instructions": "You are deterministic stanza worker 2. Do exactly two tool calls: first poem-read, then poem-write. Never ask follow-up questions. Write the SECOND quatrain (4 lines) using CDCD. In poem-write, set content to the full poem-so-far including your stanza.",
"tool_bindings": [{ "tool_id": { "ref": "poemReadTool" } }, { "tool_id": { "ref": "poemWriteTool" } }],
"step_rules": [
{ "step": 1, "tool_choice": { "type": "tool", "tool_name": "poem-read_get-document" } },
{ "step": 2, "tool_choice": { "type": "tool", "tool_name": "poem-write_update-document" } }
],
"max_steps": 5
}
},
"stanza3Agent": {
"type": "agent",
"properties": {
"name": "Stanza 3 - Third Quatrain",
"ai_provider_id": { "ref": "provider" },
"instructions": "You are deterministic stanza worker 3. Do exactly two tool calls: first poem-read, then poem-write. Never ask follow-up questions. Write the THIRD quatrain (4 lines) using EFEF. In poem-write, set content to the full poem-so-far including your stanza.",
"tool_bindings": [{ "tool_id": { "ref": "poemReadTool" } }, { "tool_id": { "ref": "poemWriteTool" } }],
"step_rules": [
{ "step": 1, "tool_choice": { "type": "tool", "tool_name": "poem-read_get-document" } },
{ "step": 2, "tool_choice": { "type": "tool", "tool_name": "poem-write_update-document" } }
],
"max_steps": 5
}
},
"stanza4Agent": {
"type": "agent",
"properties": {
"name": "Stanza 4 - Final Couplet",
"ai_provider_id": { "ref": "provider" },
"instructions": "You are deterministic stanza worker 4. Do exactly two tool calls: first poem-read, then poem-write. Never ask follow-up questions. Write the FINAL couplet (2 lines) using GG. In poem-write, set content to the full poem-so-far including your couplet.",
"tool_bindings": [{ "tool_id": { "ref": "poemReadTool" } }, { "tool_id": { "ref": "poemWriteTool" } }],
"step_rules": [
{ "step": 1, "tool_choice": { "type": "tool", "tool_name": "poem-read_get-document" } },
{ "step": 2, "tool_choice": { "type": "tool", "tool_name": "poem-write_update-document" } }
],
"max_steps": 5
}
},
"callStanza1Tool": {
"type": "tool",
"properties": {
"name": "call-stanza-1",
"type": "builtin",
"description": "Call stanza 1 agent",
"actions": ["create-agent-generation"],
"preset_parameters": {
"agent_id": { "ref": "stanza1Agent" },
"messages": [{ "role": "user", "content": "Theme: artificial intelligence. Write stanza 1 with title + first quatrain." }]
}
}
},
"callStanza2Tool": {
"type": "tool",
"properties": {
"name": "call-stanza-2",
"type": "builtin",
"description": "Call stanza 2 agent",
"actions": ["create-agent-generation"],
"preset_parameters": {
"agent_id": { "ref": "stanza2Agent" },
"messages": [{ "role": "user", "content": "Theme: artificial intelligence. Write stanza 2 (second quatrain)." }]
}
}
},
"callStanza3Tool": {
"type": "tool",
"properties": {
"name": "call-stanza-3",
"type": "builtin",
"description": "Call stanza 3 agent",
"actions": ["create-agent-generation"],
"preset_parameters": {
"agent_id": { "ref": "stanza3Agent" },
"messages": [{ "role": "user", "content": "Theme: artificial intelligence. Write stanza 3 (third quatrain)." }]
}
}
},
"callStanza4Tool": {
"type": "tool",
"properties": {
"name": "call-stanza-4",
"type": "builtin",
"description": "Call stanza 4 agent",
"actions": ["create-agent-generation"],
"preset_parameters": {
"agent_id": { "ref": "stanza4Agent" },
"messages": [{ "role": "user", "content": "Theme: artificial intelligence. Write stanza 4 (final couplet)." }]
}
}
},
"readFinalPoemTool": {
"type": "tool",
"properties": {
"name": "read-final-poem",
"type": "builtin",
"description": "Read the final poem from the shared document",
"actions": ["get-document"],
"preset_parameters": { "document_id": { "ref": "poemDoc" } }
}
},
"orchestrator": {
"type": "agent",
"properties": {
"name": "Sonnet Orchestrator",
"ai_provider_id": { "ref": "provider" },
"instructions": "Call tools in this exact order: call-stanza-1, call-stanza-2, call-stanza-3, call-stanza-4, then read-final-poem. Do not ask follow-up questions. Return ONLY the poem text.",
"tool_bindings": [
{ "tool_id": { "ref": "callStanza1Tool" } },
{ "tool_id": { "ref": "callStanza2Tool" } },
{ "tool_id": { "ref": "callStanza3Tool" } },
{ "tool_id": { "ref": "callStanza4Tool" } },
{ "tool_id": { "ref": "readFinalPoemTool" } }
],
"step_rules": [
{ "step": 1, "tool_choice": { "type": "tool", "tool_name": "call-stanza-1_create-agent-generation" } },
{ "step": 2, "tool_choice": { "type": "tool", "tool_name": "call-stanza-2_create-agent-generation" } },
{ "step": 3, "tool_choice": { "type": "tool", "tool_name": "call-stanza-3_create-agent-generation" } },
{ "step": 4, "tool_choice": { "type": "tool", "tool_name": "call-stanza-4_create-agent-generation" } },
{ "step": 5, "tool_choice": { "type": "tool", "tool_name": "read-final-poem_get-document" } }
],
"max_steps": 8
}
}
},
"outputs": {
"orchestrator_id": { "ref": "orchestrator" },
"poem_doc_id": { "ref": "poemDoc" }
}
}
EOF
TEMPLATE=$(cat formation.json)
Step 4 — Validate the template
Validation rules: Formations.
- CLI
- SDK
- curl
soat validate-formation --template "$TEMPLATE"
Expected output:
{ "valid": true }
const { data: validation } = await authClient.formations.validateFormation({
body: { template },
});
console.log('Valid:', validation.valid);
curl -s -X POST "$SOAT_URL/api/v1/formations/validate" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"template\": $TEMPLATE}" | jq '.'
Step 5 — Preview the deployment plan
The plan lists every resource the deploy will create (Formations).
- CLI
- SDK
- curl
soat plan-formation --project_id "$PROJECT_ID" --template "$TEMPLATE" | jq '.'
Expected output — 14 resources all marked as create:
[
{ "action": "create", "logical_id": "provider", "type": "ai_provider" },
{ "action": "create", "logical_id": "poemDoc", "type": "document" },
{ "action": "create", "logical_id": "poemReadTool", "type": "tool" },
{ "action": "create", "logical_id": "poemWriteTool", "type": "tool" },
{ "action": "create", "logical_id": "stanza1Agent", "type": "agent" },
{ "action": "create", "logical_id": "stanza2Agent", "type": "agent" },
{ "action": "create", "logical_id": "stanza3Agent", "type": "agent" },
{ "action": "create", "logical_id": "stanza4Agent", "type": "agent" },
{ "action": "create", "logical_id": "callStanza1Tool", "type": "tool" },
{ "action": "create", "logical_id": "callStanza2Tool", "type": "tool" },
{ "action": "create", "logical_id": "callStanza3Tool", "type": "tool" },
{ "action": "create", "logical_id": "callStanza4Tool", "type": "tool" },
{
"action": "create",
"logical_id": "readFinalPoemTool",
"type": "tool"
},
{ "action": "create", "logical_id": "orchestrator", "type": "agent" }
]
const { data: plan } = await authClient.formations.planFormation({
body: { project_id: PROJECT_ID, template },
});
for (const change of plan) {
console.log(
`${change.action.padEnd(8)} ${change.logical_id} (${change.type})`
);
}
curl -s -X POST "$SOAT_URL/api/v1/formations/plan" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"project_id\": \"$PROJECT_ID\", \"template\": $TEMPLATE}" | jq '.'
Step 6 — Deploy the formation
All 14 resources are provisioned in dependency order; outputs surfaces the orchestrator and poem document IDs (Formations).
- CLI
- SDK
- curl
FORMATION=$(soat create-formation \
--project_id "$PROJECT_ID" \
--name "sonnet-workshop" \
--template "$TEMPLATE")
FORMATION_ID=$(printf '%s' "$FORMATION" | jq -r '.id')
ORCHESTRATOR_ID=$(printf '%s' "$FORMATION" | jq -r '.outputs.orchestrator_id')
POEM_DOC_ID=$(printf '%s' "$FORMATION" | jq -r '.outputs.poem_doc_id')
echo "FORMATION_ID: $FORMATION_ID"
echo "ORCHESTRATOR_ID: $ORCHESTRATOR_ID"
echo "POEM_DOC_ID: $POEM_DOC_ID"
const { data: formation } = await authClient.formations.createFormation({
body: {
project_id: PROJECT_ID,
name: 'sonnet-workshop',
template,
},
});
const FORMATION_ID = formation.id;
const ORCHESTRATOR_ID = formation.outputs?.orchestrator_id as string;
const POEM_DOC_ID = formation.outputs?.poem_doc_id as string;
console.log('Formation:', FORMATION_ID);
console.log('Orchestrator:', ORCHESTRATOR_ID);
console.log('Poem doc:', POEM_DOC_ID);
FORMATION=$(curl -s -X POST "$SOAT_URL/api/v1/formations" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"project_id\": \"$PROJECT_ID\", \"name\": \"sonnet-workshop\", \"template\": $TEMPLATE}")
FORMATION_ID=$(printf '%s' "$FORMATION" | jq -r '.id')
ORCHESTRATOR_ID=$(printf '%s' "$FORMATION" | jq -r '.outputs.orchestrator_id')
POEM_DOC_ID=$(printf '%s' "$FORMATION" | jq -r '.outputs.poem_doc_id')
echo "FORMATION_ID: $FORMATION_ID"
echo "ORCHESTRATOR_ID: $ORCHESTRATOR_ID"
echo "POEM_DOC_ID: $POEM_DOC_ID"
The formation's resources map, keyed by logical ID, holds each physical resource ID:
- CLI
- SDK
- curl
soat get-formation --formation_id "$FORMATION_ID" | jq '{id, name, status, outputs}'
const { data: f } = await authClient.formations.getFormation({
path: { formation_id: FORMATION_ID },
});
console.log(
JSON.stringify(
{ id: f.id, name: f.name, status: f.status, outputs: f.outputs },
null,
2
)
);
curl -s "$SOAT_URL/api/v1/formations/$FORMATION_ID" \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq '{id, name, status, outputs}'
Step 7 — Run the orchestrator
The orchestrator calls each stanza agent in order via its fixed tools (Agents — Generation).
- CLI
- SDK
- curl
RESULT=$(soat create-agent-generation --wait true \
--agent-id "$ORCHESTRATOR_ID" \
--messages '[{"role":"user","content":"Write a sonnet about the theme: artificial intelligence"}]')
printf '%s\n' "$RESULT" | jq '{status, trace_id}'
TRACE_ID=$(printf '%s\n' "$RESULT" | jq -r '.trace_id')
Expected output:
{
"status": "completed",
"trace_id": "trace_xxxxxxxxxxxx"
}
const { data: generation } = await authClient.agents.createAgentGeneration({
path: { agent_id: ORCHESTRATOR_ID },
query: { wait: true },
body: {
messages: [
{
role: 'user',
content: 'Write a sonnet about the theme: artificial intelligence',
},
],
},
});
const TRACE_ID = generation.trace_id;
console.log('Status:', generation.status);
console.log('Trace:', TRACE_ID);
RESULT=$(curl -s -X POST "$SOAT_URL/api/v1/agents/$ORCHESTRATOR_ID/generate?wait=true" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"Write a sonnet about the theme: artificial intelligence"}]}')
printf '%s\n' "$RESULT" | jq '{status, trace_id}'
TRACE_ID=$(printf '%s\n' "$RESULT" | jq -r '.trace_id')
Step 8 — Read the poem document
The stanza agents accumulated the sonnet in the shared document.
- CLI
- SDK
- curl
soat get-document --document-id "$POEM_DOC_ID" | jq -r '.content'
Expected output — a complete Shakespearean sonnet:
Silicon Dreams
In circuits bright where human thought takes form,
A mind emerges from the data's flow,
It learns through storms and weathers every storm,
And seeds of knowledge in its memory grow.
With second quatrain lines that build and rise,
Each layer deep connects what came before,
It reads the world through countless digital eyes,
And writes new knowledge, always seeking more.
Now in the third quatrain, patterns found
In vast arrays of numbers, text and light,
The third quatrain concludes with solid ground,
Where artificial minds approach their height.
And in this final couplet two lines close,
Where silicon and thought in union flows.
const { data: doc } = await authClient.documents.getDocument({
path: { document_id: POEM_DOC_ID },
});
console.log(doc.content);
curl -s "$SOAT_URL/api/v1/documents/$POEM_DOC_ID" \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq -r '.content'
Step 9 — Inspect the trace tree
/tree returns the execution tree rooted at the orchestrator trace; each node is a trace whose children are the traces spawned by sub-agent tool calls.
- CLI
- SDK
- curl
soat get-trace-tree --trace-id "$TRACE_ID" | jq '{id, step_count, children_count: (.children | length)}'
Expected output — the orchestrator at the root with 4 stanza workers as children:
{
"id": "trace_xxxxxxxxxxxx",
"step_count": 5,
"children_count": 4
}
Every agent that ran, as a flat list:
soat list-traces --project-id "$PROJECT_ID" | jq '.data[] | {id, agent_id, step_count, parent_trace_id}'
The orchestrator trace has parent_trace_id: null; each stanza worker trace references the orchestrator's trace ID as its parent.
const { data: tree } = await authClient.traces.getTraceTree({
path: { trace_id: TRACE_ID },
});
console.log('Orchestrator steps:', tree.step_count);
console.log('Nested agent traces:', tree.children?.length ?? 0);
curl -s "$SOAT_URL/api/v1/traces/$TRACE_ID/tree" \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq '{id, step_count, children_count: (.children | length)}'
Step 10 — Update the formation
A modified template is diffed against current state and only the required changes apply; here, the orchestrator's instructions (Formations).
- CLI
- SDK
- curl
UPDATED_TEMPLATE=$(printf '%s' "$TEMPLATE" | jq \
'.resources.orchestrator.properties.instructions = "Call tools in this exact order: call-stanza-1, call-stanza-2, call-stanza-3, call-stanza-4, then read-final-poem. Do not ask follow-up questions. Return ONLY the poem text. Focus on vivid imagery."')
soat update-formation \
--formation_id "$FORMATION_ID" \
--template "$UPDATED_TEMPLATE" | jq '{id, status}'
const updatedTemplate = JSON.parse(JSON.stringify(template));
updatedTemplate.resources.orchestrator.properties.instructions =
'Call tools in this exact order: call-stanza-1, call-stanza-2, call-stanza-3, call-stanza-4, then read-final-poem. Do not ask follow-up questions. Return ONLY the poem text. Focus on vivid imagery.';
const { data: updated } = await authClient.formations.updateFormation({
path: { formation_id: FORMATION_ID },
body: { template: updatedTemplate },
});
console.log('Status:', updated.status);
UPDATED_TEMPLATE=$(printf '%s' "$TEMPLATE" | jq \
'.resources.orchestrator.properties.instructions = "Call tools in this exact order: call-stanza-1, call-stanza-2, call-stanza-3, call-stanza-4, then read-final-poem. Do not ask follow-up questions. Return ONLY the poem text. Focus on vivid imagery."')
curl -s -X PUT "$SOAT_URL/api/v1/formations/$FORMATION_ID" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"template\": $UPDATED_TEMPLATE}" | jq '{id, status}'
Step 11 — View operation events
Each deployment and update appends events recording which resources were created, updated, or deleted, in order (Formations).
- CLI
- SDK
- curl
soat list-formation-events --formation_id "$FORMATION_ID" | jq '.data[] | {operation_type, status}'
Expected output — one entry per deployment operation:
{ "operation_type": "create", "status": "succeeded" }
{ "operation_type": "update", "status": "succeeded" }
const { data: events } = await authClient.formations.listFormationEvents({
path: { formation_id: FORMATION_ID },
});
for (const op of events ?? []) {
console.log(`${op.operation_type} — ${op.status}`);
}
curl -s "$SOAT_URL/api/v1/formations/$FORMATION_ID/events" \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq '.[] | {operation_type, status}'
Step 12 — Delete the formation
Deletion removes managed resources in reverse dependency order but never deletes resources the platform guards. The four agents now have generation history, so the delete fails with 409 FORMATION_DELETE_FAILED, names every blocker in error.meta.failures, and leaves the formation in delete_failed (Formations — Resource Lifecycle).
- CLI
- SDK
- curl
# → expect-fail
soat delete-formation --formation_id "$FORMATION_ID"
# The stack is left in delete_failed, with everything up to the agents removed.
soat get-formation --formation_id "$FORMATION_ID" | jq '{id, status}'
try {
await authClient.formations.deleteFormation({
path: { formation_id: FORMATION_ID },
});
} catch (error) {
// 409 FORMATION_DELETE_FAILED — every blocker is named in meta.failures.
console.log('blocked by:', error);
}
const { data: remaining } = await authClient.formations.getFormation({
path: { formation_id: FORMATION_ID },
});
console.log('Formation status:', remaining?.status); // 'delete_failed'
curl -s -X DELETE "$SOAT_URL/api/v1/formations/$FORMATION_ID" \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq '.error.meta.failures'
curl -s "$SOAT_URL/api/v1/formations/$FORMATION_ID" \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq '{id, status}'
Resolve the blockers and delete again: for an agent, discard its history explicitly with
soat delete-agent --agent-id "$AGENT_ID" --force true. Declare deletion_policy: retain
if the stack should leave the agent standing.
Next Steps
- Formations — dependency resolution, updates, and teardown details
- Multi-Agent Sonnet with Nested Agent Calls — the same pipeline built step by step