Skip to main content

Create an Agent Squad

An agent squad is a team of agents plus the flow that coordinates them, deployed as a single Formation stack — see the Agent Squad example. This tutorial builds a small marketing content squad end to end: a researcher gathers facts, a writer and a reviewer work in parallel from those facts, a human reviewer approves the result, and only then does it "publish".

You will:

  1. Design the squad: member roles, instructions, and the flow that connects them.
  2. Write a formation template declaring the agents and the squad orchestration in one document, using { "ref": ... } to cross-reference resources declared in the same template.
  3. Validate the template and preview the deployment plan.
  4. Deploy the stack and read the squad_id output.
  5. Run the squad and inspect which node is waiting on you.
  6. Resume the human approval node and read the published result.
  7. Add a member to the squad and redeploy.
  8. Tear down the stack.

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 Advanced 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 for full authentication details.

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 using an activation groupapprove only activates once both finish. This is the Agent Squad pattern: because an orchestration is a formation resource type, the three agents and the orchestration that coordinates them are declared and deployed 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 before creating the orchestration, in dependency order. See Ref Expressions.

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.

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": "topic" } },
"output_mapping": { "content": "state.research" }
},
{
"id": "write",
"type": "agent",
"agent_id": { "ref": "Writer" },
"input_mapping": { "prompt": { "var": "research" } },
"output_mapping": { "content": "state.draft" }
},
{
"id": "review",
"type": "agent",
"agent_id": { "ref": "Reviewer" },
"input_mapping": { "prompt": { "var": "research" } },
"output_mapping": { "content": "state.notes" }
},
{
"id": "approve",
"type": "human",
"prompt": "Approve the draft for publishing?",
"options": ["approve", "reject"],
"input_mapping": { "draft": { "var": "draft" }, "notes": { "var": "notes" } },
"output_mapping": { "approved": "state.approved" }
},
{
"id": "publish",
"type": "transform",
"expression": { "cat": ["Published: ", { "var": "draft" }] },
"output_mapping": { "result": "state.published" }
}
],
"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

Deploy the formation. SOAT provisions the provider, all three agents, and the orchestration in dependency order, and resolves every ref expression. 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 \
--run-id "$RUN_ID" \
--node-id "approve" \
--output '{"approved": true}')

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

Expected output:

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

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

soat get-orchestration-run --run-id "$RUN_ID" | jq '.node_executions[] | {node_id, node_type, status}'

Step 9 — Add a squad member and redeploy

Add a Proofreader agent that also runs in parallel with write and review, feeding the same approve fan-in. Update the template's resources and the approve node's edges, then redeploy — SOAT diffs the new template against the current stack and applies only the required 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" } },
"output_mapping": { "content": "state.citationNotes" }
}] |
.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. See Formations — Key Concepts.

soat delete-formation --formation-id "$FORMATION_ID" | jq '.'

Summary

ConceptWhat you did
Agent squadDeclared a team of 3 agents plus the coordinating orchestration in a single template
{ "ref": ... }Wired ai_provider_id and agent_id across resources in the same template
Fan-out / fan-inresearch fanned out to write and review; an activation_group fanned them back in
Human nodePaused the run at approve and resumed it with submit-human-input
Validate and planChecked the template and previewed create/update actions before deploying
DeployCreated the provider, agents, and orchestration in one call
UpdateAdded a Proofreader agent and a new fan-in edge; SOAT applied only the diff
DeleteRemoved all managed resources in reverse dependency order