DS DevShelfHub Projects · AI tools
Cheatsheets / LangGraph
Cheatsheet · AI frameworks

LangGraph Cheatsheet: StateGraph, Nodes and Checkpoints

By DevShelfHub

LangGraph is LangChain's stateful-graph orchestration layer for building production-grade AI agents: you define a StateGraph of nodes (Python functions), connect them with typed edges and conditional routing, and attach a checkpointer for persistence and human-in-the-loop interrupts. This cheatsheet covers StateGraph setup, nodes, edges, reducers, checkpointers, streaming, interrupt/resume, subgraphs, and common multi-agent patterns.

110 items 8 min Graphs State Checkpoints

LangGraph is the stateful graph orchestration layer built on top of LangChain, designed for production-grade AI agents that need more than a single chain. Where LangChain's LCEL handles linear pipelines, LangGraph lets you define a StateGraph — a directed graph of nodes (plain Python functions) connected by typed edges. The shared state object flows through nodes, each of which reads and writes to it; reducers control how values accumulate (e.g., appending messages vs. overwriting a counter). Conditional edges let you route between nodes based on state, enabling ReAct loops, retry cycles, and branching workflows.

The killer feature is the checkpointer: every step is persisted to a backend (memory for development, SQLite or Postgres for production), giving you pause-and-resume, time-travel debugging, and human-in-the-loop interrupts. An interrupt pauses the graph before or after a node and waits for external input before the run resumes — essential for approval workflows, data-entry corrections, or any scenario where an agent must wait for a human decision. Subgraphs let you compose multiple StateGraphs into larger systems with isolated state namespaces.

LangGraph ships with a LangGraph Platform (managed hosting with built-in queuing, resumption, and streaming), but the open-source library is fully self-hostable. All graph runs support streaming at the node level and at the LLM token level simultaneously, so the frontend can show progress as the agent reasons. This cheatsheet covers the daily LangGraph surface: StateGraph construction, nodes, edges, reducers, checkpointers, streaming modes, interrupt/resume, subgraph composition, and the prebuilt ReAct agent pattern.

Start hereQuick start · 6 you’ll reach for daily

Build graphg = StateGraph(State)
Add nodeg.add_node("name", fn)
Routeg.add_conditional_edges("a", router)
Compileapp = g.compile(checkpointer=…)
Runapp.invoke(state, config)
Pauseinterrupt(value) → Command(resume=…)

Target versions · paceVersions

Targets: langgraph ≥ 0.2 langchain-core ≥ 0.3 langgraph-checkpoint-* (split packages) python ≥ 3.9

LangGraph sits under LangChain — messages, models, tools come from langchain-core. The graph and checkpointer primitives are new. Checkpointers ship in separate packages now (langgraph-checkpoint-sqlite, -postgres) — the in-memory one stays in the core package. LangGraph Platform is the hosted runtime, but everything in this sheet runs locally.

Install · envSetup

bash
# Core
pip install langgraph              # ≥ 0.2
pip install langchain-core         # message types

# Checkpointers — split into separate packages
pip install langgraph-checkpoint-sqlite     # SqliteSaver
pip install langgraph-checkpoint-postgres   # PostgresSaver

# Prebuilt agents
pip install langgraph-prebuilt     # create_react_agent

# env (whichever model you bind)
export OPENAI_API_KEY=sk-...
export ANTHROPIC_API_KEY=sk-...

Where things liveCommon imports

from langgraph.graph import StateGraph, MessagesState, START, ENDGraph primitives + the prebuilt message-list state.
from langgraph.graph.message import add_messagesReducer that appends messages and dedupes by id.
from langgraph.prebuilt import ToolNode, create_react_agentDrop-in tool executor and full ReAct agent factory.
from langgraph.checkpoint.memory import MemorySaverIn-process checkpointer. Dev only.
from langgraph.checkpoint.sqlite import SqliteSaverSQLite checkpointer. Single-node prod.
from langgraph.checkpoint.postgres import PostgresSaverPostgres checkpointer. Multi-node prod.
from langgraph.types import interrupt, Command, SendPause primitive, resume command, dynamic fan-out.
from langgraph.constants import START, ENDEntry / exit sentinels. Re-exported from graph.

Schema + reducersState

Every graph has a state schema. Field type is the data; Annotated[type, reducer] sets how concurrent updates merge.

class State(TypedDict): messages: listPlain TypedDict schema. Updates replace by default.
Annotated[list, add_messages]Append messages, dedupe by id, handle RemoveMessage.
Annotated[int, operator.add]Sum numeric updates.
Annotated[list, operator.add]Concatenate lists.
Annotated[set, operator.or_]Set union.
MessagesStatePrebuilt: {messages: Annotated[list, add_messages]}.
from pydantic import BaseModel; class State(BaseModel): …Pydantic schema instead of TypedDict — validation on every update.
return {"counter": 1}Nodes return a partial dict. Only updated keys merge.

Worked example

python
from typing import Annotated, TypedDict
from operator import add
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages

# State schema — Annotated[type, reducer] for merge semantics
class State(TypedDict):
    messages: Annotated[list, add_messages]    # append, dedupe by id
    counter:  Annotated[int,  add]              # numeric sum
    flags:    list[str]                         # default = replace

def step_one(state: State) -> dict:
    return {"counter": 1, "flags": ["seen"]}

def step_two(state: State) -> dict:
    return {"counter": state["counter"] + 1}

graph = StateGraph(State)
graph.add_node("one", step_one)
graph.add_node("two", step_two)
graph.add_edge(START, "one")
graph.add_edge("one", "two")
graph.add_edge("two", END)

app = graph.compile()
print(app.invoke({"messages": [], "counter": 0, "flags": []}))

Graph wiringNodes & edges

Nodes

g.add_node("name", fn)A node is fn(state) -> dict (or async).
async def node(state): …Async nodes work the same. Compile picks the right runtime.
g.add_node("name", fn, metadata={"tag":"v1"})Attach metadata for tracing / filters.
g.add_node("name", runnable)Any LangChain Runnable works as a node.
return Command(update={…}, goto="next")Update state and jump in one return. Skips routing.

Edges

g.add_edge(START, "first")Static entry edge.
g.add_edge("a", "b")Static transition.
g.add_edge("a", END)Terminate after node a.
g.add_conditional_edges("a", router_fn)Router returns next node name (or list).
g.add_conditional_edges("a", router, {"y":"yes","n":"no"})Map router output to node names.
return [Send("worker", arg) for arg in items]Dynamic fan-out — one node invocation per item.
g.set_entry_point("first")Legacy Use add_edge(START, …) instead.

Conditional routing

python
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI

@tool
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b

llm = ChatOpenAI(model="gpt-4o-mini").bind_tools([add])

def call_model(state: MessagesState) -> dict:
    return {"messages": [llm.invoke(state["messages"])]}

# Router: tool_use → run tools, else stop
def route(state: MessagesState) -> str:
    last = state["messages"][-1]
    return "tools" if last.tool_calls else END

graph = StateGraph(MessagesState)
graph.add_node("agent", call_model)
graph.add_node("tools", ToolNode([add]))
graph.add_edge(START, "agent")
graph.add_conditional_edges("agent", route, ["tools", END])
graph.add_edge("tools", "agent")

app = graph.compile()
out = app.invoke({"messages": [HumanMessage("What is 17 + 25?")]})
print(out["messages"][-1].content)

Turn the graph into a runnableCompile & run

app = g.compile()Stateless. No persistence between calls.
app = g.compile(checkpointer=saver)Stateful. Requires thread_id at call time.
app = g.compile(interrupt_before=["review"], interrupt_after=[…])Static interrupts at given nodes. Use for human-in-the-loop.
app = g.compile(debug=True)Verbose execution logs.
app.invoke(input, config)Block until END. Returns final state.
await app.ainvoke(…)Async variant.
app.batch([i1, i2], config)Parallel runs.
config = {"configurable": {"thread_id": "u1"}, "recursion_limit": 25}Required when checkpointer is set. Bound on supersteps.
app.get_graph().draw_mermaid()Render the graph as Mermaid text.

Updates as they happenStreaming

for chunk in app.stream(input, config): …Default mode: yields each node’s output as {node_name: update}.
app.stream(…, stream_mode="updates")Preferred Per-node deltas. The 90% case.
stream_mode="values"Full state after each step. Heavier payload.
stream_mode="messages"Token-level streaming from any LLM in the graph.
stream_mode="debug"Tasks, checkpoints, edges — firehose. Debug only.
stream_mode=["updates","messages"]Multiple modes — chunks are tagged tuples.
app.astream_events(input, version="v2")LangChain runnable events with node metadata attached.

Persistence · threadsCheckpointers

MemorySaver()In-process. Lost on restart. Dev only.
SqliteSaver.from_conn_string("checkpoints.db")Single-process durable. Context manager — opens a connection.
async with AsyncSqliteSaver.from_conn_string(…) as saver:Async SQLite variant.
PostgresSaver.from_conn_string("postgresql://…")Multi-node. Call .setup() once to create tables.
app.get_state(config)Read current state for a thread.
app.get_state_history(config)Iterator over historical checkpoints — time travel.
app.update_state(config, {"messages":[…]})Patch state from outside the graph.
app.update_state(config, …, as_node="agent")Pretend the update came from a specific node — affects routing.
app.invoke(None, config)Resume from the last checkpoint without new input.

Pause · review · resumeHuman-in-the-loop

interrupt({"proposal": …})Preferred Dynamic interrupt from inside a node. Resume returns the human’s value.
compile(interrupt_before=["review"])Legacy Static pause point. Use interrupt() in new code.
result["__interrupt__"]Value the node interrupted with. Show this to the human.
app.invoke(Command(resume="edited"), config)Resume with the human’s answer.
Command(update={"messages":[…]}, resume=…)Resume and patch state in one go.
app.update_state(config, {…}, as_node="review")Edit state during a pause — e.g., correct a draft before resume.

Worked example

python
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import interrupt, Command

def draft(state: MessagesState) -> dict:
    return {"messages": [{"role": "assistant", "content": "Proposed reply: Yes."}]}

def human_review(state: MessagesState) -> dict:
    # Pauses the graph. Resume by passing a Command(resume=...) value.
    edited = interrupt({"proposal": state["messages"][-1].content})
    return {"messages": [{"role": "assistant", "content": edited}]}

graph = StateGraph(MessagesState)
graph.add_node("draft", draft)
graph.add_node("review", human_review)
graph.add_edge(START, "draft")
graph.add_edge("draft", "review")
graph.add_edge("review", END)

app = graph.compile(checkpointer=MemorySaver())
cfg = {"configurable": {"thread_id": "t1"}}

# First run pauses at the interrupt
result = app.invoke({"messages": []}, cfg)
print(result["__interrupt__"])    # value the node interrupted with

# Resume with the human's edit
final = app.invoke(Command(resume="Approved with edits."), cfg)
print(final["messages"][-1].content)

Compose · reuseSubgraphs & prebuilt

Subgraphs

parent.add_node("child", child_graph.compile())Compiled graph used as a node. State keys merge by name.
parent.add_node("child", child, input_schema=…, output_schema=…)Disjoint state shape — map at the boundary.
app.stream(…, subgraphs=True)Surface events from nested graphs.
multi_agent_supervisor(…)Common pattern: supervisor routes to specialist subgraphs.

Prebuilt

create_react_agent(model, tools, prompt=…)Full ReAct loop in one call. Returns compiled app.
ToolNode(tools)Drop-in node that runs tool_calls from the last message.
tools_condition(state)Router fn: returns "tools" if last message has tool calls.
create_react_agent(…, checkpointer=…, interrupt_before=["tools"])Same agent, with persistence and pre-tool review.

Tool-using agent · ~35 linesEnd-to-end · Stateful tool agent

Hand-rolled LangGraph agent with a tool, SQLite checkpointer, streaming, and a thread id. Replace search with your real tool and you have a working stateful bot.

python
from typing import Annotated, TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode
from langgraph.checkpoint.sqlite import SqliteSaver
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI

class State(TypedDict):
    messages: Annotated[list, add_messages]

@tool
def search(q: str) -> str:
    """Stub web search."""
    return f"top result for '{q}': LangGraph is a state-machine library."

llm = ChatOpenAI(model="gpt-4o-mini").bind_tools([search])

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

def needs_tool(state: State) -> str:
    return "tools" if state["messages"][-1].tool_calls else END

g = StateGraph(State)
g.add_node("agent", agent)
g.add_node("tools", ToolNode([search]))
g.add_edge(START, "agent")
g.add_conditional_edges("agent", needs_tool, ["tools", END])
g.add_edge("tools", "agent")

with SqliteSaver.from_conn_string("checkpoints.db") as saver:
    app = g.compile(checkpointer=saver)
    cfg = {"configurable": {"thread_id": "user_42"}}
    for chunk in app.stream(
        {"messages": [HumanMessage("What is LangGraph?")]}, cfg,
    ):
        print(chunk)

Best practiceGood to know

Reducers replace by default. A node returning {"items":[x]} overwrites the list. Use Annotated[list, operator.add] or add_messages when you want append semantics.
Prefer dynamic interrupt() over static interrupt_before. Dynamic pauses can carry payload to the human and survive across distributed workers; static pauses are a coarser tool.
Command(update=…, goto=…) is faster than routing. When a node already knows where it wants to go, return a Command and skip the conditional-edge round trip.

Common trapsWatch out for

Missing thread_id with a checkpointer = silent statelessness. The graph runs but starts from a fresh thread every call. Always set configurable.thread_id.
Recursion limit hits on routing loops. Default is 25 supersteps. An agent that keeps calling tools without progress will hit it. Tune recursion_limit or add a termination signal in your router.
SqliteSaver is a context manager, not a value. Use with SqliteSaver.from_conn_string(…) as saver:. Storing the returned object outside the block leaves a closed connection.

Go deeperSee also

LangGraph FAQ

What is LangGraph?

LangGraph is a graph-based agent orchestration library built on LangChain. It models workflows as directed graphs where nodes are Python functions or LLM calls, edges control routing, and a shared TypedDict state is passed between nodes. Unlike linear chains, LangGraph supports cycles, branching, and long-running agents with memory via checkpointers.

What is a StateGraph in LangGraph?

StateGraph is the core graph class. You define a TypedDict schema for shared state, add nodes (functions that read and write state), connect them with edges or conditional edges, then compile the graph with graph.compile(). The compiled graph is a Runnable that accepts an initial state dict and streams or returns the final state after execution.

How do checkpoints work in LangGraph?

A checkpointer (MemorySaver, SqliteSaver, PostgresSaver) serialises the full graph state after every node execution. Pass it to graph.compile(checkpointer=...) and supply a thread_id in the config to resume a specific conversation. Checkpoints enable long-running agents, time-travel debugging, and fault recovery without re-running completed steps.

What is human-in-the-loop in LangGraph?

Human-in-the-loop pauses graph execution at a designated node so a human can review, approve, or correct the agent output before it continues. Set interrupt_before=[node_name] in compile() to pause before a node, then resume with graph.invoke(None, config=thread_config) after the human has taken action. The agent resumes from the checkpoint without replaying prior steps.

What is the difference between LangChain and LangGraph?

LangChain provides model wrappers, prompt templates, retrievers, and simple sequential chains. LangGraph adds a stateful graph execution engine for cyclic, branching, and multi-agent workflows with explicit state schemas and checkpointing. Most new complex agent work starts with LangGraph rather than the legacy LangChain AgentExecutor class.