LangGraph

LangGraph is LangChain's runtime for building agents as explicit stateful graphs instead of implicit loops. You declare a typed shared state, wire nodes and edges over it, and compile the result into a runtime that executes one graph step at a time, snapshots the state after every step, and lets you stream it, pause it for a human, and rewind it. This chapter is three things at once, a practical tutorial that stands up a real agent in a few lines, a runtime walkthrough that follows one invoke through the Pregel super-step loop from compiled channels to a durable checkpoint, and a staged guide to reading the repository. It ends with hands-on labs, understanding checks with model answers, and a compact framework for keeping the whole system in your head. Throughout it argues one thing, that a graph beats a hidden agent loop precisely because control flow becomes data you can inspect, persist, and change.

Part I: The mental model

StateGraph(State)         a typed state schema (TypedDict / Pydantic / dataclass)
      |
      v
add_node / add_edge       nodes are functions  state -> partial-state update
add_conditional_edges     a router function chooses the next node at run time
      |
      v
graph.compile(            lower the builder into a Pregel runtime (a Runnable)
   checkpointer=...,
   interrupt_before=...)
      |
      v
app.invoke / app.stream   run one super-step at a time (Pregel / bulk-synchronous)
   config={thread_id}
      |
      v
channels + reducers       each state key is a channel, writes merge via a reducer
      |
      v
checkpointer.put()        after every super-step, snapshot the state to the thread
      |
      v
interrupt() / get_state   pause for a human, inspect, time-travel, resume

The one-sentence identity. LangGraph makes an agent an explicit stateful graph, nodes that read and write a typed shared state, edges both fixed and conditional that decide what runs next, executed as a sequence of Pregel super-steps whose every boundary can be checkpointed, streamed, interrupted, and resumed. The classic way to build an agent is an implicit loop, call the model, if it asked for a tool run the tool, feed the result back, repeat until it stops. That loop lives inside a class where you cannot see it, cannot pause it, and cannot persist it. LangGraph turns the loop inside out. The control flow becomes a value you construct, draw, and modify, and the agent's memory becomes a schema rather than a pile of local variables.

Two ideas carry the whole design. First, state is a set of typed channels with reducers. Each key in the state schema is a channel. A node does not mutate state, it returns a small dict naming the keys it wants to change, and each channel's reducer decides how that write merges with what is already there. A plain key keeps only the last write, a key annotated with a reducer accumulates. This is what makes message history, parallel branches, and map-reduce first-class rather than hand-rolled. Second, execution is a series of super-steps and every super-step boundary is a durable checkpoint. Because the state lives in channels and each step end is a snapshot keyed by a thread, persistence, time-travel, and human-in-the-loop are not features bolted on top, they are the same mechanism looked at from three angles.

Everything here is verified against the main branch in July 2026. LangGraph moves quickly and the repository is a monorepo of several packages, so where an exact file path is likely to shift I name the component by its role and say so.

Part II: Using it

LangGraph is a pure-Python library and installs anywhere Python runs, including macOS, because the graph itself does no GPU work, it orchestrates calls to models that live behind an API or a local server. The core package plus the pieces you usually want look like this.

pip install langgraph
# checkpointers for real persistence live in separate packages
pip install langgraph-checkpoint-sqlite
pip install langgraph-checkpoint-postgres
# a model provider through a langchain integration
pip install langchain-openai
# the local dev server and Studio (in-memory backend)
pip install "langgraph-cli[inmem]"

The smallest useful graph is a chatbot with memory. Note the reducer on messages and the checkpointer passed at compile time, they are what make the second turn remember the first.

from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.memory import InMemorySaver  # MemorySaver is the legacy alias
from langchain_openai import ChatOpenAI

class State(TypedDict):
    messages: Annotated[list, add_messages]   # the reducer appends and merges by id

llm = ChatOpenAI(model="gpt-4o-mini")

def chatbot(state: State) -> dict:
    # return only the keys you change, not the whole state
    return {"messages": [llm.invoke(state["messages"])]}

builder = StateGraph(State)
builder.add_node("chatbot", chatbot)
builder.add_edge(START, "chatbot")
builder.add_edge("chatbot", END)

app = builder.compile(checkpointer=InMemorySaver())

config = {"configurable": {"thread_id": "user-1"}}
app.invoke({"messages": [("user", "hi, I am Sam")]}, config)
app.invoke({"messages": [("user", "what is my name?")]}, config)  # answers "Sam"

Two invoke calls share one thread_id, so the checkpointer loads the first turn's messages before the second turn runs. Give a new thread_id and the memory resets, which is exactly how one server holds thousands of independent conversations.

Now make it an agent by adding a tool and a cycle. This is the ReAct loop drawn out by hand, which is the thing LangGraph exists to make visible.

from langgraph.prebuilt import ToolNode, tools_condition
from langchain_core.tools import tool

@tool
def get_weather(city: str) -> str:
    """Return the weather for a city."""
    return f"It is sunny in {city}."

llm_with_tools = llm.bind_tools([get_weather])

def agent(state: State) -> dict:
    return {"messages": [llm_with_tools.invoke(state["messages"])]}

builder = StateGraph(State)
builder.add_node("agent", agent)
builder.add_node("tools", ToolNode([get_weather]))
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", tools_condition)  # -> "tools" or END
builder.add_edge("tools", "agent")                       # the cycle that makes it ReAct
app = builder.compile()

tools_condition reads the last message. If the model emitted a tool call it routes to tools, otherwise to END. The edge from tools back to agent is the loop, and it is a line you can see and change rather than a hidden while. If you want that exact graph in one line, the prebuilt does it, create_react_agent(llm, [get_weather], checkpointer=InMemorySaver()), and it compiles down to the same nodes and edges you just wrote. That constructor is now deprecated in favor of create_agent in the langchain package, which wraps the identical runtime with a middleware system, so the graph underneath is unchanged.

The mistakes newcomers make, in rough order of frequency. First, forgetting the reducer. Declare messages: list without Annotated[list, add_messages] and each write replaces the list instead of appending, and two nodes writing it in one step raise InvalidUpdateError. Second, forgetting thread_id. With a checkpointer set but no thread_id in the config there is no thread to persist to, and memory, resume, and interrupts silently do nothing. Third, returning the whole state from a node instead of a partial update, which fights the reducers. Fourth, mutating the incoming state in place, which the runtime does not expect, always return new values. Fifth, infinite loops, a cyclic graph with no terminating branch trips the recursion limit (25 super-steps by default) and raises GraphRecursionError, raise the limit only when the loop genuinely needs more steps.

Part III: When it is the right tool

The first comparison is with plain LangChain, because the two are constantly confused and they sit at different layers. LangChain proper is a component library, model wrappers, prompt templates, output parsers, retrievers, and LCEL, which pipes Runnables into a directed acyclic chain like prompt | model | parser. LCEL is the right tool for straight-through pipelines with no loops and no durable state. LangChain's legacy AgentExecutor is the implicit loop described above, correct but opaque. LangGraph is the orchestration layer for anything stateful, cyclic, or controllable. As of the langchain 1.0 line the recommended agent constructor, create_agent, is built on LangGraph, and create_react_agent in langgraph.prebuilt is the transitional form. LangGraph did not replace LangChain, it became the runtime underneath LangChain's agents.

Why a graph beats an implicit loop, which is the load-bearing argument of this chapter. You can see the control flow, app.get_graph().draw_mermaid() prints it. Every super-step is a checkpoint, so persistence, resume-after-crash, and time-travel come for free. You can interrupt before a sensitive node for human approval and then resume. You can stream the state after each node or the tokens as they generate. You can add branches, run nodes in parallel by fanning out, and nest a whole graph as a single node, without rewriting a monolith. And you get fault tolerance inside a step, if one node fails the completed nodes' writes are saved as pending writes so a retry does not recompute them. None of this is available when the loop is a private method.

The honest cases for something else. If you have one linear tool-call loop with no persistence, no human step, and no streaming need, LangGraph is overhead, write the while loop against the model SDK directly. LlamaIndex Workflows offers an event-driven, step-function model, a strong choice inside a retrieval-heavy stack. CrewAI is higher-level and role-and-task oriented, faster to stand up and more opinionated, with less control over the exact state machine. Microsoft AutoGen centers on multi-agent conversation. The OpenAI Agents SDK, successor to Swarm, is a lightweight handoff model flavored toward one provider. Temporal and similar durable-execution engines win when your real need is durability and retries of long workflows rather than LLM-specific state, though LangGraph's checkpointer covers much of that within the LLM domain. The rule of thumb, LangGraph earns its complexity when you need durable, inspectable, human-supervised, or multi-actor control, and for a single deterministic prompt-to-answer call an LCEL chain or a raw model call is cleaner.

One architecture note. The models a graph calls have to be served somewhere, and in production those endpoints are frequently high-throughput inference servers like vLLM or SGLang. LangGraph is agnostic to which, a node simply calls a chat-model Runnable, so the orchestration and the serving are cleanly separated concerns. If the agent ideas here are the draw, the self-improving agents and applied generative AI classes cover the surrounding design space.

Part IV: The full life of one invoke

The specimen. One call app.invoke({"messages": [("user", "...")]}, {"configurable": {"thread_id": "t1"}}) on the ReAct graph from Part II, compiled with a checkpointer. I follow it from compilation through the super-steps to the checkpoint on disk.

Stage 1: compile lowers the builder to a Pregel graph

builder.compile() walks the nodes and edges and lowers them into a Pregel object. Each state key becomes a channel, a LastValue channel by default, or a BinaryOperatorAggregate-style channel when the key carries a reducer like add_messages. Each node becomes a PregelNode that subscribes to (reads) certain channels and writes to others. Edges become trigger channels, add_edge("a", "b") arranges for b to fire when a writes, and a conditional edge compiles to a branch that runs your router and writes to the chosen node's trigger. START and END are sentinel endpoints. The compiled graph is a Runnable, so it exposes invoke, stream, batch, and their async twins, and can itself be used as a node inside a larger graph.

Stage 2: enter the loop and load the checkpoint

invoke opens the Pregel loop (conceptually langgraph/pregel/loop.py). It reads thread_id and checkpoint_ns from the config, asks the checkpointer for the latest CheckpointTuple on that thread, and rebuilds channel values from it, or starts empty for a new thread. The input dict is then applied as writes to the input channels, which for a messages graph runs the add_messages reducer to seed state["messages"] with the user turn.

Stage 3: the super-step loop, plan then execute then update

The loop repeats a three-phase cycle borrowed from bulk-synchronous parallel computing.

plan     which nodes are triggered by channels updated last step?
execute  run those nodes concurrently, each on a read-only state snapshot
         (writes are buffered, nodes cannot see each other this step)
update   at the barrier, apply all buffered writes through the reducers,
         bump each channel's version
save     checkpointer.put() the new values + versions + metadata to the thread,
         put_writes() records pending writes for fault tolerance
stream   emit to the caller per stream_mode (values / updates / messages ...)
advance  newly updated channels trigger the next nodes; END or none -> stop

For the ReAct specimen the steps read cleanly. Step one runs agent, the model returns an AIMessage with a tool call, and tools_condition routes to tools. Step two runs the ToolNode, which executes get_weather and appends a ToolMessage, and the edge back to agent re-triggers it. Step three runs agent again, this time the model answers in plain text, tools_condition routes to END, and the loop halts. The important discipline is that nodes never talk to each other directly, they only write channels, and the reducer at the barrier is the only place writes combine. That is what makes two parallel writers to a reducer channel well-defined and two parallel writers to a plain channel an error.

Stage 4: interrupts, if any

If a node calls interrupt(payload), or the graph was compiled with interrupt_before=["tools"], the loop stops at that boundary, writes the interrupt into the checkpoint, and returns control carrying an __interrupt__ marker instead of a final answer. The thread's checkpoint now has a non-empty next, which is precisely what "paused" means. The run can sit there for milliseconds or for days. A later invoke with Command(resume=value) reloads that checkpoint and continues.

Stage 5: return

With no pending interrupt and END reached, the loop reads the final channel values and returns them as the output state. The whole trace, in one breath, input written to channels, a handful of super-steps each snapshotted to the thread, communication between nodes only ever through channel writes merged by reducers, and a durable history you can list, inspect, and replay.

Part V: Internals deep dives

Deep dive: StateGraph, channels, and reducers

The state schema can be a TypedDict (the common case), a Pydantic BaseModel when you want node outputs validated at run time, or a dataclass. Whatever the form, each key becomes a channel, and the channel type is chosen by whether the key is annotated with a reducer. A bare key is a LastValue channel, it keeps only the most recent write and raises if two nodes write it in the same super-step. An Annotated[T, reducer] key becomes an accumulating channel whose reducer combines each write with the running value. operator.add concatenates lists, add_messages appends messages and merges them by id, supports a RemoveMessage sentinel to delete, and coerces (role, content) tuples into proper Message objects. The channel implementations live in langgraph/channels/, and beyond the two staples there are Topic (a pub/sub list), EphemeralValue (visible only to the next step, not carried forward), and managed values such as IsLastStep and RemainingSteps in langgraph/managed/ that the runtime maintains for you.

Two conveniences follow. MessagesState is a prebuilt TypedDict that is nothing but messages: Annotated[list, add_messages], so most chat graphs subclass it and add their own keys. And a graph can declare separate input, output, and internal schemas, so private working state never leaks into the public output. Because layout of memory is a typed schema and merging is a reducer, the graph's state is composable in the same way a well-typed data structure is, which is what lets parallel branches and map-reduce have a defined meaning.

Deep dive: the Pregel runtime

The runtime is named after Google's Pregel graph-processing system, and it inherits Pregel's bulk-synchronous model. Computation is a series of super-steps separated by barriers. Inside a step the active nodes run in parallel and cannot see one another's writes. Between steps every write is applied and the next step's nodes are selected. This is not an implementation detail you can ignore, it is the semantics, and it is why a fan-out of parallel branches all observe the same consistent snapshot and their results land together at the barrier rather than racing. A PregelNode is a bundle of channel subscriptions (its triggers and reads), a bound function, and a set of channel writes. The loop machinery (conceptually pregel/loop.py and pregel/algo.py) computes the next tasks, an executor runs them on a thread pool or the async event loop, and the algorithm applies the writes and bumps versions. The recursion limit, 25 super-steps by default, caps how long a cyclic graph may run before GraphRecursionError, which is the guardrail against an agent that never decides to stop.

Deep dive: the checkpointer, persistence, and time travel

BaseCheckpointSaver (in the langgraph-checkpoint package under langgraph/checkpoint/base.py) defines the contract, put, put_writes, get_tuple, list, and their async forms. A Checkpoint is a versioned snapshot of every channel value. A CheckpointTuple bundles the config, the checkpoint, metadata, the parent config, and any pending writes. A checkpoint is addressed by thread_id, a checkpoint_ns for subgraphs, and a monotonically increasing checkpoint_id, and a thread is just the linear history of one run. Implementations range from InMemorySaver for development to SqliteSaver and AsyncSqliteSaver (langgraph-checkpoint-sqlite) and PostgresSaver and AsyncPostgresSaver (langgraph-checkpoint-postgres) for production. Serialization runs through a SerializerProtocol, default JsonPlusSerializer, which knows how to encode LangChain messages and common Python types.

The pending_writes field is the fault-tolerance trick. If a super-step has three nodes and the second raises, the first node's completed writes are already stored, so on retry only the failed node and the not-yet-run node execute, and no successful work is repeated. Node-level retries are governed by a RetryPolicy. On the inspection side, get_state(config) returns a StateSnapshot with values, next (the nodes queued to run), config, metadata, created_at, parent_config, and tasks, and get_state_history(config) yields every snapshot for the thread, newest first, each carrying its own checkpoint_id in its config.

Time travel falls straight out of that. Pass a past snapshot's config back into invoke or stream and the graph replays from that point. update_state(config, values, as_node=...) writes new values as if a named node had produced them, which forks a new branch of history from the chosen checkpoint. Together they give you inspect, edit, and rewind. Because durable state is addressed by thread and logical checkpoint id rather than by a live process, you can crash, move machines, and resume, and you can branch the past to explore a different continuation, which an implicit loop simply cannot do.

Deep dive: streaming

stream and astream take a stream_mode. The modes that matter, "values" emits the entire state after each step, "updates" emits just the {node: update} delta from each node, "messages" emits LLM token chunks as (chunk, metadata) tuples for token-by-token UIs, "custom" emits whatever a node chooses to push, and "debug" emits verbose step events. Pass a list of modes and you receive (mode, data) tuples so you can drive several consumers at once.

for chunk in app.stream({"messages": [("user", "weather in Paris?")]},
                        config, stream_mode="updates"):
    print(chunk)   # {'agent': {...}} then {'tools': {...}} then {'agent': {...}}

To surface progress from inside a node, accept a StreamWriter parameter or call get_stream_writer() and push events that appear under stream_mode="custom". For deep instrumentation astream_events gives the fine-grained Runnable event stream, on_chat_model_stream, on_chain_start, and the rest, at the cost of volume. The tidy part is that streaming reads the same channel state the checkpointer snapshots, so what you show a user and what you persist are one source of truth.

Deep dive: human-in-the-loop and interrupts

There are two mechanisms. The static one, compile(interrupt_before=[...], interrupt_after=[...]), pauses at fixed nodes, which is the natural way to place a review gate right before a tool runs. The dynamic one, interrupt(payload) called inside a node, pauses anywhere and hands payload back to the caller. Both require a checkpointer, because a pause is nothing more than a checkpoint whose next is non-empty. The run returns with an __interrupt__ value carrying the payload, and the thread sits durably paused.

from langgraph.types import interrupt, Command

def review(state: State) -> dict:
    decision = interrupt({"proposed": state["draft"]})   # pauses here
    return {"approved": decision == "yes"}

# ... run until it pauses, inspect, then resume:
app.invoke(Command(resume="yes"), config)

Resuming with Command(resume=value) reloads the checkpoint and continues. On resume the interrupting node re-executes from its start, but interrupt() returns the supplied value instead of pausing again, using the stored resume value so an already-answered interrupt does not fire twice. The practical corollary, any side effect that sits before an interrupt() in the same node runs a second time on resume, so keep interrupts early in a node. Common patterns are approve or reject a tool call, edit the proposed arguments before it runs, or ask the human a clarifying question mid-run, and because you can also update_state before resuming, a reviewer can correct the agent's plan and let it proceed.

Deep dive: routing, Command, and the Send API

Conditional edges are the everyday router, add_conditional_edges(source, router, path_map) where router(state) returns the next node's name, a list of names to fan out, or END, and the optional path_map makes the diagram and the intent readable. A node can also skip the separate edge and return a Command, Command(update={...}, goto="next") both updates state and routes in one move, Command(goto=Send(...)) fans out, and Command(graph=Command.PARENT, goto=...) steps from a subgraph up into its parent.

The Send API is how map-reduce works. A router returns a list of Send objects, one per subtask, each carrying its own private input, and each spawns the target node as its own task in the same super-step.

from langgraph.types import Send

def fan_out(state) -> list:
    # invoke "summarize" once per document, each with its own input
    return [Send("summarize", {"doc": d}) for d in state["docs"]]

builder.add_conditional_edges("split", fan_out, ["summarize"])

All those summarize tasks run in parallel and their writes fan back into a reducer channel at the barrier, so you map a variable-length list of subtasks over one node and gather the results without touching a thread pool yourself. This is the payoff of the bulk-synchronous model, fan-out and fan-in have a precise meaning because the barrier defines when results combine.

Deep dive: prebuilt and the functional API

The langgraph.prebuilt package ships the shortcuts. create_react_agent builds a ready ReAct graph, an agent node plus a ToolNode wired by tools_condition. ToolNode executes the tool calls found in the last AIMessage and appends the resulting ToolMessages, with error handling. tools_condition is the standard route-to-tools-or-end branch. InjectedState and InjectedStore let a tool receive the live graph state or the long-term store without the model having to supply them as arguments. As noted, create_react_agent is deprecated in favor of create_agent in langchain, which is the same runtime with a middleware layer, so it stays a good thing to read even as the entry point moves.

For teams that prefer imperative code to a graph object, langgraph.func offers the functional API. @task marks a unit of work that is checkpointed and retryable, and @entrypoint(checkpointer=...) marks the top-level function, inside which you call tasks, receive futures, and use interrupt() exactly as in the graph API. It trades the explicit graph for plain Python control flow while keeping persistence, streaming, and human-in-the-loop, because under the hood it still compiles to Pregel. Separately, long-term memory that outlives any single thread lives in a BaseStore (InMemoryStore, or a Postgres-backed store), which holds namespaced key-value memory with put, get, and search, optionally with embeddings for semantic recall. Pass store=... to compile() and a node or a tool can read and write user-level memory across threads.

Part VI: Reading the repository

LangGraph is a monorepo of packages under libs/, langgraph (the core), checkpoint (base saver interfaces plus the in-memory saver and the store), checkpoint-sqlite, checkpoint-postgres, prebuilt, cli, and the SDKs (sdk-py, sdk-js). All paths below verified on main in July 2026 and may drift as the monorepo evolves.

Stage 0, orientation. Read the top-level README, then the concept docs on the low-level graph API, persistence, streaming, and human-in-the-loop, and run one example against the local dev server with langgraph dev. Questions to hold, what is a channel, what is a thread, and what does compile() return.

Stage 1, the graph builder. libs/langgraph/langgraph/graph/, the state builder (state.py, where schema keys are lowered to channels), the message helpers (message.py, add_messages and MessagesState), and the base graph and branch (graph.py). Questions, how does a state key become a channel, and how does add_conditional_edges compile to a branch that writes a trigger channel.

Stage 2, the runtime. libs/langgraph/langgraph/pregel/, the Pregel class and the loop, read side by side with langgraph/channels/ (LastValue, the binary-operator aggregate, Topic, EphemeralValue) and constants.py and types.py (START, END, Send, Command, interrupt, StateSnapshot). Questions, what selects the next tasks, where are writes applied, and where is the checkpoint written.

Stage 3, persistence. libs/checkpoint/langgraph/checkpoint/, base.py (BaseCheckpointSaver, Checkpoint, CheckpointTuple), the serializer, and the in-memory saver, then libs/checkpoint-sqlite and libs/checkpoint-postgres for the real backends, and langgraph/store/ for long-term memory. Questions, what exactly is in a Checkpoint, and how do pending_writes make resume-after-failure correct.

Stage 4, streaming and interrupts. The streaming path in langgraph/pregel/ and the langgraph/stream/ package, plus how interrupt() raises, how the loop catches it and writes it into the checkpoint, and how Command(resume=...) feeds it back.

Stage 5, prebuilt and functional. libs/prebuilt (create_react_agent, ToolNode, tools_condition) and libs/langgraph/langgraph/func (the @entrypoint and @task decorators).

Where not to start, the LangGraph Platform server, the SDKs, and the CLI are deployment surface and read best after the runtime is clear, and the managed-value and cache machinery is worth meeting only once the plain channel story is solid.

Part VII: Hands-on labs

None of these need a GPU, only an API key for a chat model (or a local model behind an OpenAI-compatible endpoint). Log and output shapes vary with the fast pace of main.

Lab 1: memory across turns. Concept, threads and checkpoints.

Build the two-line chatbot from Part II with an InMemorySaver. Invoke twice on one thread_id and confirm the second turn remembers the first. Invoke a third time on a fresh thread_id and watch the memory reset. Then call app.get_state(config) and read the StateSnapshot.

Lab 2: hand-built ReAct versus prebuilt. Concept, the explicit loop.

print(app.get_graph().draw_mermaid())   # the agent/tools/tools_condition cycle
# now swap in the prebuilt and compare the drawing:
from langgraph.prebuilt import create_react_agent
agent = create_react_agent(llm, [get_weather])
print(agent.get_graph().draw_mermaid())

Confirm the two graphs behave identically on the same input, and notice that the prebuilt is the same nodes and edges you wrote by hand. This is the whole thesis in one lab, the shortcut lowers to a graph you could have drawn.

Lab 3: time travel. Concept, checkpoints as history.

Run a multi-step graph, then iterate app.get_state_history(config) and print each snapshot's next and checkpoint_id. Pick an earlier snapshot's config and pass it back to invoke to replay from there. Then call update_state to edit one value and resume, and watch the run take a different path from the fork.

Lab 4: a human approval gate. Concept, static interrupts.

app = builder.compile(checkpointer=InMemorySaver(),
                      interrupt_before=["tools"])
app.invoke({"messages": [("user", "delete all files")]}, config)
# returns with __interrupt__ before the tool runs
print(app.get_state(config).next)      # ('tools',)  -- paused
app.invoke(None, config)               # approve and continue

Inspect the proposed tool call through get_state, then either resume with invoke(None, config) to approve, or update_state to edit the arguments first and only then resume.

Lab 5: dynamic interrupt and resume. Concept, interrupt/resume.

Put value = interrupt({"question": "confirm?"}) inside a node, run until it pauses, read the payload from the returned __interrupt__, and resume with app.invoke(Command(resume="yes"), config). Add a print before the interrupt() and observe it fire twice, which teaches why side effects belong after the interrupt.

Lab 6: streaming modes. Concept, what to show a user.

Stream the ReAct graph with stream_mode="updates", then "values", then "messages", and compare what each yields. Add a StreamWriter progress event inside a node and read it back under stream_mode="custom".

Lab 7: map-reduce with Send. Concept, fan-out and fan-in.

Give the state a docs list and a summaries: Annotated[list, operator.add] channel. Write a router that returns one Send("summarize", {"doc": d}) per document, and confirm all summaries arrive gathered in summaries after a single super-step.

Part VIII: Questions and model answers

Understanding checks. Answer aloud before reading.

1. What is LangGraph in one sentence?

A runtime that models an agent as an explicit stateful graph, nodes that read and write a typed shared state over fixed and conditional edges, executed as bulk-synchronous Pregel super-steps whose every boundary can be checkpointed, streamed, interrupted, and resumed.

2. How is the graph's state actually stored, and what does a reducer do?

Each key of the state schema is a channel. A plain key is a LastValue channel that keeps the most recent write, a key annotated with a reducer becomes an accumulating channel. A node returns a partial dict of the keys it changes, and at the super-step barrier each channel's reducer merges the writes, so add_messages appends messages while a bare key would overwrite.

3. Why does a graph beat an implicit agent loop?

Because control flow becomes data. You can draw it, checkpoint every step for persistence and time-travel, pause it for a human and resume, stream intermediate state, fan out and nest subgraphs, and recover from a mid-step failure using pending writes. A loop hidden inside a class offers none of these.

4. What is a super-step, and why does the barrier matter?

A super-step is one plan-execute-update cycle. The nodes selected in the plan phase run in parallel on a consistent snapshot and cannot see each other's writes, and only at the barrier are all writes applied through reducers and a checkpoint taken. The barrier is what gives fan-out and fan-in a precise meaning and makes each step a durable, resumable unit.

5. What is in a checkpoint, and why can a run resume on another machine?

A checkpoint is a versioned snapshot of every channel value plus metadata, addressed by thread_id, checkpoint_ns, and checkpoint_id. Because durable state is keyed by thread and logical id rather than by a live process, any machine with access to the checkpointer can load the latest tuple and continue.

6. How does human-in-the-loop work, and what does it require?

A pause is a checkpoint whose next is non-empty. Either compile with interrupt_before or interrupt_after, or call interrupt(payload) inside a node. The run returns with __interrupt__ and sits durably paused, and Command(resume=value) continues it. Both require a checkpointer, since there is nowhere to pause without one.

7. On resume, why can a side effect run twice?

The interrupting node re-executes from its start on resume, and interrupt() returns the stored resume value instead of pausing. Any code before the interrupt therefore runs a second time, so side effects should sit after the interrupt or be made idempotent.

8. What are the main streaming modes?

"values" for the full state after each step, "updates" for per-node deltas, "messages" for LLM token chunks with metadata, "custom" for data a node pushes through a StreamWriter, and "debug" for verbose events. A list of modes yields (mode, data) tuples.

9. What does the Send API give you that a conditional edge does not?

A conditional edge routes to one of a fixed set of nodes. A router returning a list of Send objects invokes the same node many times in one super-step, each with its own private input, which is map-reduce. The results fan back into a reducer channel at the barrier.

10. How do LangGraph and LangChain relate now?

LangChain is the component library and LCEL for acyclic pipelines, LangGraph is the stateful, cyclic orchestration runtime. In the current langchain line the recommended agent constructor create_agent is built on LangGraph, so LangGraph became the runtime underneath LangChain's agents rather than a competitor to them.

11. When is LangGraph the wrong tool?

For a single deterministic prompt-to-answer call, where an LCEL chain or a raw model call is cleaner, and for a lone linear tool-call loop with no persistence, no human step, and no streaming, where a plain while loop is less machinery. LangGraph earns its weight only when you need durable, inspectable, supervised, or multi-actor control.

12. A cyclic agent runs forever. What stops it, and what should you check?

The recursion limit, 25 super-steps by default, raises GraphRecursionError. The real fix is a terminating branch, a tools_condition-style router that routes to END once the model stops asking for tools, rather than simply raising the limit.

Part IX: Design lessons

Make control flow data, not code. An agent's loop is a graph you build, draw, and edit, not a private while. Once the flow is a value, you can inspect it, version it, and change it at run time, which is the same move as representing a workflow as a DAG in a scheduler or a state machine as a table rather than nested conditionals.

Represent shared memory as a typed schema with reducers. Turning state into channels with merge rules makes concurrency, accumulation, and map-reduce well-defined instead of ad hoc. This is the same win as CRDTs in collaborative editing and reducers in a front-end store, put the merge rule next to the data and composition becomes checkable.

One barrier, one checkpoint. Because every super-step ends in a durable snapshot, persistence, resume-after-crash, and time-travel are one mechanism rather than three subsystems. Systems that make their unit of progress durable by construction, write-ahead logs and event-sourced aggregates among them, get recovery almost for free.

Address state by thread and logical id, not by process. A checkpoint keyed by thread and checkpoint id can be loaded anywhere, so a run survives a crash or a migration and can even be forked to explore an alternate past. Never let the running process become the only home of important state.

Unify persistence, streaming, and human-in-the-loop. All three read the same channel state at the same step boundaries, so they are three views of one truth rather than three features that can disagree. Designing one mechanism to serve several needs beats three mechanisms that must be kept consistent.

Prebuilt is a shortcut, not a ceiling. create_react_agent lowers to a graph you could have written by hand, so the easy path and the fully custom path are the same substrate. Good frameworks make the convenient thing a thin layer over the general thing, so you never hit a wall when you outgrow the default.

Part X: Memorization framework

The one-sentence summary. LangGraph compiles a typed state schema into channels and a set of nodes into a Pregel graph, runs it one bulk-synchronous super-step at a time, merges each step's writes through reducers, and checkpoints the result to a thread, and out of that single mechanism fall streaming, time-travel, human-in-the-loop, and fault tolerance.

StateGraph(State) -> add_node / add_edge / add_conditional_edges
  -> compile(checkpointer, interrupt_before) -> a Pregel Runnable
  -> invoke/stream(config={thread_id})
  -> super-step: plan -> execute (parallel) -> update (reducers) -> checkpoint
  -> interrupt() pauses, Command(resume=...) continues
  -> END: read channels, return state

The chain mapped to source:

builder           libs/langgraph/langgraph/graph/ (state.py, message.py, graph.py)
channels          libs/langgraph/langgraph/channels/ (LastValue, aggregate, Topic)
runtime           libs/langgraph/langgraph/pregel/ (the Pregel loop, super-steps)
types/constants   langgraph/types.py, constants.py (Send, Command, interrupt, START/END)
checkpoint        libs/checkpoint/langgraph/checkpoint/ (BaseCheckpointSaver, Checkpoint)
store             libs/checkpoint/langgraph/store/ (BaseStore, long-term memory)
prebuilt          libs/prebuilt (create_react_agent, ToolNode, tools_condition)
functional        libs/langgraph/langgraph/func (@entrypoint, @task)

Memorize these blocks:

  • State is channels: each schema key is a channel, plain keys keep the last write, Annotated[T, reducer] keys accumulate, nodes return partial updates that merge at the barrier.
  • Execution is super-steps: plan the triggered nodes, execute them in parallel on a snapshot, update all channels through reducers at the barrier, checkpoint, advance. Recursion limit 25 by default.
  • A checkpoint is a thread snapshot: versioned channel values keyed by thread_id and checkpoint_id, with pending_writes for resume-after-failure, which is why runs resume anywhere and history can be replayed and forked.
  • Interrupts need a checkpointer: static interrupt_before/after or dynamic interrupt(), resume with Command(resume=...), and side effects before an interrupt run twice.
  • Prebuilt lowers to a graph: create_react_agent is an agent node plus a ToolNode wired by tools_condition, now transitional toward langchain's create_agent.

Part XI: Papers and further reading

The ideas in this walkthrough trace back to a short list of papers, and each one rewards a direct read. Where this site covers the same ground in depth, the companion link points there.

  1. Yao et al., ReAct, Synergizing Reasoning and Acting in Language Models, 2022. The reason-act-observe cycle that the agent-tools graph of Part II draws out as explicit nodes and edges.
  2. Malewicz et al., Pregel, A System for Large-Scale Graph Processing, SIGMOD 2010. The graph-processing system the runtime is named after, vertices computing in super-steps and communicating only between them.
  3. Valiant, A Bridging Model for Parallel Computation, 1990. The bulk-synchronous parallel model behind the plan-execute-update loop and its barrier.
  4. Schick et al., Toolformer, Language Models Can Teach Themselves to Use Tools, 2023. An early demonstration that a model can decide when to call a tool, the ability tools_condition routes on.
  5. Shinn et al., Reflexion, Language Agents with Verbal Reinforcement Learning, 2023. Agents that critique their own attempts across retries, a pattern that leans on exactly the durable per-thread state the checkpointer provides. The self-improving agents class on this site surveys this line.
  6. Wang et al., Plan-and-Solve Prompting, Improving Zero-Shot Chain-of-Thought Reasoning by Large Language Models, 2023. A root of the plan-then-execute agent shape that planner and executor graphs implement, and the GPT Researcher walkthrough reads a working planner-executor agent end to end.
  7. Wu et al., AutoGen, Enabling Next-Gen LLM Applications via Multi-Agent Conversation, 2023. The conversation-centered alternative weighed in Part III, covered in the AutoGen walkthrough.
  8. Shapiro et al., Conflict-Free Replicated Data Types, 2011. The formal version of putting the merge rule next to the data, which is the design lesson the reducer channels echo.

Part XII: Final takeaway

If the model-calling and tool-calling pieces underneath a node are the gap, the applied generative AI and self-improving agents classes build them up, and the serving layer a production node talks to is the subject of the vLLM and SGLang chapters. Then come back and read one compiled graph again, watch a single invoke take a few super-steps, checkpoint each one, and pause for a human, and the agent will stop feeling like a mysterious loop and start reading like the state machine it always was.

Key takeaway: LangGraph's bet is that an agent is better expressed as an explicit graph over a typed state than as a hidden loop. Make the control flow data, make the memory a schema of channels with reducers, and run it as bulk-synchronous super-steps that checkpoint after every step, and persistence, streaming, time-travel, and human-in-the-loop stop being separate features and become the same mechanism, which is exactly the control an implicit agent loop can never give you.