Two agents, one dispatcher: the orchestrator that decides who answers
So far, my agent series revolved around a single agent: the harness that turns an LLM into a doer, the Claude Agent SDK that ships you the loop. One agent, one task, one answer.
The real shift happens when you want two. And the surprise is that it doesn’t play out inside the agents. It plays out in the piece that never answers: the dispatcher.
The use case I wanted: a small assistant on my own site. You ask it a question, and depending on what you’re after, it either digs through my notes or goes searching the web for fresh info. Two skills with nothing in common. The interesting question isn’t “how does each one answer” — it’s “who decides which of the two works?”
The pattern has a name: routing
What I’m describing is a documented agent pattern: routing. A component classifies the request’s intent, then dispatches it to the right specialist. No magic — it’s exactly what a switchboard operator does: they don’t solve your problem, they connect you to the right person.
Question
│
▼
Orchestrator ──► detects intent
│
├─ "fresh / external info" ──► Web agent (searches the internet)
└─ "info that lives with me" ──► Internal agent (digs through my notes)
Three components, two agents. The orchestrator isn’t a third agent that “does” something — it’s a sorter. And it’s the subject of this article, because it’s what makes the whole thing smart.
Three components, mostly markdown
Before the code, the key point: each of these three pieces isn’t a block of TypeScript, it’s a folder. The pattern I’ve repeated since the harness article — agent.md + skill + tools — applies as-is. Here’s the system’s full tree:
.claude/
├─ CLAUDE.md ← agent.md: the contract shared by all 3
└─ skills/
├─ intent-router/SKILL.md ← the orchestrator
├─ web-search/SKILL.md ← the web agent
└─ internal-search/SKILL.md ← the internal agent
Three SKILL.md, one CLAUDE.md, and a little code that wires it together. Let’s break down each piece.
The agent.md — the contract all three share
The CLAUDE.md is the agent.md: the tone, the language, the rules that every agent follows. You write it once, it applies to the orchestrator and the specialists alike.
# Site assistant — shared contract
- Answer in English, direct tone, zero fluff.
- Essentials first, details after.
- Don't know? Say so. Never invent a source. The orchestrator — a skill with no tools at all
Its only job: turn a fuzzy question into a clean decision. Its SKILL.md gives it no tools — an orchestrator that can search or read is an orchestrator that will do the specialists’ job instead of routing to them.
---
name: intent-router
description: Classifies an incoming question and picks the agent that answers.
allowed-tools: [] # no tools: it decides, that's all
---
You NEVER answer the question. You classify it.
- "web" → external, fresh info (news, prices, releases).
- "internal" → info that lives in my notes / my site.
Return { route, reason, reformulated }. Reword the question
for the specialist — without answering it. The web agent — a skill, one tool: WebSearch
---
name: web-search
description: Answers questions that need external, up-to-date info.
allowed-tools: WebSearch # it sees the web, not my files
---
Search the web before answering. Cite your sources (title + link).
If sources contradict each other, say so rather than guessing. The internal agent — a skill, read tools (and no RAG)
---
name: internal-search
description: Answers questions about my notes, my site, my specs.
allowed-tools: Read, Glob, Grep # it sees my files, not the web
---
Dig through ./content YOURSELF — agentic search, no RAG.
Glob to target, Grep to find, Read to read in full.
Stay faithful to what I wrote; don't fill gaps with general knowledge. The key is in the allowed-tools frontmatter: the tools don’t live in the code, they live in the skill. Loading a skill loads its tools. The web agent can’t read my files; the internal agent can’t touch the network. This isn’t just tidiness — it’s security by perimeter, declared in two words of markdown.
The full map
Three rows, and you see the entire system — agent.md, skill, tools, model:
| Component | agent.md | skill | tools | model |
|---|---|---|---|---|
| Orchestrator | CLAUDE.md | intent-router | none | Haiku (fast) |
| Web agent | CLAUDE.md | web-search | WebSearch | Opus |
| Internal agent | CLAUDE.md | internal-search | Read · Glob · Grep | Opus |
The CLAUDE.md is shared; what sets the three apart is the skill and its tools. And the model: classifying a question is trivial, so the orchestrator runs on a fast, cheap Haiku while Opus is reserved for the answer. Paying for the right model in the right place is one of routing’s real wins.
The code just wires it up
Once the files are written, the TypeScript is thin: it loads the right skill and passes the baton. The structured output forces the orchestrator to return a typed decision, not a sentence.
import { query } from "@anthropic-ai/claude-agent-sdk";
const ROUTE_SCHEMA = {
type: "object",
properties: {
route: { type: "string", enum: ["web", "internal"] },
reason: { type: "string" },
reformulated: { type: "string" },
},
required: ["route", "reason", "reformulated"],
};
// 1. The orchestrator classifies — its skill, no tools, a fast model.
const decision = await query({
prompt: question,
options: {
settingSources: ["project"], // loads CLAUDE.md
skills: ["intent-router"], // → allowed-tools: [] comes from the skill
model: "claude-haiku-4-5-20251001",
outputFormat: { type: "json_schema", schema: ROUTE_SCHEMA },
},
});
// 2. Dispatch to the specialist — it loads ITS skill, hence ITS tools.
const SKILL = { web: "web-search", internal: "internal-search" };
const { route, reformulated } = decision.structured_output;
const answer = await query({
prompt: reformulated,
options: {
settingSources: ["project"],
skills: [SKILL[route]], // the skill carries its allowed-tools
},
});
Notice what’s not in the code: no hardcoded tool list, no giant system prompt. Each agent’s behavior lives in its SKILL.md; the code just picks which one to load. That’s what “you mostly write markdown” means.
By the way: how does the orchestrator “call” the agent?
The question everyone glosses over. The answer fits in one sentence: agents share no magic channel — “calling an agent” is a tool call. And there are two ways to wire it.
Style A — orchestration lives in your code. That’s what the code above does: the orchestrator doesn’t really call the specialist, it’s my TypeScript chaining two query() calls and passing one’s result to the other. The “communication” between the two agents is a JS variable — reformulated. The channel is the host program. Explicit, debuggable, you force everything.
Style B — the orchestrator delegates on its own. Here the orchestrator is an agent that knows its subagents and holds the Task tool. During its own loop, it decides to emit a Task(...) call; the SDK runs the subagent and feeds the result back. You declare the subagents inline:
const answer = await query({
prompt: question,
options: {
allowedTools: ["Task"], // the orchestrator can delegate
agents: { // the subagents, defined inline
"web-search": {
description: "External, fresh info: news, prices, releases",
prompt: "Search the web and cite your sources.",
tools: ["WebSearch"],
},
"internal-search": {
description: "Questions about my notes / my site",
prompt: "Agentic search over ./content, no RAG.",
tools: ["Read", "Glob", "Grep"],
},
},
},
});
(Same subagents can be defined as .claude/agents/*.md files, frontmatter name / description / tools / model — the file counterpart of skills.)
Whatever the style, the protocol is identical and fits in three points:
- Isolated context — the subagent runs in its own blank context window. It doesn’t see the parent’s history.
- Outbound — the parent passes it just a prompt: the reworded task. Nothing else.
- Inbound — only the final result comes back, as a tool result in the parent’s loop. The subagent’s intermediate steps don’t pollute it.
Which to pick? For a deterministic router like mine — the orchestrator classifies with Haiku and a structured output — Style A keeps control: you force the decision. Style B is more autonomous, but you also hand the choice to delegate to the model; save it for open-ended delegation (“figure it out, subcontract what you need”), not for routing you want to control.
Why not a single agent with all the tools?
The honest question, because it has a real answer — and sometimes the answer is “you’re right, one is enough.”
The router earns its place when the specialists genuinely diverge:
- Different tools —
WebSearchon one side, file reads on the other. That’s my case, and it’s the best reason. - Sharpened skills — one agent “cite your web sources,” the other “stay faithful to what I wrote.” Two incompatible stances in a single prompt.
- Different models — Haiku to classify, Opus to analyze. You can’t tune that finely inside one monolithic agent.
If none of that is true, keep it simple: one agent, both tools, a good skill. Architecture is an answer to real complexity — not a trophy.
What it teaches me
I wrote recently that the dev job was sliding toward steering: the value is no longer in production, but in framing and judgment. This little assistant is the miniature, literal version of that.
The three SKILL.md are almost mundane — a few lines, one tool, one instruction. All the system’s intelligence lives in the routing. The component that never answers is the one that decides everything. So going from one agent to two isn’t about doubling the answering work: it’s about adding the only question that truly matters — “who’s best placed for this?”
And once you have a two-way router, adding a third route costs almost nothing: one more SKILL.md folder, one more entry in the enum. The dispatcher itself doesn’t change. That’s exactly what makes this pattern as solid as it looks simple.