Declarative multi-agent orchestration for strands-agents — wire entire agent systems with YAML
Important
Community project — not affiliated with AWS or the strands-agents team. Bugs here? Open an issue. Bugs in the underlying SDK? Head to strands-agents.
Think Docker Compose, but for AI agents
Strands is a powerful agent SDK. But once you have more than one agent, a few MCP servers, safety hooks, and shared models — you end up writing the same plumbing over and over. strands-compose kills that boilerplate.
You describe the shape of your agent system in YAML, and strands-compose resolves, validates, and starts everything — models, MCP servers & clients, hooks, tools, orchestration topology — as a live, fully wired multi-agent system.
Already working with strands? Guess what — you already know strands-compose. After load() resolves your YAML, what you get back are plain strands objects. Every agent is a strands.Agent. Every MCP client is a strands.tools.mcp.MCPClient. Every orchestrator is a strands.multiagent.Swarm or Graph or just strands.Agent. No wrappers, no subclasses, no magic. Just the real deal, fully wired and ready to go.
models:
default:
provider: bedrock
model_id: us.anthropic.claude-sonnet-4-6-v1:0
agents:
researcher:
model: default
system_prompt: "You research topics."
tools: [strands_tools:http_request]
writer:
model: default
system_prompt: "You write reports."
coordinator:
model: default
system_prompt: "Coordinate research and writing."
orchestrations:
team_leader:
mode: delegate
entry_name: coordinator
connections:
- agent: researcher
description: "Research a topic."
- agent: writer
description: "Write the report."
entry: team_leaderfrom strands_compose import load
resolved = load("config.yaml")
result = resolved.entry("Write a report about quantum computing.")
print(result)Three agents, orchestration wiring, model sharing — zero plumbing code.
Your entire agent network — models, prompts, tools, hooks, MCP servers, orchestration topology — captured in a single YAML file and maybe a few Python files for custom tools or hooks. That's it. That's your agent environment. Here's what that unlocks:
Push to Git. Tag it. Diff two versions and see exactly what changed — which prompt was tweaked, which model was swapped, which hook was added. Your agent system gets the same auditability as your infrastructure code. No more "I think someone changed the system prompt last Tuesday."
A folder of YAML configs — one per agent environment. production.yaml, staging.yaml, experiment-42.yaml. Each is a complete, self-contained snapshot of an agent system. Load any of them with load("experiment-42.yaml"). That's your agent environments registry — no platform needed.
Your entire config is data, so you can generate it. Build 20 variations — different models, different prompts, different tool combinations — and run them all in CI. With session persistence, every agent interaction is tracked. Point another strands-compose pipeline at those session logs to analyze results, compare quality, compute metrics. You're benchmarking agent systems with agent systems.
A bug report comes in. You have the exact YAML config, the session ID, the full conversation trace. Load it, replay it, debug it. No "works on my machine" — the config is the machine.
| Feature | What it does |
|---|---|
| YAML-first config | Models, agents, tools, hooks, MCP, orchestrations — all in one file |
| Full YAML power | Variables (${VAR:-default}), anchors (&ref / *ref), x- scratch pads, multi-file merge |
| Multi-model support | Bedrock, OpenAI, Ollama, Gemini — swap with one line |
| MCP servers & clients | Launch local servers from Python files, connect to remote HTTP endpoints, or spawn stdio subprocesses |
| MCP lifecycle management | Startup ordering, readiness polling, graceful shutdown — servers before clients, always |
| Orchestration modes | Delegate (agent-as-tool), Swarm (peer handoffs), Graph (DAG pipelines) — arbitrarily nestable |
| Event streaming | Unified async event queue across any orchestration depth — tokens, tool calls, handoffs, completions |
| Session persistence | File, S3, or Bedrock AgentCore Memory — agents remember across restarts |
| Custom agent factories | Plug your own Agent subclass or factory via type: |
| Deployment-agnostic | Pure core library — no HTTP server, no deployment opinions baked in |
When you call load("config.yaml"), strands-compose runs a deterministic pipeline:
YAML source(s)
│
├─ Parse & strip x-* anchors
├─ Interpolate ${VAR:-default} variables
├─ Sanitize collection keys
├─ Merge (if multi-file)
│
├─ Validate against Pydantic schema
│
├─ Resolve infrastructure (models, MCP servers/clients, session managers)
├─ Start MCP lifecycle (servers up → clients connect)
│
├─ Create agents (with tools, hooks, MCP clients attached)
├─ Wire orchestrations (delegate/swarm/graph, topological sort)
│
└─ Return ResolvedConfig — ready to call
Every step is explicit. Every error is caught early with a clear message. The pipeline is the same whether you load one file or merge five — load(["base.yaml", "agents.yaml", "mcp.yaml"]) just works.
strands-compose gives you Docker Compose-style variable interpolation plus full YAML anchor/alias support. DRY configs that adapt to any environment:
vars:
MODEL: ${MODEL:-us.anthropic.claude-sonnet-4-6-v1:0}
TONE: ${TONE:-friendly}
x-base: &base_prompt |
You are a ${TONE} assistant.
Keep answers clear and concise.
x-hooks: &safety_hooks
- type: strands_compose.hooks:MaxToolCallsGuard
params: { max_calls: 15 }
- type: strands_compose.hooks:ToolNameSanitizer
models:
default:
provider: bedrock
model_id: ${MODEL}
agents:
assistant:
model: default
system_prompt: *base_prompt
hooks: *safety_hooks
entry: assistantOverride at runtime: TONE=formal MODEL=us.anthropic.claude-sonnet-4-6-v1:0 python main.py
Split large configs across files — models in one, agents in another, MCP in a third — and merge them with load(["base.yaml", "agents.yaml"]). Each file interpolates its own vars: independently, collections merge, and duplicates are caught.
Install with uv:
uv add strands-compose # Bedrock (default)
uv add strands-compose[ollama] # + Ollama
uv add strands-compose[openai] # + OpenAI
uv add strands-compose[gemini] # + GeminiOr with pip:
pip install strands-compose # Bedrock (default)
pip install strands-compose[ollama] # + Ollama
pip install strands-compose[openai] # + OpenAI
pip install strands-compose[gemini] # + GeminiCreate a config.yaml:
models:
default:
provider: bedrock
model_id: us.anthropic.claude-sonnet-4-6-v1:0
agents:
assistant:
model: default
system_prompt: "You are a helpful assistant."
entry: assistantRun it:
from strands_compose import load
resolved = load("config.yaml")
with resolved.mcp_lifecycle:
result = resolved.entry("Hello!")
print(result)strands-compose ships a CLI to validate and debug configs without writing Python.
check — fast, static validation (YAML syntax, schema, variable interpolation, cross-references). No side-effects, safe for CI. Will not catch runtime issues like bad credentials, unreachable MCP servers, or missing Python modules.
strands-compose check config.yaml
strands-compose check base.yaml agents.yaml # merge multiple files
strands-compose check config.yaml --json # JSON output for scripts
strands-compose check config.yaml --quiet # exit code onlyload (recommended) — full end-to-end validation. Builds real Python objects, starts MCP servers, and probes connectivity. Catches everything check catches plus import errors, auth failures, and MCP health issues.
strands-compose load config.yaml
strands-compose load config.yaml --json
strands-compose load config.yaml --quietEvery example is a self-contained folder with a README.md, config.yaml, and main.py. Start from the top and work your way down — each one builds on concepts from the previous.
| # | Example | What it shows |
|---|---|---|
| 01 | Minimal | load() one-liner — the simplest possible agent |
| 02 | Vars & Anchors | ${VAR:-default} interpolation and YAML &anchor / *alias reuse |
| 03 | Tools | tools: — auto-load @tool functions from Python files |
| 04 | Session | session_manager: — persistent memory across turns and restarts |
| 05 | Hooks | hooks: — MaxToolCallsGuard, ToolNameSanitizer, and custom hooks |
| 06 | MCP | All three MCP modes: local server, remote URL, stdio subprocess |
| 07 | Delegate | mode: delegate — coordinator routes work to specialist agents |
| 08 | Swarm | mode: swarm — peer agents hand off to each other autonomously |
| 09 | Graph | mode: graph — deterministic DAG pipeline between agents |
| 10 | Nested | Nested orchestration — Swarm inside a Delegate |
| 11 | Multi-file | Split config across files — infra in one YAML, agents in another |
| 12 | Streaming | wire_event_queue() — stream every token, tool call, and handoff live |
| 13 | Graph conditions | Conditional edges — condition:, reset_on_revisit, max_node_executions |
| 14 | Agent factory | type: + agent_kwargs: — custom agent factory instead of Agent() |
# Run any example
uv run python examples/01_minimal/main.pystrands-compose supports 3 orchestration modes from strands. They can be nested arbitrarily — a delegate target can be a swarm, a graph node can be a delegate.
The coordinator calls sub-agents like tool functions. Best for hub-and-spoke patterns:
orchestrations:
team_leader:
mode: delegate
entry_name: coordinator # Agent declared in `agents:`
connections:
- agent: researcher
description: "Research the topic."
- agent: writer
description: "Write the report."Peer agents pass control to each other. No central coordinator — agents decide when to hand off:
orchestrations:
review_team:
mode: swarm
entry_name: drafter # Swarm entry - agent name
agents: [drafter, reviewer, tech_lead] # Agents declared in `agents:`
max_handoffs: 10Agents execute in dependency order. Independent nodes run in parallel. Supports conditional edges:
orchestrations:
blog:
mode: graph
entry_name: writer # Graph entry - agent name
edges: # We use agent names to define edges
- from: writer
to: reviewer
- from: reviewer
to: writer
condition: ./conditions.py:needs_revision
- from: reviewer
to: publisher
condition: ./conditions.py:is_approvedNamed orchestrations reference each other. A swarm becomes a delegate tool, a delegate becomes a graph node — compose them however you want:
orchestrations:
content_team: # This swarm is plugged in as a tool for the team_leader
mode: swarm
entry_name: researcher
agents: [researcher, writer, reviewer]
team_leader:
mode: delegate
entry_name: coordinator
connections:
- agent: content_team # Nested swarm as a delegate tool
description: "Content creation team."
- agent: qa_bot # Nested agent as a delegate tool
description: "Quality assurance."
entry: team_leaderstrands-compose topologically sorts all orchestrations, builds inner ones first, then wires them as tools or nodes for outer ones. Circular dependencies are caught at load time.
When you have a 3-level nested orchestration — a delegate calling a swarm that uses graph nodes — you still want to know exactly what's happening. Which agent is thinking? What tool just fired? When did a handoff occur?
EventPublisher is a strands HookProvider that captures every lifecycle event and publishes it to a shared async queue. The trick: wire_event_queue() attaches publishers to every agent in your entire system — no matter how deeply nested — so all events flow to one place.
import asyncio
from strands_compose import AnsiRenderer, load
async def main():
resolved = load("config.yaml")
queue = resolved.wire_event_queue()
async def invoke():
try:
await resolved.entry.invoke_async("Analyse LLM trends.")
finally:
await queue.close()
asyncio.create_task(invoke())
renderer = AnsiRenderer()
while (event := await queue.get()) is not None:
renderer.render(event)
renderer.flush()
asyncio.run(main())Event types: AGENT_START, TOKEN, REASONING, TOOL_START, TOOL_END, NODE_START, NODE_STOP, HANDOFF, COMPLETE, MULTIAGENT_START, MULTIAGENT_COMPLETE, ERROR — each carrying {type, agent_name, timestamp, data}. Enough for a real-time frontend, a log aggregator, or a debugging dashboard. The AnsiRenderer gives you coloured terminal output out of the box — agent names, tool calls, reasoning traces, all streaming live.
git clone https://github.com/strands-compose/sdk-python
cd sdk-python
uv run just install # install deps + wire git hooks (run once after clone)
uv run just check # lint + type check + security scan
uv run just test # pytest with coverage
uv run just format # auto-format (Ruff)Re-install hooks after a fresh clone or if hooks stop running:
uv run just install-hooks
See CONTRIBUTING.md for the full contribution guide and CHANGELOG.md for release history.