Skip to main content

Create an Agent Squad

An agent squad is a team of agents plus the flow that coordinates them, deployed as one Formation stack (see the Agent Squad example). This tutorial builds a marketing content squad: a researcher gathers facts, a writer and a reviewer work in parallel, a human approves, then it "publishes". You declare it in one template, deploy it, run it through approval, add a member, and tear it down.

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, and orchestrations before diving in.
  • CLI installed and configured, or SDK set up. See CLI or SDK.
  • For production hardening (secrets, env vars), see Configuration.
  • Ollama running locally with qwen2.5:0.5b pulled (ollama pull qwen2.5:0.5b).
  • Server is at http://localhost:5047.
export SOAT_BASE_URL=http://localhost:5047

Step 1 — Log in as admin​

Admin is the built-in superuser. See Users.

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

Step 2 — Create a project​

All resources are scoped to a project.

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

Step 3 — Design the squad​

The squad has three agents plus a human checkpoint, wired together by one orchestration:

NodeTypeRole
researchagentGathers 3 short bullet facts about the topic
writeagentDrafts a two-sentence marketing blurb from the research (runs in parallel with review)
reviewagentWrites one sentence of style guidance from the same research (runs in parallel with write)
approvehumanPauses the run so a person can approve the draft before it "publishes"
publishtransformCombines the approved draft into the final artifact

research fans out to write and review, which fan back in at approve through an activation group: approve activates once both finish. An orchestration is a formation resource type, so agents and orchestration deploy together in the next step.


Step 4 — Write the formation template​

A formation template is a JSON object with a resources map and an outputs map. ContentSquad's nodes reference Researcher, Writer, and Reviewer with { "ref": "LogicalId" }; SOAT resolves each to its physical agent_... ID in dependency order. See Ref Expressions.

This tutorial uses a local Ollama provider. To connect xAI, OpenAI, Anthropic, or Amazon Bedrock instead, see Connect Third-Party LLMs.

cat > squad.json << 'EOF'
{
"resources": {
"Provider": {
"type": "ai_provider",
"properties": {
"name": "Squad Ollama",
"provider": "ollama",
"default_model": "qwen2.5:0.5b"
}
},
"Researcher": {
"type": "agent",
"properties": {
"name": "Researcher",
"ai_provider_id": { "ref": "Provider" },
"instructions": "You are a research assistant. Given a topic, reply with exactly 3 short bullet facts. Never ask follow-up questions.",
"max_steps": 3
}
},
"Writer": {
"type": "agent",
"properties": {
"name": "Writer",
"ai_provider_id": { "ref": "Provider" },
"instructions": "You are a marketing writer. Given research notes, write a two-sentence marketing blurb. Never ask follow-up questions.",
"max_steps": 3
}
},
"Reviewer": {
"type": "agent",
"properties": {
"name": "Reviewer",
"ai_provider_id": { "ref": "Provider" },
"instructions": "You are an editor. Given research notes, write one sentence of style guidance for the writer. Never ask follow-up questions.",
"max_steps": 3
}
},
"ContentSquad": {
"type": "orchestration",
"properties": {
"name": "marketing-content-squad",
"input_schema": {
"type": "object",
"properties": { "topic": { "type": "string" } },
"required": ["topic"]
},
"nodes": [
{
"id": "research",
"type": "agent",
"agent_id": { "ref": "Researcher" },
"input_mapping": { "prompt": { "var": "input.topic" } },
"state_mapping":{"state.research":{"var":"output.content"}}
},
{
"id": "write",
"type": "agent",
"agent_id": { "ref": "Writer" },
"input_mapping": { "prompt": { "var": "research" } },
"state_mapping":{"state.draft":{"var":"output.content"}}
},
{
"id": "review",
"type": "agent",
"agent_id": { "ref": "Reviewer" },
"input_mapping": { "prompt": { "var": "research" } },
"state_mapping":{"state.notes":{"var":"output.content"}}
},
{
"id": "approve",
"type": "human",
"prompt": "Approve the draft for publishing?",
"options": ["approve", "reject"],
"input_mapping": { "draft": { "var": "draft" }, "notes": { "var": "notes" } },
"state_mapping":{"state.approved":{"var":"output.approved"}}
},
{
"id": "publish",
"type": "transform",
"expression": { "cat": ["Published: ", { "var": "draft" }] },
"state_mapping":{"state.published":{"var":"output.result"}}
}
],
"edges": [
{ "from": "research", "to": "write" },
{ "from": "research", "to": "review" },
{ "from": "write", "to": "approve", "activation_group": "join", "activation_condition": "all" },
{ "from": "review", "to": "approve", "activation_group": "join", "activation_condition": "all" },
{ "from": "approve", "to": "publish" }
]
}
}
},
"outputs": {
"squad_id": { "ref": "ContentSquad" }
}
}
EOF
TEMPLATE=$(cat squad.json)

Step 5 — Validate and preview the template​

Validate the template's structure, then preview what will be created. See Formations — Key Concepts.

soat validate-formation --template "$TEMPLATE"

Expected output:

{ "valid": true }
soat plan-formation --project-id "$PROJECT_ID" --template "$TEMPLATE" | jq '.'

Expected output — 5 resources all marked as create:

[
{ "action": "create", "logical_id": "Provider", "type": "ai_provider" },
{ "action": "create", "logical_id": "Researcher", "type": "agent" },
{ "action": "create", "logical_id": "Writer", "type": "agent" },
{ "action": "create", "logical_id": "Reviewer", "type": "agent" },
{ "action": "create", "logical_id": "ContentSquad", "type": "orchestration" }
]

Step 6 — Deploy the squad​

SOAT provisions the provider, the three agents, and the orchestration in dependency order, resolving every ref. See Formations — Examples.

FORMATION=$(soat create-formation \
--project-id "$PROJECT_ID" \
--name "content-squad" \
--template "$TEMPLATE")

FORMATION_ID=$(printf '%s' "$FORMATION" | jq -r '.id')
SQUAD_ID=$(printf '%s' "$FORMATION" | jq -r '.outputs.squad_id')

echo "FORMATION_ID: $FORMATION_ID"
echo "SQUAD_ID: $SQUAD_ID"

Step 7 — Run the squad​

Start a run against the squad_id output, passing { "topic": "..." } as input. research runs first, then write and review run in parallel, then the run pauses at the approve human node.

RUN=$(soat start-orchestration-run \
--orchestration-id "$SQUAD_ID" \
--input '{"topic": "the launch of a new project management app"}' \
--wait)

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

Expected output — the run pauses at approve:

{
"status": "awaiting_input",
"required_action": {
"type": "human_input",
"node_id": "approve",
"prompt": "Approve the draft for publishing?",
"options": ["approve", "reject"],
"context": { "draft": "...", "notes": "..." }
}
}

Step 8 — Resume the human approval node​

Submit the human reviewer's decision with submit-human-input, naming the paused node_id. The run resumes and runs publish.

RESULT=$(soat submit-human-input \
--orchestration-run-id "$RUN_ID" \
--node-id "approve" \
--output '{"approved": true}')

printf '%s\n' "$RESULT" | jq '{status, state}'

Expected output:

{
"status": "completed",
"state": {
"research": "...",
"draft": "...",
"notes": "...",
"approved": true,
"published": "Published: ..."
}
}

Inspect the per-node executions to see who did what:

soat list-orchestration-run-node-executions --orchestration-run-id "$RUN_ID" | jq '.data[] | {node_id, node_type, status}'

Step 9 — Add a squad member and redeploy​

Add a Proofreader agent running in parallel with write and review, feeding the same approve fan-in. Update resources and the approve node's edges, then redeploy; SOAT diffs the template against the stack and applies only the changes. See Formations — Update a formation.

UPDATED_TEMPLATE=$(printf '%s' "$TEMPLATE" | jq '
.resources.Proofreader = {
"type": "agent",
"properties": {
"name": "Proofreader",
"ai_provider_id": { "ref": "Provider" },
"instructions": "You are a proofreader. Given research notes, list any factual claims that need a citation. Never ask follow-up questions.",
"max_steps": 3
}
} |
.resources.ContentSquad.properties.nodes += [{
"id": "proofread",
"type": "agent",
"agent_id": { "ref": "Proofreader" },
"input_mapping": { "prompt": { "var": "research" } },
"state_mapping":{"state.citationNotes":{"var":"output.content"}}
}] |
.resources.ContentSquad.properties.edges += [
{ "from": "research", "to": "proofread" },
{ "from": "proofread", "to": "approve", "activation_group": "join", "activation_condition": "all" }
]
')

soat plan-formation --formation-id "$FORMATION_ID" --template "$UPDATED_TEMPLATE" | jq '.'

soat update-formation \
--formation-id "$FORMATION_ID" \
--template "$UPDATED_TEMPLATE" | jq '{id, status}'

Expected plan — one new resource, one updated resource:

[
{ "action": "create", "logical_id": "Proofreader", "type": "agent" },
{ "action": "update", "logical_id": "ContentSquad", "type": "orchestration" }
]

Step 10 — Tear down​

Deleting a formation removes managed resources in reverse dependency order, except resources the platform guards. Each agent now has generation history, so the delete fails 409 FORMATION_DELETE_FAILED with the blocking agents in error.meta.failures and the stack stays delete_failed. See Formations — Resource Lifecycle.

# → expect-fail
soat delete-formation --formation-id "$FORMATION_ID"

soat get-formation --formation-id "$FORMATION_ID" | jq '{id, status}'

To finish, discard each agent's history explicitly (soat delete-agent --agent-id "$AGENT_ID" --force true) and delete the formation again, or declare the agents with deletion_policy: retain.


Next steps​