You've built single agents with tools, state, and hooks. Now the question becomes: how do you compose multiple agents into a system? Strands provides three orchestration patterns — each with different tradeoffs between determinism and flexibility. This is where your interest in controlled, repeatable pipelines meets the need for LLM-driven branching.
Every multi-agent system answers one question differently: who decides what runs next?
Developer defines nodes + edges. The LLM decides which edge to follow at each node. Cycles allowed. Good for conditional branching where you control the topology but not the path.
Pool of agents with handoff tools. Agents autonomously decide which peer to delegate to. Emergent path. Good for exploration and creative problem-solving.
DAG of tasks with fixed execution order. Independent tasks run in parallel. Fully deterministic — same input always produces the same execution path. Good for repeatable processes.
The core insight: these patterns sit on a spectrum from most deterministic (Workflow) to most autonomous (Swarm), with Graph in the middle giving you structural control with runtime flexibility.
A Graph is a set of agent nodes connected by directed edges. You define the topology — which agents can talk to each other. The LLM at each node decides whether to follow an edge or stop. Cycles are allowed, so a reviewer can send work back to a writer.
import { Agent } from '@strands-agents/sdk'
import { Graph } from '@strands-agents/sdk/multi-agent'
// Each node is a standalone agent
const researcher = new Agent({
name: 'researcher',
systemPrompt: 'Research the given topic. Provide facts and sources.',
model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
})
const writer = new Agent({
name: 'writer',
systemPrompt: 'Write a clear, concise article from the research provided.',
model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
})
// Define topology: researcher feeds into writer
const graph = new Graph({
nodes: [researcher, writer],
edges: [['researcher', 'writer']],
})
const result = await graph.invoke('Research and write about WebAssembly')
The graph engine passes the output of each node as input context to the next. The LLM at the researcher node produces research, which becomes part of the writer node's conversation.
Graphs support cycles — a reviewer can reject and send work back to the writer:
const reviewer = new Agent({
name: 'reviewer',
systemPrompt: `Review the article for accuracy and clarity.
If it meets standards, approve it.
If not, send it back to the writer with specific feedback.`,
model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
})
const graph = new Graph({
nodes: [researcher, writer, reviewer],
edges: [
['researcher', 'writer'],
['writer', 'reviewer'],
['reviewer', 'writer'], // Cycle: reviewer can send back to writer
],
maxCycles: 3, // Prevent infinite loops
})
Always set maxCycles on graphs with cycles. Without it, a pedantic reviewer and a stubborn writer can loop indefinitely, burning tokens. Start with maxCycles: 3 and increase only if your use case requires it.
A Swarm gives each agent a handoff tool — the ability to transfer control to another agent in the pool. No edges defined by you. The agents decide among themselves who should handle what.
import { Swarm } from '@strands-agents/sdk/multi-agent'
const analyst = new Agent({
name: 'analyst',
systemPrompt: `You analyze data and identify patterns.
If you need creative writing, hand off to the copywriter.
If you need technical details, hand off to the engineer.`,
model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
})
const copywriter = new Agent({
name: 'copywriter',
systemPrompt: `You write compelling copy from analysis.
If you need more data, hand off back to the analyst.`,
model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
})
const engineer = new Agent({
name: 'engineer',
systemPrompt: `You provide technical implementation details.
If you need analysis of requirements, hand off to the analyst.`,
model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
})
const swarm = new Swarm({
agents: [analyst, copywriter, engineer],
entryAgent: 'analyst', // Who receives the initial input
maxHandoffs: 5, // Safety limit
})
const result = await swarm.invoke('Create a product brief for our new API')
The swarm automatically injects handoff tools into each agent. When the analyst decides it needs creative writing, it calls the handoff tool targeting the copywriter. The copywriter receives the conversation context and continues.
Swarms are powerful for exploration but non-deterministic by nature. The same input can produce different handoff sequences on different runs. Use them for creative/exploratory tasks, not for repeatable business processes. Always set maxHandoffs to prevent runaway delegation.
A Workflow defines a fixed execution order. You specify which tasks depend on which — the engine resolves the DAG, runs independent tasks in parallel, and executes dependent tasks in order. Same input, same execution path, every time.
import { Workflow } from '@strands-agents/sdk/multi-agent'
const researchTask = new Agent({
name: 'research',
systemPrompt: 'Research the given topic thoroughly.',
model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
})
const outlineTask = new Agent({
name: 'outline',
systemPrompt: 'Create a structured outline from the research.',
model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
})
const writeTask = new Agent({
name: 'write',
systemPrompt: 'Write the full article following the outline.',
model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
})
const formatTask = new Agent({
name: 'format',
systemPrompt: 'Format the article as clean markdown with headers.',
model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
})
const workflow = new Workflow({
tasks: [
{ agent: researchTask, dependsOn: [] },
{ agent: outlineTask, dependsOn: ['research'] },
{ agent: writeTask, dependsOn: ['outline'] },
{ agent: formatTask, dependsOn: ['write'] },
],
})
// Execution is deterministic: research → outline → write → format
const result = await workflow.invoke('Write about edge computing')
When tasks have no dependency relationship, the workflow engine runs them concurrently:
const workflow = new Workflow({
tasks: [
{ agent: researchAgent, dependsOn: [] },
{ agent: competitorAgent, dependsOn: [] }, // Runs parallel with research
{ agent: synthesizerAgent, dependsOn: ['research', 'competitor'] }, // Waits for both
],
})
This is the pattern for consulting deliverables: research and competitive analysis happen in parallel, synthesis waits for both to complete. The execution graph is fixed — you get repeatability and parallelism without sacrificing control.
All three patterns share a common state mechanism. When you invoke a multi-agent system, an invocationState object flows through every node. This is how agents pass structured data — not just conversation text — to each other.
import { Graph, InvocationState } from '@strands-agents/sdk/multi-agent'
// Create shared state accessible to all nodes
const state: InvocationState = {
results: new Map(), // Each node's output, keyed by node name
steps: [], // Ordered list of which nodes have run
app: new StateStore(), // Your custom application state
}
const graph = new Graph({
nodes: [researcher, writer, reviewer],
edges: [
['researcher', 'writer'],
['writer', 'reviewer'],
],
invocationState: state,
})
Inside a tool or hook, access the multi-agent state to make decisions:
// In a hook: check what previous nodes produced
const writerOutput = state.results.get('writer')
const researchLength = state.results.get('researcher')?.length
The MultiAgentState structure provides:
| Field | Type | Purpose |
|---|---|---|
results | Map<string, string> | Output of each completed node, keyed by name |
nodes | string[] | List of all node names in the system |
steps | string[] | Ordered execution trace — which nodes ran and in what order |
app | StateStore | Your custom application state (counters, flags, accumulated data) |
You already know agent-level hooks from Lesson 3. Multi-agent systems add orchestration-level hooks that fire at node boundaries — letting you observe, modify, or cancel node execution.
Fires before each node agent is invoked. You can inspect the input, modify it, or cancel the node entirely:
import { Graph, BeforeNodeCallEvent } from '@strands-agents/sdk/multi-agent'
const graph = new Graph({
nodes: [researcher, writer, reviewer],
edges: [
['researcher', 'writer'],
['writer', 'reviewer'],
['reviewer', 'writer'],
],
hooks: {
onBeforeNodeCall: (event: BeforeNodeCallEvent) => {
console.log(`About to call node: ${event.nodeName}`)
console.log(`Input: ${event.input.slice(0, 100)}...`)
// Conditionally skip a node
if (event.nodeName === 'reviewer' && someCondition) {
event.cancel() // Skip this node, proceed to next edge or end
}
},
},
})
Fires after a node completes. Inspect outputs, update custom state, log metrics:
hooks: {
onAfterNodeCall: (event: AfterNodeCallEvent) => {
console.log(`Node ${event.nodeName} completed`)
console.log(`Output length: ${event.result.length} chars`)
console.log(`Stop reason: ${event.stopReason}`)
// Track execution in custom state
state.app.set(`${event.nodeName}_completedAt`, Date.now())
},
}
Fires when an agent in a Swarm hands off to another. Observe the delegation chain:
const swarm = new Swarm({
agents: [analyst, copywriter, engineer],
entryAgent: 'analyst',
hooks: {
onHandoff: (event: MultiAgentHandoffEvent) => {
console.log(`${event.fromAgent} → ${event.toAgent}`)
console.log(`Reason: ${event.reason}`)
},
},
})
Fires as tokens stream from a node's inner agent. Use this for real-time progress updates in a UI:
hooks: {
onNodeStreamUpdate: (event: NodeStreamUpdateEvent) => {
process.stdout.write(event.chunk) // Stream to terminal
// Or emit to a WebSocket for live UI updates
},
}
Let's build the full pattern: a content creation graph where the reviewer is skipped if the writer's output is short (implying a simple, low-risk response that doesn't need review).
import { Agent } from '@strands-agents/sdk'
import {
Graph,
BeforeNodeCallEvent,
AfterNodeCallEvent,
InvocationState,
StateStore,
} from '@strands-agents/sdk/multi-agent'
// --- Agents ---
const researcher = new Agent({
name: 'researcher',
systemPrompt: `Research the given topic. Provide 3-5 key facts with sources.
Be thorough but concise.`,
model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
})
const writer = new Agent({
name: 'writer',
systemPrompt: `Write a clear article from the research provided.
Target 200-500 words. Use simple language.`,
model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
})
const reviewer = new Agent({
name: 'reviewer',
systemPrompt: `Review the article for accuracy and clarity.
Provide a final approved version with any corrections.`,
model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
})
// --- Shared state ---
const state: InvocationState = {
results: new Map(),
steps: [],
nodes: ['researcher', 'writer', 'reviewer'],
app: new StateStore(),
}
// --- Graph with conditional hook ---
const REVIEW_THRESHOLD = 200 // Skip review for short outputs
const contentPipeline = new Graph({
nodes: [researcher, writer, reviewer],
edges: [
['researcher', 'writer'],
['writer', 'reviewer'],
],
invocationState: state,
hooks: {
onAfterNodeCall: (event: AfterNodeCallEvent) => {
// Store output length for the conditional check
if (event.nodeName === 'writer') {
state.app.set('writerOutputLength', event.result.length)
}
},
onBeforeNodeCall: (event: BeforeNodeCallEvent) => {
// Skip reviewer if writer output is short (low complexity)
if (event.nodeName === 'reviewer') {
const writerLength = state.app.get('writerOutputLength') ?? 0
if (writerLength < REVIEW_THRESHOLD) {
console.log(
`Skipping reviewer — writer output (${writerLength} chars) ` +
`below threshold (${REVIEW_THRESHOLD})`
)
event.cancel()
}
}
},
},
})
// --- Run it ---
const result = await contentPipeline.invoke(
'Explain what a semaphore is in operating systems'
)
console.log('Execution trace:', state.steps)
console.log('Final output:', result.message?.content)
This pattern gives you structural determinism (researcher → writer → reviewer) with runtime flexibility (skip the reviewer when it's not needed). The hook inspects shared state to make the decision — no LLM involved in the skip logic.
This conditional-skip pattern is common in consulting workflows: skip QA for trivial changes, skip legal review for low-risk content, skip manager approval below a dollar threshold. The hook makes the decision based on structured data, not model judgment.
When designing a multi-agent system, start with the determinism requirement:
| Need | Pattern | Why |
|---|---|---|
| Fixed process, parallel tasks, repeatable | Workflow | DAG guarantees execution order. Same input → same path. Independent tasks parallelize automatically. |
| Conditional branching, cycles, review loops | Graph | You control the topology (what's possible). The LLM controls the path (what actually happens). Cycles enable feedback loops. |
| Exploratory, creative, unknown path | Swarm | Agents self-organize. The system discovers the right sequence through handoffs. Best when you can't predict the workflow in advance. |
These patterns compose. A Workflow step can itself be a Graph. A Graph node can be a single agent with complex tools. Use the simplest pattern that satisfies your requirements at each level:
// A workflow where one step is itself a graph
const researchGraph = new Graph({
nodes: [webSearcher, factChecker],
edges: [['webSearcher', 'factChecker'], ['factChecker', 'webSearcher']],
maxCycles: 2,
})
const pipeline = new Workflow({
tasks: [
{ agent: researchGraph, dependsOn: [] },
{ agent: writerAgent, dependsOn: ['research'] },
{ agent: formatterAgent, dependsOn: ['write'] },
],
})
Start with Workflow. Only reach for Graph when you need cycles or conditional edges. Only reach for Swarm when you genuinely don't know the path upfront. Each step up the autonomy spectrum is harder to debug and predict.
InvocationState) flows through all nodes in every pattern. Use it for structured data passing and hook decisions.BeforeNodeCallEvent, AfterNodeCallEvent) give you programmatic control without LLM involvement — conditional execution, logging, state updates.maxCycles for Graphs, maxHandoffs for Swarms, limits.turns on each inner agent.BeforeNodeCallEvent) instead of putting the skip logic in the reviewer's system prompt?maxHandoffs has been reached?Multi-Agent Patterns — Strands Agents Docs — Official documentation on Graph, Swarm, and Workflow patterns with additional examples and API reference. ~12 min read.