· 7 min read
Orchestration is the logic that determines which agents run, in what order, with what inputs, and how their outputs combine. It is the control plane of a multi-agent system. Individual agents may be well-prompted and well-tooled, but without effective orchestration, they cannot coordinate to solve complex problems.
The Claude Agent SDK provides building blocks — agents, tools, handoffs, and run configuration — but the orchestration pattern is yours to design. Different problem structures call for different orchestration approaches. A customer support system needs a router pattern. A document processing system needs a pipeline. A research system benefits from parallel workers with synthesis. Choosing the wrong pattern creates friction; choosing the right one makes the system feel natural.
This guide covers the five core orchestration patterns, when each applies, and how to implement them with the Claude Agent SDK. These patterns can also be composed — a pipeline stage might internally use parallel workers, or a router might dispatch to a hierarchical team.
You need explicit orchestration when:
For a single agent with tools, the SDK handles the tool-call loop automatically. Orchestration becomes your responsibility when you step beyond a single agent.
A single agent classifies input and dispatches to specialists. Best for variable input types that require different handling.
import { Agent } from "@anthropic-ai/agent-sdk";
const routerAgent = new Agent({
name: "request-router",
model: "claude-sonnet-4-20250514",
instructions: `Classify the incoming request and route to the appropriate handler.
ROUTING TABLE:
- Data questions (SQL, metrics, dashboards) -> data-analyst
- Code issues (bugs, reviews, refactoring) -> code-specialist
- Writing tasks (docs, emails, reports) -> writing-specialist
- General questions -> general-assistant
Route immediately. Do not attempt to answer the question yourself.`,
handoffs: [
{ agent: dataAnalyst, condition: "Data or analytics question" },
{ agent: codeSpecialist, condition: "Code-related task" },
{ agent: writingSpecialist, condition: "Writing or documentation task" },
{ agent: generalAssistant, condition: "General inquiry" },
],
});
Agents execute in a fixed order, each transforming the output of the previous stage. Best for workflows with well-defined stages.
interface PipelineStage {
agent: Agent;
name: string;
validateOutput: (output: string) => boolean;
}
async function runPipeline(
stages: PipelineStage[],
initialInput: string
): Promise<{ output: string; stageResults: Array<{ name: string; output: string; durationMs: number }> }> {
let currentInput = initialInput;
const stageResults: Array<{ name: string; output: string; durationMs: number }> = [];
for (const stage of stages) {
const start = Date.now();
const result = await stage.agent.run(currentInput, { maxTurns: 10 });
if (!stage.validateOutput(result.output)) {
throw new Error(
`Pipeline stage "${stage.name}" produced invalid output. ` +
`Output: ${result.output.slice(0, 200)}...`
);
}
stageResults.push({
name: stage.name,
output: result.output,
durationMs: Date.now() - start,
});
currentInput = result.output;
}
return { output: currentInput, stageResults };
}
// Define the pipeline
const contentPipeline: PipelineStage[] = [
{
agent: new Agent({
name: "outliner",
model: "claude-sonnet-4-20250514",
instructions: `Create a detailed outline for the given topic. Include
5-7 sections with 2-3 bullet points each. Output only the outline.`,
}),
name: "outline",
validateOutput: (output) => output.includes("##") || output.includes("-"),
},
{
agent: new Agent({
name: "drafter",
model: "claude-sonnet-4-20250514",
instructions: `Expand the outline into a full draft. Write 200-300 words
per section. Maintain a professional, informative tone.`,
}),
name: "draft",
validateOutput: (output) => output.split(" ").length > 500,
},
{
agent: new Agent({
name: "editor",
model: "claude-sonnet-4-20250514",
instructions: `Edit the draft for clarity, accuracy, and flow. Fix
grammar issues. Tighten verbose sections. Ensure consistent tone.
Return the final edited version.`,
}),
name: "edit",
validateOutput: (output) => output.split(" ").length > 400,
},
];
const result = await runPipeline(contentPipeline, "Write about microservice architecture");
Multiple agents process the same input simultaneously. A synthesis agent merges their results. Best when diverse perspectives improve quality.
async function parallelWithSynthesis(
workers: Agent[],
synthesizer: Agent,
input: string
): Promise<string> {
// Fork: Run all workers in parallel
const workerResults = await Promise.allSettled(
workers.map((worker) => worker.run(input, { maxTurns: 10 }))
);
// Collect successful results
const outputs: string[] = [];
for (let i = 0; i < workerResults.length; i++) {
const result = workerResults[i];
if (result.status === "fulfilled") {
outputs.push(`### ${workers[i].name}\n${result.value.output}`);
} else {
outputs.push(`### ${workers[i].name}\n[Failed: ${result.reason?.message}]`);
}
}
// Join: Synthesize all results
const synthesisPrompt = `Synthesize these analyses into a unified report.
Resolve contradictions, highlight consensus, and note any gaps.\n\n${outputs.join("\n\n")}`;
const synthesisResult = await synthesizer.run(synthesisPrompt, { maxTurns: 5 });
return synthesisResult.output;
}
A manager agent breaks down complex tasks and delegates subtasks to specialist agents. Best for open-ended problems that require dynamic task decomposition.
import { Tool } from "@anthropic-ai/agent-sdk";
import { z } from "zod";
const specialists = new Map<string, Agent>([
["research", researchAgent],
["analysis", analysisAgent],
["writing", writingAgent],
["code", codeAgent],
]);
const delegateTool = new Tool({
name: "delegate_task",
description: "Assign a subtask to a specialist agent and receive their result",
inputSchema: z.object({
specialist: z.enum(["research", "analysis", "writing", "code"]),
task: z.string().describe("Clear description of what the specialist should do"),
context: z.string().optional().describe("Relevant context from previous subtasks"),
}),
async execute({ specialist, task, context }) {
const agent = specialists.get(specialist);
if (!agent) return { error: `Unknown specialist: ${specialist}` };
const fullTask = context ? `Context:\n${context}\n\nTask:\n${task}` : task;
const result = await agent.run(fullTask, { maxTurns: 10 });
return {
specialist,
output: result.output,
tokensUsed: result.usage?.totalTokens ?? 0,
};
},
});
const managerAgent = new Agent({
name: "project-manager",
model: "claude-sonnet-4-20250514",
instructions: `You are a project manager. Break down complex requests into
subtasks and delegate them to specialists.
Available specialists:
- research: Gathers information and data
- analysis: Analyzes data and produces assessments
- writing: Creates written content and documentation
- code: Writes and reviews code
Process:
1. Analyze the request and identify required subtasks
2. Delegate subtasks in logical order (research before analysis, etc.)
3. Pass relevant context from completed subtasks to subsequent ones
4. Compile specialist outputs into a final deliverable`,
tools: [delegateTool],
});
Agents respond to events rather than being called in a predetermined order. Best for reactive systems that handle asynchronous triggers.
type EventType = "new_ticket" | "escalation" | "resolution" | "feedback";
interface AgentEvent {
type: EventType;
payload: Record<string, unknown>;
timestamp: number;
sourceAgent?: string;
}
class EventOrchestrator {
private handlers = new Map<EventType, Agent[]>();
private eventLog: AgentEvent[] = [];
registerHandler(eventType: EventType, agent: Agent): void {
const existing = this.handlers.get(eventType) ?? [];
existing.push(agent);
this.handlers.set(eventType, existing);
}
async emit(event: AgentEvent): Promise<string[]> {
this.eventLog.push(event);
const handlers = this.handlers.get(event.type) ?? [];
if (handlers.length === 0) {
console.warn(`No handlers registered for event type: ${event.type}`);
return [];
}
const eventMessage = `Event: ${event.type}\nPayload: ${JSON.stringify(event.payload)}\n` +
`Timestamp: ${new Date(event.timestamp).toISOString()}`;
const results = await Promise.allSettled(
handlers.map((agent) => agent.run(eventMessage, { maxTurns: 5 }))
);
return results
.filter((r): r is PromiseFulfilledResult<any> => r.status === "fulfilled")
.map((r) => r.value.output);
}
}
// Setup
const orchestrator = new EventOrchestrator();
orchestrator.registerHandler("new_ticket", triageAgent);
orchestrator.registerHandler("escalation", managerAgent);
orchestrator.registerHandler("resolution", qualityCheckAgent);
orchestrator.registerHandler("feedback", analyticsAgent);
These five patterns are not mutually exclusive — production systems often compose them. Common compositions include:
Router + Pipeline: The router classifies input, then dispatches to one of several specialized pipelines. A support system might route billing inquiries to a billing pipeline (lookup, assess, resolve) and technical inquiries to a technical pipeline (diagnose, fix, verify).
Hierarchical + Parallel: The manager decomposes a task and runs independent subtasks in parallel via the delegate tool. This combines the flexibility of dynamic decomposition with the speed of concurrent execution.
Pipeline + Event-Driven: A processing pipeline handles the main workflow, but specific conditions (errors, threshold breaches, SLA violations) emit events that trigger side-channel agents for alerting, logging, or escalation.
The choice of pattern should follow the structure of the problem, not the other way around. If work naturally flows through stages, use a pipeline. If different inputs need different treatment, use a router. If the task decomposition cannot be predicted in advance, use hierarchical delegation.
Start with the simplest pattern that could work. A router is simpler than a hierarchical manager. A pipeline is simpler than event-driven orchestration. Complexity should be justified by requirements, not by architectural ambition.
Validate between every stage transition. Whether it is a pipeline handoff, a router dispatch, or a manager receiving a specialist's result — validate that the output meets the next stage's expectations. Catching format errors at boundaries is cheaper than debugging cascading failures.
Make orchestration logic observable. Log which pattern is executing, which agents are active, what inputs they received, and what outputs they produced. When multi-agent systems misbehave, the orchestration trace is the primary debugging tool.
Avoid orchestration agents. It is tempting to build an "orchestrator agent" that uses an LLM to decide which pattern to use. In practice, the orchestration logic should be deterministic code. Use agents for tasks that require language understanding; use code for control flow.
Plan for the team to grow. Design your orchestration to accommodate new agents without restructuring. Routers should handle new routes. Pipelines should accept new stages. Hierarchical managers should discover new specialists through their tool definitions, not hardcoded knowledge.