Skip to content

Swarm

A user message arrives that could be billing OR technical OR a refund. You don’t want one giant prompt that knows everything — that’s where agents get vague and hallucinate. You want a triage agent that figures out which specialist should handle this, then hands off. The specialist focuses on its domain. That’s Swarm — and it’s just a Loop over Agents with a route function.

Swarm = a hierarchical agent pattern where one orchestrator routes each turn to a specialist agent. The route function is sync, pure, and returns the id of the next agent (or undefined to halt). Each handoff is one iteration of a Loop:

Loop {
current_agent = route(input)
if current_agent is undefined → exit
result = current_agent.run(input)
input = result + handoff annotation
}

In agentfootprint there is no SwarmAgent class. Swarm is a recipe — swarm({ agents, route, maxHandoffs }) constructs a runnable that wraps a Loop. The 4-composition substrate (Sequence/Parallel/Conditional/Loop) is enough; we don’t need a 5th primitive.

examples/patterns/06-swarm.ts (region: swarm-recipe)
const router = swarm({
agents: [
{ id: 'triage', runner: triage },
{ id: 'billing', runner: billing },
{ id: 'tech', runner: tech },
],
// Route function — pure sync over the current message. First turn goes
// to triage, then to billing or tech based on content, then halts.
route: (input) => {
const msg = input.message.toLowerCase();
if (msg.includes('[billing]')) return undefined; // billing done → halt
if (msg.includes('[tech]')) return undefined; // tech done → halt
if (msg.includes('refund') || msg.includes('bill')) return 'billing';
if (msg.includes('status') || msg.includes('error')) return 'tech';
return 'triage'; // first turn
},
maxHandoffs: 5,
});

The route function is the orchestrator’s brain. It inspects the current input (often the previous handoff’s result) and decides which agent to dispatch next. Returning undefined halts the loop — the most recent specialist’s output becomes the final answer.

maxHandoffs caps the loop so a buggy route function can’t infinite-loop. Production: 3–5 handoffs covers most triage flows.

You CAN make route call an LLM internally — it’s just a function. But the standard pattern is a deterministic route based on the previous specialist’s output (often via convention markers like [BILLING] or [ESCALATE] in the message). That keeps the routing layer cheap, fast, and debuggable. The intelligence lives in the specialists; the routing is mechanical dispatch.

If you need LLM-driven routing, use Conditional with an LLM-based predicate — different shape, same outcome.

Every specialist’s call inherits the orchestrator’s identity automatically. Memory, observability, cost tracking — all scoped per-tenant across the entire handoff chain.

When to use Swarm vs Conditional vs Sequence

Section titled “When to use Swarm vs Conditional vs Sequence”
SituationUse
One classifier picks ONE specialist for the whole turnConditional
Specialists may hand back to triage or to each otherSwarm (this guide)
Fixed pipeline: research → write → edit (no branching)Sequence
Multi-perspective fan-out → mergeParallel
  • Don’t put expensive logic in the route function. It runs every iteration. Keep it as cheap as text inspection.
  • Don’t share state between specialists via globals. Use MemoryIdentity + memory layer for cross-agent state.
  • Don’t omit maxHandoffs. A buggy route returning the same agent every time runs forever.
  • Patterns overview — Reflexion, ToT, Debate, Map-Reduce all run the same way (recipes over the substrate)
  • Conditional — single-shot routing alternative