Command: the resume / goto / update three-in-one primitive
Responsibilities
Command is LangGraph's "cross-graph control primitive", defined in libs/langgraph/langgraph/types.py (Command class:759). A single Command instance can carry three kinds of semantics at once: resume (resume execution from an interrupt), goto (specify which node to jump to next), and update (write a value into the graph's state channel). A node function returns Command(...), or the caller passes it as input to graph.invoke(Command(...)). Pregel unpacks it into pending writes and feeds them into channels.
It sits between the node and the Pregel main loop: the node produces a Command, and Pregel uses map_command (map_command:56) to break its fields into (task_id, channel, value) triples written into pending writes. The next prepare_next_tasks naturally sees those writes and schedules accordingly: goto becomes a Send written into the TASKS channel, update writes directly to the corresponding channel, and resume goes through the RESUME channel to be consumed by interrupt().
Design motivation
- Unified expression of side effects: besides writing to state, a node may want to "jump to a non-default next node", "pass a message to the parent graph", or "resume an interrupt". These used to be scattered APIs;
Commandpacks them into a single return type. The node signature-> Command[Literal["node_a", "node_b"]]also lets the type checker validate legal jump targets at compile time. Command.PARENTcrosses graph layers: a node inside a subgraph canreturn Command(graph=Command.PARENT, update=...)to write updates into the parent graph's state.map_commandseesgraph == "__parent__"(PARENT guard:58) andraise InvalidUpdateError("There is no parent graph"); the actual handoff to the parent happens in the parent's_firstwhen it processes writes reported by the subgraph.gotoacceptsSend:Command(goto=Send("node", arg))is equivalent to fanning out a PUSH task;goto="node"is equivalent togoto=Send("node", START)(triggering that node's PULL). SoCommandembeds the capability of conditional edges into the node return value, with no need for a separateadd_conditional_edges.resumeas the single entry point:Command(resume=...)is the only resume path forinterrupt(). It supports both single values (Command(resume="answer"), auto-routed to the unique pending interrupt) and dicts (Command(resume={interrupt_id: value, ...})) for resuming multiple interrupts concurrently.updatehonors reducers:Command(update={"messages": [msg]})still flows through the channel's reducer. Themessageschannel is configured withoperator.add, so this update appends rather than overwrites. TheOverwritewrapper bypasses the reducer (Overwrite:938) and takes the direct-write path on aBinaryOperatorAggregatechannel.
Key files
Command class:759—@dataclass class Command(graph, update, resume, goto), genericNdenotes the type of the goto node name.Command fields:780— definitions and docstrings forgraph/update/resume/goto._update_as_tuples:791— splitsupdateinto[(channel, value), ...], supporting dict, list[tuple], and annotated dataclass forms.map_command:56— core function that breaks aCommandinto a sequence of pending writes.PARENT guard:58— raises "no parent graph" whencmd.graph == Command.PARENT.goto as sends:62—cmd.gotois decomposed into a list ofSends;Sendis written into theTASKSchannel,stris written intobranch:to:<node>to trigger START.resume write:72—cmd.resumeis written into theRESUMEchannel, to be read byinterrupt()on re-entry.update writes:74—cmd.updateis split into(channel, value)pairs written into the corresponding channels, going through the reducer.Command resume mapping:904— entry point where the parent graph, upon receivingCommand(resume=...), routes resume to the corresponding pending interrupt.Overwrite:938—Overwrite(value=...)wrapper, bypasses the reducer when used with aBinaryOperatorAggregatechannel.
Data flow
map_command is the core of Command parsing (map_command:56):
def map_command(cmd: Command) -> Iterator[tuple[str, str, Any]]:
"""Map input chunk to a sequence of pending writes in the form (channel, value)."""
if cmd.graph == Command.PARENT:
raise InvalidUpdateError("There is no parent graph")
if cmd.goto:
if isinstance(cmd.goto, (tuple, list)):
sends = cmd.goto
else:
sends = [cmd.goto]
for send in sends:
if isinstance(send, Send):
yield (NULL_TASK_ID, TASKS, send)
elif isinstance(send, str):
yield (NULL_TASK_ID, f"branch:to:{send}", START)
else:
raise TypeError(
f"In Command.goto, expected Send/str, got {type(send).__name__}"
)
if cmd.resume is not None:
yield (NULL_TASK_ID, RESUME, cmd.resume)
if cmd.update:
for k, v in cmd._update_as_tuples():
yield (NULL_TASK_ID, k, v)task_id is always NULL_TASK_ID — these writes do not belong to any specific task, they are graph-level "outbound" writes: the Send from goto goes into the TASKS channel (the next prepare_next_tasks will schedule it as a PUSH task), the str form of goto goes into the branch:to:<node> channel (a channel reserved for conditional edges; writing START means "trigger that node's entry"); resume goes into the RESUME channel, consumed by interrupt() when the node re-enters; each (k, v) from update goes into the corresponding state channel and is merged by the reducer.
When Command is the input to graph.stream(...), PregelLoop._first (Command resume mapping:904) first converts it into pending writes, then groups them by task_id into checkpoint_pending_writes, and prepare_next_tasks handles them naturally. When Command is a node return value, attach_node attaches a _get_updates mapper to the node's writers (attach_node:1431) which extracts updates from Command._update_as_tuples() and writes them into state channels; goto and resume are routed to the branch:to: channel and RESUME via the _control_branch mapper.
Boundaries and failure modes
Command.PARENTraises in the root graph:map_commanddirectlyraise InvalidUpdateError("There is no parent graph")(PARENT guard:58) —Command.PARENTis only meaningful inside a subgraph node; calling it from the root graph raises immediately.gototype restrictions:gotoaccepts onlySend/stror a sequence of them; anything elseraise TypeError(goto type:69) — passing an integer or dict directly is not allowed.- Single pending interrupt enforced when resume has no id:
Command(resume="single")with multiple pending interruptsraise RuntimeError(multi interrupt error:917); you must switch toCommand(resume={id: value, ...}). updategoes through the reducer and cannot write directly: aBinaryOperatorAggregatechannel (e.g.Annotated[list, operator.add]) applies the reducer by default and merges multiple updates; to bypass the reducer useOverwrite(value=...)(Overwrite:938), but multipleOverwrites to the same channel within the same superstep raiseInvalidUpdateError.Commandandentrypoint.finalare mutually exclusive: an@entrypointfunction returnsentrypoint.final(value=..., save=...)which internally uses theEND/PREVIOUSchannels; returningCommanduses the control channels — the two cannot be combined. An entrypoint function should return eitherCommandorfinal, not both.- Empty
Commandraises: when all three fields ofCommand()areNone/(),map_commandyields nothing and_firstdetects the empty writes andraise EmptyInputError("Received empty Command input")(empty command:928).
Summary
Command is the three-in-one primitive for resume / goto / update. Through map_command it is decomposed into channel writes, and the next prepare_next_tasks handles them naturally. It unifies conditional edges, interrupt resume, and state updates in a single return value. See /interrupt/interrupt for the companion interrupt(), and /subgraph/send-command for Send fan-out semantics. See official docs: LangGraph docs · README.