skip to content

Multi-Agent Patterns

You will learn multi-agent composition: supervisor and hierarchical architectures, agents as nodes, subgraph delegation, handoffs via Command and goto, and choosing shared versus isolated state. Interviewers ask because they want to hear when several agents genuinely beat one agent with more tools, and what the extra latency and token spend actually buys.

part ofAI agent & RAG frameworksoverview, primer and where to startread it →
on this pageshow

questions

5

In LangGraph, what does returning Command(goto=...) from a node give you over a plain state update?

level: middleimportance: must knowfreq 68%

answer

  1. one return value, two jobs
  2. update plus goto together
  3. handoff without a declared edge
  4. Literal annotation restores the drawn edges
  5. graph=Command.PARENT escapes a subgraph

basics

~20 s

A Command return carries both a state update and a goto, so one node writes state and names its successor in a single value. That is the handoff idiom in multi-agent graphs; a plain dict return leaves routing entirely to the declared edges.

solid answer

~50 s

A LangGraph node normally returns a dict that the state reducers merge in, and where control goes next is decided by edges declared at build time. `Command`, from `langgraph.types`, collapses those two steps into one value: `return Command(update={"messages": [msg]}, goto="writer")` applies the update through the same reducers and then jumps to the `writer` node. That is exactly what an agent handoff wants — the agent that decides to delegate is the same code that records why it delegated. Because there is no static edge to read, annotate the node as `-> Command[Literal["writer", "__end__"]]` so the compiled graph still knows the candidate destinations for drawing and inspection. From a node inside a subgraph, `Command(goto="other_agent", graph=Command.PARENT)` hands control to a node in the parent graph. In LangGraph 1.x this is the current handoff idiom; keep plain dict returns for nodes whose successor is genuinely static.

code

python · 26 lines
python
from typing import Literal

from langgraph.graph import END, START, MessagesState, StateGraph
from langgraph.types import Command


def researcher(state: MessagesState) -> Command[Literal["writer"]]:
    findings = f"researched: {state['messages'][-1].content}"
    return Command(
        update={"messages": [{"role": "assistant", "content": findings}]},
        goto="writer",
    )


def writer(state: MessagesState) -> Command[Literal["__end__"]]:
    return Command(
        update={"messages": [{"role": "assistant", "content": "final draft"}]},
        goto=END,
    )


builder = StateGraph(MessagesState)
builder.add_node("researcher", researcher)
builder.add_node("writer", writer)
builder.add_edge(START, "researcher")
graph = builder.compile()

go deeper

for a junior

Know that a node normally returns a dict of state changes and that edges decide what runs next. Be able to say that Command is the object that does both at once.

for a middle

Explain that update flows through the same reducers and goto is resolved at runtime, and show the Command[Literal[...]] annotation that keeps the drawn graph honest.

for a senior

Argue about where routing should live: decisions the node makes belong in Command, structural transitions belong in edges. Mention that a bad goto fails at runtime, not at compile time.

for a principal

Own the convention for a whole codebase — a graph where every node routes itself has no inspectable topology, and a graph with no Command pushes agent decisions through state flags. Set the boundary and enforce it in review.

## The two things a node does Every LangGraph node does at most two things: it produces a partial state update, and it causes control to move somewhere. Classically those are separate mechanisms. The update is the dict the node returns, merged into the graph's state channels by each channel's reducer. The movement is declared at build time with `add_edge("a", "b")`, or decided by a separate routing function you attach to the graph, which reads the state the node just wrote and returns a node name. That separation is fine for a pipeline. It is awkward for agents. When a research agent concludes "I have enough, the writer should take over", the decision and the evidence for it are produced by the same reasoning step, and splitting them forces you to smuggle the decision through state — write `state["next"] = "writer"`, then have a second function read `state["next"]` back out and return it. That indirection is the thing `Command` removes. ## What Command is `Command` is a dataclass in `langgraph.types`. The fields that matter for multi-agent composition are: - `update` — the same partial-state dict a node would otherwise return. It is not special-cased: it flows through the channel reducers exactly as a plain return value would, so an `add_messages` channel still appends and de-duplicates by message id. - `goto` — the name of the next node, or a list of node names, or `END`. Resolved at runtime, when the node returns. - `graph` — set to `Command.PARENT` to resolve `goto` against the parent graph instead of the graph the node lives in. A node returning `Command(update={...}, goto="writer")` therefore performs the state write and the transition atomically from the caller's point of view: the update lands, then `writer` runs against the updated state. ## Why the Literal annotation matters When routing lives in `goto`, the compiled graph has no static outgoing edge for that node, so `graph.get_graph().draw_mermaid()` would show a node with no successors and reviewers lose the topology. The convention is to type the return as `Command[Literal["writer", "__end__"]]`; LangGraph reads those literals to render the candidate destinations. Treat it as documentation the toolchain can see, not as a runtime constraint — it is the thing that keeps a Command-heavy graph readable six months later. ## Handoff from inside a subgraph Multi-agent graphs are usually nested: each agent is itself a compiled graph attached as a node. A node deep inside the research subgraph that wants to hand off to the `writer` node — a sibling of the whole research subgraph in the parent — cannot name it in a plain `goto`, because `goto` resolves within the current graph. `Command(goto="writer", graph=Command.PARENT)` is the escape hatch: the update applies to the subgraph's state (and propagates upward through whatever keys parent and child share), and control resumes at `writer` in the parent. This is the mechanism behind swarm-style handoffs, where any agent can pass control to any other without a central router. ## Tools can return Command too The prebuilt tool-executing node recognises a `Command` returned by a tool function and applies it, rather than wrapping it as a tool result string. That is how a handoff *tool* is built: the model calls `transfer_to_writer`, the tool returns `Command(goto="writer", update={...}, graph=Command.PARENT)`, and the transfer becomes an ordinary tool call the model already knows how to emit. The alternative — a supervisor node that reads structured output and routes — puts the decision in a dedicated LLM call instead of inside the worker's own turn. ## When not to use it `Command` is not a replacement for edges. If a node always goes to exactly one place, `add_edge` says so declaratively, is visible in the drawn graph, and cannot drift. Scattering `goto` through a dozen nodes rebuilds a control-flow graph that only exists at runtime, which is precisely what LangGraph's static topology was meant to make inspectable. The rule of thumb: use `Command` where the transition is a *decision the node makes*, and edges where the transition is *structure*. ## Failure modes A `goto` naming a node that does not exist is a runtime failure, not a build-time one, so a typo in an agent name survives `compile()`. Keep the destination names in one module-level constant list shared by the annotation and the routing logic. And remember that `update` is still merged, not assigned: returning `Command(update={"messages": [...]})` on a channel with an appending reducer appends — it does not replace the history.

  • If a node returns a Command with goto, do you still declare an outgoing edge for that path?
    No. The destination is resolved at runtime from `goto`, so you do not add an edge for it; adding one would route unconditionally regardless of the decision. What you lose is the visible topology, which is why the return type is annotated `Command[Literal[...]]` — LangGraph reads those literals to render the possible successors in the drawn graph.
  • Does update= bypass the state reducers?
    No. The dict in `update` goes through exactly the same channel reducers as a plain returned dict. On a messages channel with an appending reducer it appends and de-duplicates by message id rather than replacing the list. Treating `update` as a direct assignment is a common source of vanished conversation history.
  • Can goto point at END, and when would you want that?
    Yes — `Command(goto=END)` terminates the run from inside the node. It is how a worker signals that no further delegation is needed without a supervisor having to make another model call to reach the same conclusion, which saves one round trip per finished task.

saying these in an interview costs you the question

  • Says Command should replace every static edge in the graph
  • Thinks goto names are validated when the graph is compiled
  • Believes update= assigns state directly, bypassing reducers
  • Confuses Command.PARENT with jumping back to START
  • Claims a node must choose between updating state and routing

context

open as a page

In LangGraph, how does a supervisor route to worker agents, and what does each hop cost?

level: middleimportance: must knowfreq 74%

basics

~20 s

A supervisor is an LLM node that reads shared state and returns a Command naming which worker node runs next; each worker hands control back to the supervisor. Every delegation therefore costs an extra model call, on a message list that keeps growing.

open as a page

In a LangGraph multi-agent graph, when should agents get isolated state instead of one shared message list?

level: seniorimportance: must knowfreq 58%

basics

~20 s

Share one message channel when agents must see each other's reasoning; isolate when they must not. Isolation means giving an agent its own state schema in a subgraph and writing only a summarized result back to the parent — smaller prompts and no cross-contamination, at the cost of lost context.

open as a page

How do you add a compiled LangGraph subgraph as a node when its state schema differs?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Wrap it. When parent and subgraph share the state keys, pass the compiled subgraph straight to add_node. When the schemas differ, add an ordinary function node that translates parent state into the subgraph's input, calls subgraph.invoke, and maps the result back into parent keys.

open as a page

When do multiple LangGraph agents beat one agent with more tools?

level: principalimportance: should knowfreq 42%

basics

~20 s

Split when a single prompt can no longer carry the job: too many tools to select from reliably, genuinely different models or permissions per role, or work that must run in parallel. Otherwise one agent with a good tool set is cheaper, faster and far easier to debug.

open as a page