Appearance
5.1 — State, Nodes, and Edges
1. What is it?
LangGraph is a library for building stateful, multi-step LLM applications as explicit graphs: nodes (units of work — a function, often calling an LLM or a tool), edges (the connections defining what runs next), and a shared state object that flows through and is updated by nodes as execution proceeds. Recall from Part 4.1 that create_agent is itself built on top of LangGraph's runtime — this Part goes underneath that high-level abstraction to the graph execution model itself, which you need to understand fully for anything beyond the standard agent pattern: custom multi-agent systems, complex conditional workflows, and fine-grained control over persistence and human-in-the-loop behavior (Part 5.3, 5.4).
2. Why does it exist?
Part 3.7 drew a hard line between workflows (fixed control flow) and agents (dynamic control flow), and argued for building the fixed, knowable parts of a system as explicit workflows. A plain Python function with if/else branches can express a simple fixed workflow, but real production workflows need more than branching: they need persistence (resuming after a crash mid-workflow, Part 5.3), the ability to pause for human input and resume later, potentially hours or days afterward (Part 5.4), streaming visibility into progress (Part 5.5), and composability (nesting one workflow inside another, Part 5.6). LangGraph exists to give these production-grade properties to both fixed workflows and dynamic agents, expressed through one unified graph abstraction, rather than requiring you to hand-build persistence and resumability from scratch for every custom control-flow pattern Part 3.7 described.
3. What problem does it solve?
It solves "how do I build an arbitrarily complex, multi-step LLM application — whether its control flow is fixed (a workflow) or dynamic (an agent) — with real persistence, resumability, and observability, without hand-rolling the state-management and checkpointing machinery myself." This directly extends Part 3.7's workflow/agent patterns and Part 3.9's memory patterns into a framework with production-grade infrastructure already built for exactly this class of problem.
4. How does it work internally?
The state object and how it flows
A LangGraph graph has one shared state — typically defined as a typed structure (a TypedDict or Pydantic model) — that every node receives as input and returns a (partial) update to. This is the single most important concept to internalize correctly, because it's genuinely different from how a plain function call or a LangChain LCEL chain (Part 4.1, where each step's entire output becomes the next step's entire input) works:
python
from typing import TypedDict
class SupportState(TypedDict):
messages: list[dict]
ticket_category: str | None
resolved: boolEach node is a function taking the current state and returning a dict of the fields it wants to update — not the entire new state, just the delta:
python
def classify_ticket_node(state: SupportState) -> dict:
category = classify(state["messages"][-1]["content"])
return {"ticket_category": category} # only this field is updatedLangGraph merges this returned partial update into the overall state before passing the updated state to whichever node runs next. This partial-update model is precisely what makes a graph's state genuinely shared and cumulative across many nodes (Part 3.9's structured-memory-within-a-run concept, formalized), rather than each step needing to explicitly thread through every piece of information it doesn't itself modify, the way you would with plain function composition.
Reducers — how updates actually combine (not just overwrite)
By default, a node's returned update for a given field replaces that field's previous value. But for fields that should accumulate rather than replace — most importantly, messages, which should grow with each new message rather than being overwritten — LangGraph uses reducers, specified via Annotated type hints:
python
from typing import Annotated
from langgraph.graph.message import add_messages
class SupportState(TypedDict):
messages: Annotated[list[dict], add_messages]
ticket_category: str | Noneadd_messages is a reducer function: instead of the new value replacing the old one, LangGraph calls add_messages(old_messages, new_messages) and uses that result as the updated field — in this case, appending new messages (and handling message-ID-based deduplication/updates) rather than discarding conversation history on every node's update. This reducer mechanism is exactly what makes the shared-state model work correctly for accumulating data like conversation history, while still allowing other fields (like ticket_category) to use simple overwrite semantics by default — you can write custom reducers for any field needing different accumulation logic (e.g., merging two dicts, appending to a running list of tool-call results) rather than being limited to overwrite-or-append.
Nodes and edges — the graph structure
python
from langgraph.graph import StateGraph, END
graph = StateGraph(SupportState)
graph.add_node("classify", classify_ticket_node)
graph.add_node("handle_billing", handle_billing_node)
graph.add_node("handle_technical", handle_technical_node)
graph.add_edge("classify", "route_by_category") # simplified — see conditional routing, Part 5.2
graph.set_entry_point("classify")
graph.add_edge("handle_billing", END)
graph.add_edge("handle_technical", END)
compiled_graph = graph.compile()A node is registered with a name and a callable (sync or async — Part 1.1's async discipline applies identically here); an edge connects one node's output to the next node that should run. compile() transforms this graph definition into an executable object — importantly, compile() is also where you attach a checkpointer for persistence (Part 5.3) and other runtime configuration, meaning the same graph definition can be compiled differently for different deployment needs (e.g., an in-memory checkpointer for local development, a Postgres-backed one for production, Part 5.3).
The execution model — supersteps
LangGraph executes a compiled graph in discrete supersteps: at each superstep, all nodes that are currently "active" (their incoming edges have been satisfied) run, potentially in parallel if multiple are active simultaneously, each producing a state update; these updates are merged (via reducers where defined); then the next superstep begins with whichever nodes become active based on the new state and the graph's edges. This superstep model is what makes genuine parallelism (Part 3.7's "parallelization" workflow pattern, and Part 5.6's multi-agent patterns) a natural, first-class part of the execution model rather than something bolted on via manual asyncio.gather calls within a single node.
5. Simple mental model
Think of a LangGraph graph as a factory assembly line with a shared parts bin, not a single conveyor belt passing one item hand to hand. In a simple linear pipeline (LCEL, Part 4.1), each station hands its complete output directly to the next station. In LangGraph, every station (node) instead reaches into a shared bin (the state) to get what it needs, does its work, and puts back just the specific parts it changed or added — the bin as a whole accumulates the full picture across every station's contribution, and a reducer is the rule for how a specific type of part gets combined into the bin (does a new part replace the old one, or get added alongside it).
6. Real-world example
A loan-application review workflow (Part 3.7's real-world example, now built as an actual LangGraph graph) has a shared state including credit_check_result, income_verification_result, fraud_check_result, and messages (an accumulating log of the process, using the add_messages reducer). Each check runs as its own node, writing only its own specific result field into the state without needing to know about or preserve the other checks' results — and because these three checks are logically independent, they can be structured as three nodes active in the same superstep, running concurrently, each safely updating its own distinct state field without interfering with the others' updates.
7. Architecture diagram
Shared Statemessages (accumulates via add_messages reducer) · credit_check, income_check, fraud_check (each overwritten by its own node)
superstep · all three can run concurrently
credit_check node
income_check node
fraud_check node
synthesize_result nodenext superstep, runs after all three complete
8. Production considerations
- Choose reducers deliberately for every field that should accumulate rather than overwrite — forgetting
Annotated[..., add_messages]on a conversation-history field is a common, easy mistake that silently drops history on every node update instead of accumulating it. - Keep state schemas explicit and typed (
TypedDictor Pydantic) rather than using a loosely-typed dict — this is directly Part 1's typed-boundary discipline (Part 1.1, Part 3.2) applied to graph state, and it's what makes a complex graph's data flow reviewable and debuggable. - Design node granularity deliberately — very fine-grained nodes (one LLM call per node) give more observable checkpoints (Part 5.3) and more precise streaming visibility (Part 5.5) at the cost of more graph-definition boilerplate; very coarse-grained nodes are simpler to define but checkpoint/observe less precisely. There's a real trade-off here worth making consciously rather than defaulting to one extreme.
9. Common mistakes
- Forgetting to specify a reducer for a field that needs to accumulate (most commonly
messages), causing state to be silently overwritten instead of building up correctly across nodes. - Treating a node's return value as "the new state" rather than correctly understanding it as a partial update to be merged — leading to confusion about why other state fields seem to "disappear" (they don't — they're preserved by the merge, but a misunderstanding of this model causes real bugs when a node unintentionally returns and thus overwrites a field it didn't mean to touch).
- Building overly coarse-grained nodes (one giant node doing five sequential things) that lose the observability, parallelism, and fine-grained checkpointing benefits the graph model exists to provide.
10. Security considerations
- The shared state object is exactly where Part 9's data-handling considerations concentrate for a graph-based application — if any field in state holds sensitive data, every node that can read state (which, by the shared-state model, is potentially every node) has access to it; scope sensitive data narrowly and deliberately rather than dumping everything into one global shared state by default.
- A checkpointed graph's state (Part 5.3) is persisted — meaning sensitive data in state is now also sensitive data in whatever checkpoint storage you're using, with the same access-control and retention considerations as any other data store (Part 9.6, Part 3.9's memory-storage security discussion applies directly).
11. Performance considerations
- The superstep model's natural parallelism (independent nodes in the same superstep running concurrently) is a real, structural performance benefit over manually sequencing independent LLM/tool calls — but only materializes if your graph structure actually expresses independent nodes as such (parallel edges from a common predecessor) rather than artificially chaining them sequentially.
- Very large, complex state objects passed to every node add serialization/deserialization overhead, particularly relevant when checkpointing (Part 5.3) persists state after every superstep — keep state lean, storing large artifacts (a full retrieved document set) by reference where practical rather than inline in state if size becomes a measured concern.
12. Cost considerations
- No new LLM-token cost mechanism introduced by LangGraph itself — cost is still governed by Part 2.6/3.11's framework, applied per node that makes an LLM call. The graph structure's main cost-relevant contribution is making it easier to see and reason about exactly how many LLM calls a given execution path makes (valuable for the cost auditing Part 7.10 and Part 3.11 argue for).
13. When to use it
Any multi-step LLM application (fixed workflow or dynamic agent, Part 3.7) that needs persistence, resumability, human-in-the-loop pausing, fine-grained streaming, or genuine parallelism between independent steps — which describes most production-grade agent/workflow systems beyond a simple, single-pass LCEL chain (Part 4.1).
14. When NOT to use it
A simple, linear, single-pass pipeline with no need for persistence, pausing, or parallelism (Part 4.1's LCEL chains) doesn't need LangGraph's additional structure — using a plain LCEL chain for a genuinely simple sequential pipeline is the right, simpler choice per Part 3.12's "simplest architecture that could plausibly work" principle.
15. Alternatives and trade-offs
| Approach | Good for | Weak point |
|---|---|---|
| LangGraph (StateGraph) | Complex, stateful, persistent, resumable multi-step workflows/agents | More structure/boilerplate than a simple linear chain needs |
| LCEL chain (Part 4.1) | Simple, linear, single-pass pipelines | No native persistence, pausing, or complex branching/parallelism |
| Hand-rolled state machine | Full control, no framework dependency | Reimplements checkpointing/persistence/streaming from scratch |
16. Practical Python/code example
python
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
class SupportState(TypedDict):
"""Shared state flowing through the support-ticket workflow graph."""
messages: Annotated[list[dict], add_messages]
ticket_category: str | None
async def classify_node(state: SupportState) -> dict:
"""Classifies the latest message into a ticket category, updating only that field."""
category = await classify_ticket(state["messages"][-1]["content"])
return {"ticket_category": category}
async def respond_node(state: SupportState) -> dict:
"""Generates a category-appropriate response, appended via the add_messages reducer."""
response_text = await generate_response(state["ticket_category"], state["messages"])
return {"messages": [{"role": "assistant", "content": response_text}]}
graph = StateGraph(SupportState)
graph.add_node("classify", classify_node)
graph.add_node("respond", respond_node)
graph.set_entry_point("classify")
graph.add_edge("classify", "respond")
graph.add_edge("respond", END)
compiled = graph.compile()
result = await compiled.ainvoke({"messages": [{"role": "user", "content": "I was charged twice"}]})17. Production-quality example
A graph with genuinely parallel independent nodes (the loan-review pattern from section 6), demonstrating the superstep model's real parallelism:
python
from typing import TypedDict
class LoanReviewState(TypedDict):
"""Shared state for a loan application review workflow with independent checks."""
application_id: str
credit_check_result: dict | None
income_check_result: dict | None
fraud_check_result: dict | None
final_recommendation: str | None
async def credit_check_node(state: LoanReviewState) -> dict:
"""Runs the credit check independently, writing only its own result field."""
result = await run_credit_check(state["application_id"])
return {"credit_check_result": result}
async def income_check_node(state: LoanReviewState) -> dict:
"""Runs income verification independently, writing only its own result field."""
result = await run_income_verification(state["application_id"])
return {"income_check_result": result}
async def fraud_check_node(state: LoanReviewState) -> dict:
"""Runs fraud detection independently, writing only its own result field."""
result = await run_fraud_check(state["application_id"])
return {"fraud_check_result": result}
async def synthesize_node(state: LoanReviewState) -> dict:
"""Runs only after all three independent checks have completed, using an LLM
to synthesize a final recommendation from their combined results."""
recommendation = await synthesize_recommendation(
state["credit_check_result"], state["income_check_result"], state["fraud_check_result"]
)
return {"final_recommendation": recommendation}
graph = StateGraph(LoanReviewState)
graph.add_node("credit_check", credit_check_node)
graph.add_node("income_check", income_check_node)
graph.add_node("fraud_check", fraud_check_node)
graph.add_node("synthesize", synthesize_node)
graph.set_entry_point("credit_check") # illustrative; a real fan-out uses multiple entry edges
# Fan-out: all three checks are added as parallel branches from the same predecessor,
# and synthesize_node's incoming edges from all three make it wait for all to complete
# before running — verify exact current fan-out/fan-in edge syntax against current
# LangGraph documentation, as graph-construction APIs are an actively evolving surface.This structure — three independent checks genuinely running in the same superstep, with synthesize_node only becoming active once all three have completed — is the direct, buildable version of the real-world example in section 6, and a concrete illustration of why the superstep execution model matters beyond being an implementation detail.
18. Short exercise
Take the SupportState example from section 16 and add a new field, escalation_count: Annotated[int, some_reducer], that should increment by 1 every time a node marks the ticket for escalation, rather than being overwritten. Write the reducer function this would require, and explain why a plain int field without a custom reducer would behave incorrectly for this purpose.
19. Interview questions
- Explain the partial-update-and-merge model of LangGraph state, and why it's different from how a plain LCEL chain passes data between steps.
- What is a reducer, and why is
add_messagesspecifically necessary for conversation-history fields? - What is a superstep, and how does it enable genuine parallelism between independent nodes?
20. FDE/customer scenario
Customer's engineering lead: "We tried building our loan-review process as a single large LangChain chain, but it's slow because each check waits for the previous one to finish, even though they don't depend on each other."
This is close to a direct diagnostic match for section 4/6/17: a linear LCEL chain forces sequential execution even for logically independent steps, while restructuring the same three checks as parallel LangGraph nodes within one superstep lets them run concurrently, directly reducing wall-clock latency without changing what work is actually being done — a concrete, technically-grounded recommendation that follows directly from understanding the execution model rather than guessing at a fix.
Key takeaways
- LangGraph's state is shared and cumulative: nodes return partial updates, which are merged (via reducers) rather than replacing the whole state — a genuinely different model from linear LCEL chains.
- Reducers (like
add_messages) define how a field's updates combine — forgetting one on an accumulating field is a common, state-corrupting mistake. - The superstep execution model gives genuine, structural parallelism to independent nodes, not something bolted on via manual concurrency code.
Things you should be able to explain
- The partial-update-and-merge state model and why it differs from LCEL's full-output-to-full-input chaining.
- What a reducer does and why
messagesfields specifically need one.
Things you should be able to build
- A StateGraph with both sequential and genuinely parallel (fan-out/fan-in) node structures.
Common mistakes
- Missing reducers on accumulating fields, silently losing data.
- Overly coarse-grained nodes losing observability and parallelism benefits.
Recommended next chapter
02-conditional-routing-reducers.md