LangGraph's State, Node, and Edge Model — A Practical Walkthrough of Flow Control
LangGraph's explicit state reducers and execution-step model make concurrent state mutations predictable — a hard problem when multiple agent nodes run in parallel. Getting the reducer wrong silently corrupts state; getting it right means agent workflows can fan out, loop, and merge without race-condition surprises.
LangGraph is the lower-level orchestration framework underneath LangChain's `create_agent`, built around three primitives: a shared State schema, executable Nodes, and Edges that wire them together. State passes between nodes via a reducer mechanism — `operator.add` for concatenation, `add_messages` for incremental message lists with deduplication — and `Overwrite` can bypass the reducer entirely when a node needs to reset a field. Input and output schemas constrain what enters and leaves the graph, while intermediate state types let nodes exchange transient data without polluting the main state.
Flow control goes well beyond linear chains. Conditional edges route execution through a `router` function with an optional `path_map`; `Command` moves routing logic inside the node itself, returning a `goto` target and an `update` payload. Parallel branches run multiple nodes in the same execution step — but if they touch the same state field without a reducer, `InvalidUpdateError` fires. `Send` dispatches dynamic parallel tasks from within a router, and `defer` marks a node to run last, useful for logging or audit steps.
Loop structures — the core of a ReAct agent — are built either by wiring a conditional edge back to a model node or by having tool nodes return `Command(goto="model_node")`. A global recursion limit (default 25 steps) prevents infinite loops; `RemainingSteps` exposes a countdown so nodes can exit gracefully before hitting `GraphRecursionError`. Execution order is visible through `langgraph_step` in the node config, revealing that parallel nodes share a step while linear nodes each get their own.
TypedDict has become the dominant State definition style in LangGraph not for elegance but because `Annotated` injection for reducers works cleanly with it — dataclass and Pydantic alternatives throw different errors on access.
The `Overwrite` mechanism is a sharp escape hatch: it doesn't just set a value, it invalidates all prior reducer accumulation, which means downstream nodes see only the overwritten result.
Parallel execution in LangGraph is scheduling-level, not thread-level; nodes in the same step still run sequentially in code order, but the framework treats them as a single batch for state merging.
The `langgraph_step` counter exposes a subtle footgun: a non-`END` merge node in a parallel graph runs multiple times — once per incoming path — unless you collapse the edges by passing a list of sources.
Moving routing logic from `add_conditional_edges` into `Command` inside the node itself shifts the graph from declarative edge wiring to imperative control flow, which can simplify complex decision trees at the cost of making the graph topology less visible.
The recursion limit is graph-wide, not per-node; a self-looping node consumes steps for the entire graph, so other branches can be starved if one loop runs away.