scalable/ai/langgraph agent teams/lesson 04 cph / /
lesson 04 / 08 · 14 min · updated ·

the supervisor — teams with a router

a team is agents plus a routing decision. the supervisor pattern, handoffs via Command, and why the supervisor should do nothing else.

tl;dr. the supervisor pattern is a hub-and-spoke team: specialist workers, and one routing agent whose only job is deciding who works next. workers never talk to each other — everything round-trips through the hub. Command lets the supervisor return a routing decision and a state update in one value. it's the boring team shape, which is why it's the one to reach for first.

a team is agents plus a routing decision

lesson three ended at the twenty-tool agent — one generalist drowning in its own menu. the fix is division of labour: three narrow agents, each with a toolset small enough to use well. but the moment there are three, someone has to decide whose turn it is. that decision is the actual design problem of multi-agent systems, and the supervisor pattern answers it the simplest way: make the decision a node.

hub and spoke — all roads lead back
//                ┌────────────┐
//     START ───► │ supervisor │ ◄─────────┐
//                └─────┬──────┘           │
//          reads state, picks the         │
//          next worker — or END           │
//       ┌─────────┼──────────┐            │
//       ▼         ▼          ▼            │
//  ┌─────────┐ ┌───────┐ ┌────────┐       │
//  │ billing │ │ tech  │ │ orders │       │
//  └────┬────┘ └───┬───┘ └────┬───┘       │
//       └──────────┴──────────┴───────────┘
//        every worker reports back to the
//        supervisor. nobody talks sideways.

the supervisor node

a router that returns a Command
import { StateGraph, MessagesAnnotation, Command, START, END } from "@langchain/langgraph";
import { z } from "zod";

const WORKERS = ["billing", "tech", "orders"] as const;

// the supervisor is a model call with ONE job: pick the next hop.
// structured output, not prose — the router should never parse.
const routeSchema = z.object({
  next: z.enum([...WORKERS, "done"]),
  reason: z.string().describe("one line, for the trace"),
});

async function supervisor(state: typeof MessagesAnnotation.State) {
  const { next, reason } = await model
    .withStructuredOutput(routeSchema)
    .invoke([
      { role: "system", content: SUPERVISOR_PROMPT },
      ...state.messages,
    ]);

  // Command = routing decision + state update in one return value
  return new Command({
    goto: next === "done" ? END : next,
    update: { messages: [aiMsg(`→ ${next}: ${reason}`)] },
  });
}

workers are lesson-three agents

agents as nodes; fixed edges back to the hub
// each worker is a lesson-three agent,
// mounted as a single node. narrow prompt, narrow toolset.
const billing = createAgent({ model: fast, tools: billingTools,
  systemPrompt: "you handle invoices and payment plans. nothing else." });
const tech    = createAgent({ model: strong, tools: techTools,
  systemPrompt: "you debug product issues. nothing else." });
const orders  = createAgent({ model: fast, tools: orderTools,
  systemPrompt: "you handle order state. nothing else." });

const team = new StateGraph(MessagesAnnotation)
  .addNode("supervisor", supervisor, { ends: [...WORKERS, END] })
  .addNode("billing", billing)
  .addNode("tech", tech)
  .addNode("orders", orders)
  .addEdge(START, "supervisor")
  // workers always report back — the supervisor owns control flow
  .addEdge("billing", "supervisor")
  .addEdge("tech", "supervisor")
  .addEdge("orders", "supervisor")
  .compile();

two things earn attention here. the workers' return edges are fixed — a worker finishes and reports back, no negotiation. keeping workers dumb about control flow is what makes them reusable and testable in isolation; they're personas, and the graph is the thing that composes them. and the models are mixed on purpose: routing and judgment get the stronger model, mechanical workers get the faster one. that mix is what keeps a daily-run agent team affordable rather than a demo you can't afford to leave on.

there's a library for this. @langchain/langgraph-supervisor packages this pattern — one createSupervisor call with your agents and llm, done. use it once you've built the shape by hand once; the library's defaults (what workers see of history, how handoffs are worded) are exactly the knobs you'll need to reason about when routing goes weird.

what the supervisor should not do

the pattern degrades the same way in every codebase: the supervisor starts helping. it answers the easy questions itself, it summarizes worker output, it patches a worker's half-done job. now routing quality and answer quality are tangled in one prompt, and improving either degrades the other. hold the line: the supervisor routes. if it keeps answering trivial questions itself, that's your signal the team is missing a general worker — add the specialist, don't grow the router. one prompt, one job — the same rule that governs every persona in the pipeline.

when it breaks

next: swarms — delete the hub, let agents hand off directly, and find out what that buys and what it costs.

scalable labs·cvr 30091604·github·linkedin·hello@scalable.dk