← Stack

LangGraph

Stateful agent graphs: cycles, checkpoints, human-in-the-loop.

Overview

LangGraph is the low-level orchestration framework and runtime for building stateful, long-running agents from LangChain. The core abstraction is a directed graph: nodes are functions that receive the current state and return partial updates; edges determine which node runs next. The runtime (Pregel) executes supersteps, running eligible nodes in parallel within each step.

The differentiator is durability. Via a checkpointer (Postgres in production), every superstep is snapshotted. If the process crashes at 3am, it resumes from the last stable checkpoint, without restarting from scratch. This makes LangGraph the correct choice for pipelines that run more than a few seconds, interact with external APIs, or need a human approval step.

LangGraph 1.0 shipped on 22 October 2025, simultaneous with LangChain 1.0. Semver commitment: no breaking changes before 2.0. The platform layer (managed hosting) reached GA on 14 May 2025 under the name LangGraph Platform, later rebranded LangSmith Deployment.

Architecture

Three mental models cover 90% of production use. The StateGraph builder compiles down to a Pregel runtime. The runtime advances by supersteps: at each step, all nodes with satisfied preconditions run in parallel, their writes are merged by reducers, and the next set of nodes is determined. Persistence is orthogonal: attach a checkpointer before calling compile() and every superstep is snapshotted automatically.

StateGraphdefine Stateadd nodesadd edges.compile()Compiledinvoke()stream()astream()Pregel runtimesuperstepssuperstep n+1
StateGraph lifecycle: define State → add nodes/edges → compile() → Pregel runtime
State[n]Node ANode BNode CPARALLELReducersmerge writesState[n+1]
One Pregel superstep: parallel node execution, reducer aggregation, next-step resolution
THREADck0checkpointck1checkpointck2checkpointinterrupt()ck3checkpointCommand(resume=)ck4checkpointhuman review
Thread as checkpoint sequence: interrupt() pauses, resume via Command(resume=)

Key concepts

StateGraph
Builder class parameterised by a user-defined State type. Nodes are added as functions State → Partial<State>; edges connect them. Not executable directly: must call .compile() first.
Reducer
Per-key merge function (Value, Value) → Value declared via Annotated. Without a reducer, concurrent writes use last-write-wins. The built-in add_messages reducer appends rather than replaces message lists.
Checkpoint
Snapshot of the graph state at one superstep: monotone UUID v6 ID, channel_values, channel_versions, versions_seen per node. The checkpointer writes one per superstep; the thread is the ordered sequence of these snapshots.
Thread
Sequence of checkpoints identified by thread_id. Configured via {"configurable": {"thread_id": ...}}. Narrows to a specific checkpoint_id for time-travel or debug.
Pregel
The message-passing runtime underlying LangGraph, inspired by Google Pregel (BSP). Developers never instantiate it directly: CompiledStateGraph extends Pregel. The @entrypoint decorator in the Functional API also returns a Pregel.
interrupt() / Command
interrupt() pauses execution inside a node, saves state, waits indefinitely. Resume via Command(resume=value), which becomes the return value of interrupt(). Requires a checkpointer and a thread_id. The entire node re-executes from the start on resume, not from the interrupt() line.

When to use

Good fit

  • Cyclic flows: the agent needs to loop back (reflect, retry, iterate) based on intermediate results.
  • Stateful pipelines: state must survive across multiple LLM calls, across API calls, or across a session restart.
  • Fault tolerance: if the process crashes mid-run, resuming from the last checkpoint is a hard requirement.
  • Human-in-the-loop: a human must review or approve before the pipeline continues.
  • Multi-agent coordination with explicit state machines: you need full control over which agent runs when and under what condition.

Anti-patterns

  • Checkpoint bloat at scale: the Postgres checkpointer writes across 4 tables. A typical conversation (2 messages, 3 tool calls) generates approximately 93 records. At 120,000+ conversations per week, storage grows without bound unless you set a retention/purge policy. Source: tadeodonegana.com/posts/scaling-langgraph-postgres-checkpointer.
  • Infinite debate in multi-agent loops: without a terminal state, agents can cycle indefinitely, burning tokens and hitting rate limits. Mitigate with recursion_limit and conditional edges on a retry_count field.
  • Node re-execution on resume: when Command(resume=) is called, the entire node re-executes from its first line, not from the interrupt() call. Any side-effect before interrupt() (API call, DB write) will run twice. Make those side-effects idempotent or move them after interrupt().
  • InMemorySaver in production: state is lost on process restart. Use PostgresSaver (or an equivalent persistent checkpointer) in any environment that can restart.
  • Linear stateless pipeline: if the flow has no cycles, no persistent state, and no human approval step, the overhead of StateGraph is not justified. Use LCEL or plain function composition instead.

Code examples

Minimal StateGraph with add_messages reducer

from typing import Annotated
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_core.messages import HumanMessage, AIMessage
from typing_extensions import TypedDict

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

def chat_node(state: State) -> dict:
    # Replace with your actual LLM call
    reply = AIMessage(content="Hello from LangGraph")
    return {"messages": [reply]}

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

graph = builder.compile()
result = graph.invoke({"messages": [HumanMessage(content="Hi")]})

add_messages is the built-in reducer that appends messages instead of replacing the list. Without it, each node write would overwrite the previous messages.

Human-in-the-loop with interrupt() and Command(resume=)

from langgraph.types import interrupt, Command
from langgraph.checkpoint.postgres import PostgresSaver
from langgraph.graph import StateGraph, START, END
from typing_extensions import TypedDict

class State(TypedDict):
    proposal: str
    approved: bool

def generate_node(state: State) -> dict:
    return {"proposal": "Deploy to production at 14:00"}

def review_node(state: State) -> dict:
    decision = interrupt({"proposal": state["proposal"]})
    return {"approved": decision == "yes"}

def deploy_node(state: State) -> dict:
    return {}

builder = StateGraph(State)
builder.add_node("generate", generate_node)
builder.add_node("review", review_node)
builder.add_node("deploy", deploy_node)
builder.add_edge(START, "generate")
builder.add_edge("generate", "review")
builder.add_edge("review", "deploy")
builder.add_edge("deploy", END)

with PostgresSaver.from_conn_string("postgresql://...") as checkpointer:
    graph = builder.compile(checkpointer=checkpointer)
    config = {"configurable": {"thread_id": "run-001"}}
    graph.invoke({"proposal": "", "approved": False}, config)
    # Human reviews, then:
    graph.invoke(Command(resume="yes"), config)

interrupt() requires both a checkpointer and a thread_id. The node re-executes from its first line on resume: any side-effect before interrupt() runs twice. Keep side-effects after interrupt() or make them idempotent.

PostgresSaver for production persistence

from langgraph.checkpoint.postgres import PostgresSaver
import os

POSTGRES_URI = os.environ["DATABASE_URL"]

with PostgresSaver.from_conn_string(POSTGRES_URI) as checkpointer:
    checkpointer.setup()  # creates tables on first run

    graph = builder.compile(checkpointer=checkpointer)
    config = {"configurable": {"thread_id": "session-42"}}

    result = graph.invoke(input_state, config)
    result2 = graph.invoke(next_input, config)

    history = list(graph.get_state_history(config))

PostgresSaver writes to 4 tables. At scale (100k+ conversations/week), set up a retention policy to bound storage growth: approximately 93 records per average conversation.

Comparison

vs LangChain LCEL

Cycles, state, fault tolerance

LCEL for linear pipelines with no persistent state. LangGraph whenever you need cycles, conditional branching, checkpointing, or human approval. The two compose: a LCEL chain can be a node inside a LangGraph.

vs CrewAI

Abstraction level, role-based teams

CrewAI raises the abstraction: you define roles (Researcher, Writer, Validator) and the framework handles the coordination. Less code for role-based sequential pipelines, but less control over cycle topology and state transitions. LangGraph is the right choice when the state machine is the product. Source: docs.crewai.com (first-party, CrewAI bias acknowledged).

vs AutoGen / AG2

Flow control, state, project status

LangGraph drives flow with an explicit graph (nodes, edges, conditional routing) and an automatic checkpointer. AutoGen models agent interaction as a conversation with emergent routing, and persistence is manual (save_state / load_state). Reach for AutoGen-style when you want fast conversational multi-agent prototyping, for LangGraph when you need an auditable, cyclic state machine. Status matters: the original Microsoft AutoGen is in maintenance (successor: Microsoft Agent Framework), and AG2 is the community fork keeping the v0.2 API. Sources: zenml.io, github.com/ag2ai/ag2.

vs LlamaIndex Workflows

Programming model, ideal workload

LlamaIndex Workflows are event-driven and step-based (branches return event types, loops re-emit events), with a Context store and checkpointing for restart resume. They shine for RAG and document Q&A; LangGraph shines for long-running, cyclic multi-agent control. The two often compose: LlamaIndex for retrieval, LangGraph for orchestration. Sources: developers.llamaindex.ai, leanware.co.

Resources

FAQ

What is the difference between LangGraph and LangChain?
LangChain provides building blocks (LLM clients, prompt templates, LCEL pipelines). LangGraph is the stateful orchestration layer built on top: it adds the graph model, the Pregel runtime, and checkpointing. LangChain without LangGraph works for linear pipelines. Add LangGraph when you need cycles, persistent state, or fault-tolerant resumption.
Do I need Postgres in production?
If your agent needs to survive a process restart, yes. InMemorySaver loses all state on restart. PostgresSaver (or any persistent checkpointer) is the production-grade choice. The tradeoff: PostgresSaver writes to 4 tables, generating approximately 93 records per average conversation. Plan a retention policy if you expect high volume.
How does human-in-the-loop work?
Call interrupt(payload) inside a node. LangGraph saves the current state and pauses indefinitely. Resume by calling graph.invoke(Command(resume=value), config) with the same thread_id. The return value of interrupt() becomes the value passed to Command(resume=). Caveat: the entire node re-executes from its start on resume, so side-effects before interrupt() will run twice.
Is LangGraph a durable execution system like Temporal?
Not quite. LangGraph checkpointing lets you resume from a saved state, but it is not a full durable-execution runtime. In OSS single-process mode, there is no watchdog, no automatic failure detection, and no re-scheduling. You need external infrastructure to detect a crashed process and trigger a resume. Temporal solves this at the runtime level. Source: langchain.com/resources/langgraph-vs-temporal.
What is a checkpoint and a thread?
A checkpoint is a snapshot of the entire graph state at one superstep: it has a monotone UUID v6 ID and stores channel values and version vectors. A thread is the ordered sequence of checkpoints sharing the same thread_id. You can replay or branch a thread by targeting a specific checkpoint_id.