Command, Send & control flow — API reference
Command, Send & control flow — API reference
Section titled “Command, Send & control flow — API reference”Verified against langgraph==1.1.10 (module: langgraph.types).
LangGraph’s control flow primitives live in langgraph.types:
| Symbol | Purpose |
|---|---|
Command(update, goto, resume, graph) | Update state and/or jump to another node and/or resume an interrupt — all in one return value from a node. |
Send(node, arg) | Dispatch a node with custom state; used from conditional edges for fan-out and from Command.goto for dynamic routing. |
interrupt(value) | Pause the current task and surface value to the client (resume with Command(resume=...)). |
Overwrite(value) | Write directly to a reducing channel, bypassing the reducer. |
Interrupt(value, id) | The dataclass surfaced inside StateSnapshot.interrupts (v1.1: value and id only; older attributes ns, when, resumable were removed in v0.6). |
Minimal runnable example
Section titled “Minimal runnable example”from typing import Literalfrom typing_extensions import TypedDictfrom langgraph.graph import StateGraph, START, ENDfrom langgraph.types import Command
class State(TypedDict): messages: list[str] next: str
def planner(state: State) -> Command[Literal["writer", "critic", "__end__"]]: if len(state["messages"]) >= 3: return Command(goto=END) if state["messages"] and state["messages"][-1].startswith("draft"): return Command(update={"next": "critique"}, goto="critic") return Command(update={"next": "write"}, goto="writer")
def writer(state: State) -> Command[Literal["planner"]]: return Command(update={"messages": state["messages"] + ["draft v1"]}, goto="planner")
def critic(state: State) -> Command[Literal["planner"]]: return Command(update={"messages": state["messages"] + ["critique"]}, goto="planner")
builder = StateGraph(State)builder.add_node("planner", planner)builder.add_node("writer", writer)builder.add_node("critic", critic)builder.add_edge(START, "planner")
graph = builder.compile()print(graph.invoke({"messages": [], "next": ""}))Notes:
- No
add_edgefromplannertowriter/critic/END— the node returnsCommand(goto=...). Declaredestinations={"writer", "critic"}onadd_nodeonly for diagram purposes. - Type-hinting the return as
Command[Literal["writer", "critic", "__end__"]]keeps the Mermaid visualization accurate.
Command in full
Section titled “Command in full”@dataclass(frozen=True, kw_only=True, slots=True)class Command(Generic[N], ToolOutputMixin): graph: str | None = None # target graph ("__parent__" for Command.PARENT) update: Any | None = None # state update (dict, dataclass, Pydantic, tuple list, scalar) resume: dict[str, Any] | Any | None = None goto: Send | Sequence[Send | N] | N = () PARENT: ClassVar[Literal["__parent__"]] = "__parent__"Any subset of update, resume, goto, graph can be set. When a node returns Command(update={...}) without goto, it behaves like returning a dict — the graph’s edges decide where to go next.
update
Section titled “update”update accepts the same shapes as a normal node return:
dict— keys are channel names.- A list of
(channel, value)tuples. - A Pydantic model / dataclass matching the state schema.
- A scalar — written to the
__root__channel when the state has a root channel.
Reducers apply as usual; wrap a value in Overwrite(...) to bypass them.
Command(goto="next_node") # singleCommand(goto=["fan_out_a", "fan_out_b"]) # multiple (unrelated to Send fan-out)Command(goto=Send("worker", {"item": x})) # dispatch a node with custom inputCommand(goto=[Send("w", {"i": i}) for i in xs]) # fan-out with SendsSpecial values:
END→ terminate this execution path.- A node name not in the graph raises
ValueErrorat runtime. - Mixing
Sendand plain names in the same list is allowed.
resume
Section titled “resume”Used to resume from an interrupt(). Two shapes:
Command(resume="a single value") # next interrupt gets this valueCommand(resume={"interrupt-id-1": "v1", "interrupt-id-2": "v2"})# address by interrupt idSee the interrupt() section below.
graph / Command.PARENT
Section titled “graph / Command.PARENT”Command(graph=Command.PARENT, goto="retry", update={"reason": "timeout"})From inside a subgraph node, this routes the command to the parent graph — useful for bubbling an error or a handoff signal up to a supervisor.
class Send: node: str arg: Any def __init__(self, /, node: str, arg: Any) -> None: ...Send packages a node name and a custom state payload. Two places accept it:
- Conditional edges: return one or more
Sends from thepathcallable. Command.goto: returnCommand(goto=Send("worker", {...}))from a node.
The receiving node runs with the provided arg as its state snapshot for this task. The node is a concrete named node; the sent state can be any subset of the node’s input schema.
Equality is structural (node + arg), and Send is hashable.
interrupt()
Section titled “interrupt()”from langgraph.types import interrupt, Command
def ask(state: State) -> dict: answer = interrupt({"question": "How old are you?"}) return {"age": int(answer)}Semantics:
- First execution inside a node raises a
GraphInterruptcontaining anInterrupt(value, id). The graph pauses; theInterruptshows up inStateSnapshot.interruptsand in the__interrupt__key emitted onstream_mode="updates". - The client resumes with
graph.invoke(Command(resume="42"), cfg). The node re-runs from the top, this timeinterrupt(...)returns"42". - Multiple
interrupt()calls in one node are matched by order in the current task. Resume values scope to the task, not the graph. - A checkpointer is required. Without one,
interrupt()raises with no way to resume.
Resume by id when a node has several interrupts:
from langgraph.types import Commandcfg = {"configurable": {"thread_id": "t"}}# From the streaming output, you saw:# __interrupt__ = (Interrupt(value=..., id='abc'), Interrupt(value=..., id='def'))graph.invoke(Command(resume={"abc": "yes", "def": "no"}), cfg)Interrupt dataclass
Section titled “Interrupt dataclass”@final@dataclass(init=False, slots=True)class Interrupt: value: Any id: strOnly value and id are supported. The deprecated interrupt_id property still exists but warns. ns, when, and resumable were removed in v0.6 — use StateSnapshot.interrupts for structural info.
Overwrite
Section titled “Overwrite”Writes a value to a reducing channel without applying the reducer:
import operatorfrom typing import Annotatedfrom typing_extensions import TypedDictfrom langgraph.types import Overwrite
class S(TypedDict): items: Annotated[list[str], operator.add]
def reset(state: S) -> dict: return {"items": Overwrite(["start-over"])}Two Overwrites for the same channel in one super-step raise InvalidUpdateError.
Patterns
Section titled “Patterns”1. Map-reduce with Send
Section titled “1. Map-reduce with Send”from langgraph.types import Send
def dispatch(state: dict) -> list[Send]: return [Send("score", {"item": x}) for x in state["items"]]
builder.add_node("score", score_fn)builder.add_conditional_edges("dispatch", dispatch)builder.add_edge("score", "aggregate")builder.add_edge(["dispatch", "score"], "aggregate") # barrier waitscore runs once per item with its own state snapshot. Use a reducer on the downstream channel (e.g., Annotated[list, operator.add]) so results concatenate.
2. Supervisor routing without edges
Section titled “2. Supervisor routing without edges”from typing import Literalfrom langgraph.types import Command
def supervisor(state: dict) -> Command[Literal["researcher", "writer", "__end__"]]: if not state.get("notes"): return Command(goto="researcher") if not state.get("draft"): return Command(goto="writer", update={"phase": "drafting"}) return Command(goto=END)
builder.add_node("supervisor", supervisor, destinations=("researcher", "writer", END))destinations= feeds the diagram only; the supervisor’s typed return drives execution.
3. Subgraph bubbling to parent
Section titled “3. Subgraph bubbling to parent”def worker(state: dict) -> Command: if state["escalate"]: return Command( graph=Command.PARENT, goto="human_review", update={"reason": state["reason"]}, ) return Command(update={"done": True})Inside a compiled subgraph worker can hand control back to the parent graph’s human_review node while carrying state.
4. Tool-authored commands
Section titled “4. Tool-authored commands”Any @tool that returns a Command is treated as control flow by ToolNode. Example:
from langchain_core.tools import toolfrom langgraph.types import Command
@tooldef transfer_to_refunds(reason: str) -> Command: """Hand this conversation to the refunds agent.""" return Command(goto="refunds_agent", update={"transfer_reason": reason})ToolNode unpacks the Command into a state update plus goto.
5. Interrupt + resume + update
Section titled “5. Interrupt + resume + update”from langgraph.types import interrupt, Command
def approve(state): decision = interrupt({"approve?": state["proposal"]}) if decision == "yes": return Command(goto="execute", update={"approved_by": "human"}) return Command(goto="cancel")
# Client:graph.stream(initial, cfg) # emits __interrupt__graph.invoke(Command(resume="yes"), cfg) # continues into "execute"Gotchas
Section titled “Gotchas”Command(goto="name")bypasses explicit edges. A node that returns a Command will follow the command’s goto even if you calledadd_edge("node", "next"). Pick one style per node.Command.gotodoes not acceptstrfor subgraph namespaces. Always use a plain node name at the current graph level; cross-graph jumps usegraph=Command.PARENT.- The type parameter on
Command[Literal[...]]is for the visualizer. It doesn’t narrow to runtime errors. Send(node, arg)ignores the main state.argis the snapshot for the target node’s run. If you need context, stuff it intoarg.- Equality compares
argtoo. TwoSend("x", {...})with unhashable dicts are hashable at theSendlevel but raise if you stick them in a set without care — dict compares structurally, hash uses tuple of(node, arg). - A node that returns
Command(graph=Command.PARENT)outside a subgraph raises. Only valid when the node runs inside a compiled subgraph used by a parent. update=in aCommandstill goes through reducers. UseOverwrite(...)in theupdatevalues if you need to replace a reducing channel.- Resuming an interrupt re-runs the node from the top. Make side effects idempotent or put them in
@tasks.
Breaking changes
Section titled “Breaking changes”| Version | Change |
|---|---|
| 1.0 | Command is the canonical way for a node/tool to return control-flow intent. Returning a dict still works for pure state updates. |
| 0.6 | Interrupt.ns, Interrupt.when, Interrupt.resumable removed. Interrupt.interrupt_id deprecated in favor of Interrupt.id. |
| 0.4 | Interrupt.id introduced as a property, supporting resume-by-id via Command(resume={id: value}). |
| 0.2.24 | RetryPolicy, CachePolicy, Interrupt first exported from langgraph.types. |