LangGraph is an MIT-licensed Python (and TypeScript) library for building long-running, stateful agent workflows as directed, potentially cyclic graphs — addressing the fundamental limitations of linear chain-based LLM orchestration.
Early LLM orchestration frameworks modelled pipelines as directed acyclic sequences of operations. This works for simple prompt-response tasks but breaks down as soon as an agent must loop, retry, or branch based on intermediate results.
ReAct loops: Reason → Act → Observe → Reason, repeating until a goal is met or a budget exceeded.
Shared memory visible to every node — updated incrementally, not reconstructed each step.
Ability to pause at an arbitrary node, await human input, then resume with full history intact.
A LangGraph application is a compiled StateGraph — a finite state machine whose transitions are driven by LLM outputs and tool results rather than hardcoded logic.
Three primitives compose every graph:
A typed shared dictionary (TypedDict or Pydantic) that flows through every node.
Nodes read the full state and return only the keys they modify.
Pure Python callables — plain functions, async functions, LangChain Runnables, or nested compiled sub-graphs — that transform state.
Direct edges unconditionally connect two nodes. Conditional edges call a router function on the current state to select the next node at runtime.
Every graph is parameterised by a State schema. All nodes and edges receive an instance of this schema as their sole argument.
from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages
class AgentState(TypedDict):
# add_messages is a reducer — it appends
# rather than overwriting the list
messages: Annotated[list, add_messages]
tool_output: str
iteration: int
When a node returns a partial dict, LangGraph merges each key using its reducer function. The default reducer is last-write-wins. Custom reducers enable append semantics, set union, or conflict resolution.
A node only needs to return the keys it modifies — not the entire state object. This avoids race conditions in parallel branches and keeps node logic cohesive.
def call_llm(state: AgentState) -> dict:
response = llm.invoke(state["messages"])
# return only the keys this node touches
return {
"messages": [response], # reducer appends
"iteration": state["iteration"] + 1,
}
Nodes can declare additional state keys not in the root schema — useful for subgraph-internal bookkeeping that should not propagate to the parent graph.
A node is any callable that accepts a state dict and returns a (partial) state dict. This uniformity means nodes are trivially composable and independently testable.
Executes tool calls embedded in the last AI message and writes results back
as ToolMessage objects — handles serialisation, error propagation, and
parallel fan-out automatically.
Calls any LangChain ChatModel with bound tools. Returns updated
message list. The standard pattern for ReAct-style reasoning.
A compiled StateGraph can be registered as a node in a parent graph,
enabling hierarchical, modular multi-agent architectures.
Nodes support async def natively. When multiple nodes have no
data dependency in the same super-step they execute in
parallel within that super-step.
from langgraph.graph import StateGraph, START, END
graph = StateGraph(AgentState)
# register functions as nodes
graph.add_node("reason", call_llm)
graph.add_node("act", ToolNode(tools))
# direct edge: reason always proceeds to act
graph.add_edge(START, "reason")
graph.add_edge("act", "reason") # ← cycle!
# conditional edge: route after reasoning
graph.add_conditional_edges(
"reason",
should_continue, # router fn → str
{"continue": "act", "end": END}
)
app = graph.compile()
| Type | API | Behaviour |
|---|---|---|
| Direct | add_edge(a, b) | Always transitions A→B |
| Conditional | add_conditional_edges(a, fn, map) | Calls fn(state) → key → next node |
| Fan-out | router returns a list | Spawns N parallel branches |
| Send | Send(node, state) | Dynamically dispatches sub-tasks mid-graph |
Execution proceeds in discrete super-steps, inspired by Google's Pregel BSP model. A super-step ends when all currently active nodes have completed. Nodes activated in the same super-step run concurrently.
Before a graph can be executed it must be compiled via
graph.compile(). Compilation is lightweight but serves several critical roles.
STARTTypedDict or Pydantic modelCompile-time parameters attach persistent infrastructure to the immutable graph:
app = graph.compile(
checkpointer = PostgresSaver(conn),
interrupt_before = ["human_review"],
interrupt_after = ["tool_call"],
)
The compiled object is a LangChain Runnable, giving it a standard interface regardless of internal complexity:
invoke(input, config)Synchronous, blocking execution. Returns the final state dict.
stream(input, config)Yields chunks as each node completes. Supports multiple stream modes:
values, updates,
debug, messages.
astream_events(input, config)Async generator emitting fine-grained lifecycle events — token streaming from LLM nodes, tool start/end, etc.
The compiled graph is immutable — runtime modifications that could destabilise execution are prevented by design.
LangGraph's checkpointer saves a complete snapshot of graph
state after every super-step. Snapshots are keyed by
(thread_id, checkpoint_id).
| Backend | Class | Use Case |
|---|---|---|
| In-memory | InMemorySaver | Development, tests |
| SQLite | SqliteSaver | Local / single-process |
| PostgreSQL | PostgresSaver | Production |
| Azure Cosmos DB | CosmosDBSaver | Azure-hosted production |
All backends implement a common
BaseCheckpointSaver interface with .put,
.get_tuple, and .list methods, making backends
interchangeable with a single line change.
A crashed or timed-out run can be restarted from its last checkpoint by re-invoking
with the same thread_id.
Pass a historic checkpoint_id to re-execute the graph from that
point, creating a new branch. Invaluable for debugging agent decisions.
The Store interface complements checkpoints by persisting facts
across different thread IDs — e.g. user preferences across sessions.
Interrupts require checkpointing: the graph serialises state on pause, allowing resumption after an arbitrarily long wait for human input.
LangGraph provides two complementary mechanisms for injecting human oversight at arbitrary points in a running graph.
Setting interrupt_before=["node_name"] or
interrupt_after=["node_name"] at compile time causes execution to pause
and serialise state before or after that node fires. The __interrupt__ key
in the returned state carries a structured payload describing why execution paused.
# Resume after human provides approval
app.invoke(
Command(resume={"approved": True}),
config={"configurable": {"thread_id": tid}}
)
Inside any node, interrupt(payload) pauses execution programmatically
— no compile-time configuration required. The payload is surfaced to the caller.
While paused, the caller can patch graph state via app.update_state()
before resuming — enabling human correction of LLM-generated content mid-run.
A central LLM node decides which specialised sub-agent to invoke next. Each sub-agent is a compiled subgraph registered as a node in the parent graph. Common in tool-heavy research or coding pipelines.
supervisor → agent_A
supervisor → agent_B
agent_A → supervisor # report back
agent_B → supervisor
Agents hand off to each other directly via
Command(goto="agent_B") without a central coordinator.
LangGraph's Send API enables dynamic dispatch of sub-tasks
to whichever peer is best placed to handle them.
agent_A → agent_B
agent_B → agent_C
agent_C → agent_A # or END
A fan-out edge spawns N parallel worker agents (via Send);
a downstream aggregator node merges their outputs using a list-append reducer.
Suits parallel web search or document analysis.
plan → Send("worker", s0)
plan → Send("worker", s1)
plan → Send("worker", s2)
worker → aggregate
A subgraph shares state keys that appear in both the parent and child schemas;
additional keys in the child schema are private. The parent's checkpointer
automatically propagates to all subgraphs — a single compile(checkpointer=…)
at the root is all that is required.
LangGraph is designed from the ground up for token-by-token streaming — essential for responsive UIs in agentic applications.
| Mode | Yields |
|---|---|
values | Full state after each super-step |
updates | Partial state dict from each node |
debug | Full execution trace events |
messages | Token-level chunks from LLM nodes |
custom | User-defined payloads via StreamWriter |
Multiple modes can be combined in a single call.
Subgraph events are exposed by passing subgraphs=True.
The highest-resolution API, emitting structured lifecycle events for every LLM call, tool invocation, and node transition. Powers real-time "thinking" indicators and step-by-step progress UIs.
async for event in app.astream_events(
{"messages": [HumanMessage(query)]},
config={"configurable": {"thread_id": "1"}},
version="v2"
):
kind = event["event"]
if kind == "on_chat_model_stream":
token = event["data"]["chunk"].content
print(token, end="", flush=True)
elif kind == "on_tool_start":
print(f"\n[tool] {event['name']}")
LangGraph adds no latency to streaming paths — the framework is explicitly designed so that streaming workflows carry no orchestration overhead.
While LangGraph can be deployed as a plain Python library, LangChain provides a managed LangGraph Platform (LangSmith Deployment) that wraps any compiled graph behind a production API server.
PostgresSaver wiringA local desktop GUI that renders the compiled graph topology, lets developers step through execution node-by-node, inspect state at each checkpoint, and replay from any prior step — dramatically reducing iteration time for complex agents.
LangGraph Server can be self-hosted as a Docker container using the
langgraph-cli tool. It provides the same REST API and handles
persistence, streaming, and task queuing independently.
# build and serve
langgraph build -t my-agent:latest
langgraph up --port 8000
Graphs accept a typed Configuration schema in addition to state.
These are runtime-tunable parameters — which LLM to call, search depth limits,
etc. — that callers inject per-request via configurable without
recompiling the graph.
The same graph-compilation primitives underpin several Anthropic and LangChain browser-extension agents, illustrating the framework's range from local scripts to production SaaS agents.
| Feature | LangGraph | CrewAI | AutoGen / AG2 | OpenAI Agents SDK |
|---|---|---|---|---|
| Execution model | Directed graph (cyclic) | Role-based sequential crew | Conversation-driven | Tool-loop with handoffs |
| State management | Typed, reducer-driven | Implicit via task context | Message history only | SDK-managed context |
| Cycles / loops | First-class | Limited | Via conversation turns | Via tool retries |
| Persistence | Pluggable checkpointers | None built-in | Partial (memory store) | Limited |
| Human-in-the-loop | interrupt() / breakpoints | Human agent role | human_input_mode | Hooks (limited) |
| Multi-agent topology | Supervisor, peer, subgraph | Hierarchical crew | GroupChat, Nested | Handoffs |
| LLM provider agnosticism | Any LangChain ChatModel | Any (via LangChain) | OpenAI-centric | OpenAI-only |
| Streaming | Token-level, zero overhead | Limited | Partial | SSE stream |
| Observability | LangSmith (deep) | CrewAI Plus | OpenTelemetry | OpenAI dashboard |
| Licence | MIT | MIT | CC-BY-4 / MIT | MIT |
Arbitrary graph topologies — including cycles, fan-out, fan-in, and nested subgraphs — model real-world agent behaviour without contorting the code.
Checkpointing, time-travel, and fault-tolerant resumption make long-running multi-step agents viable in production at scale.
Human-in-the-loop interrupts, state editing, and typed schemas give engineers precise control over agent behaviour — essential for safety-sensitive applications.
Drop-in use of hundreds of LangChain integrations — LLMs, vector stores, tools, retrievers — with no adaptor layer.
Graph primitives, reducers, super-steps, and checkpointer semantics require meaningful upfront investment compared to simple chain-based frameworks.
Distributed agent failures, non-deterministic LLM routing, and cycle detection are harder to trace than linear pipelines. LangSmith partially mitigates this.
Realising fault tolerance requires a PostgreSQL-backed checkpointer and careful thread management — non-trivial to configure and operate.
While standalone use is possible, the richest tooling (LangSmith, LangGraph Platform) ties the stack to the LangChain ecosystem.
A complete, minimal ReAct agent with persistent memory and human approval before each tool call.
from typing import Annotated, TypedDict
from langchain_core.messages import BaseMessage
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode
from langgraph.checkpoint.memory import InMemorySaver
# ── State ──────────────────────────────────────
class State(TypedDict):
messages: Annotated[list[BaseMessage],
add_messages]
# ── Nodes ──────────────────────────────────────
llm_with_tools = llm.bind_tools(tools)
def agent(state: State) -> dict:
return {"messages": [
llm_with_tools.invoke(state["messages"])
]}
def should_continue(state: State) -> str:
last = state["messages"][-1]
if last.tool_calls:
return "tools"
return END
# ── Graph construction ─────────────────────────
builder = StateGraph(State)
builder.add_node("agent", agent)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "agent")
builder.add_conditional_edges(
"agent", should_continue,
{"tools": "tools", END: END}
)
builder.add_edge("tools", "agent")
# ── Compile with persistence + HITL ────────────
memory = InMemorySaver()
app = builder.compile(
checkpointer = memory,
interrupt_before = ["tools"], # pause for approval
)
# ── Invoke ─────────────────────────────────────
cfg = {"configurable": {"thread_id": "t1"}}
state = app.invoke(
{"messages": [HumanMessage("Research LangGraph")]},
config=cfg
)
# state["__interrupt__"] signals pending approval
# after human approval:
app.invoke(Command(resume=True), config=cfg)
This pattern — under 40 lines — captures the full LangGraph capability surface: typed state, reducers, conditional edges, cyclic execution, checkpointing, and human-in-the-loop interrupts.
LangGraph ships a first-class TypeScript / JavaScript SDK
(@langchain/langgraph) with API parity to the Python version.
The same graph primitives — state, nodes, edges, checkpointers — work identically.
npm install @langchain/langgraph @langchain/core
# or
yarn add @langchain/langgraph @langchain/core
TypeScript uses an Annotation builder instead of
Python's TypedDict. Reducers are declared inline.
import { Annotation } from "@langchain/langgraph";
import { BaseMessage } from "@langchain/core/messages";
const AgentState = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: (curr, update) => [...curr, ...update],
default: () => [],
}),
iteration: Annotation<number>({
reducer: (_curr, update) => update,
default: () => 0,
}),
});
import { StateGraph, START, END } from "@langchain/langgraph";
import { ToolNode } from "@langchain/langgraph/prebuilt";
const graph = new StateGraph(AgentState)
.addNode("reason", callLlm)
.addNode("act", new ToolNode(tools))
.addEdge(START, "reason")
.addEdge("act", "reason")
.addConditionalEdges("reason", shouldContinue, {
continue: "act",
end: END,
});
const app = graph.compile();
async function callLlm(
state: typeof AgentState.State
) {
const response = await llmWithTools.invoke(
state.messages
);
return {
messages: [response],
iteration: state.iteration + 1,
};
}
function shouldContinue(
state: typeof AgentState.State
): string {
const last = state.messages[state.messages.length - 1];
return last.tool_calls?.length ? "continue" : "end";
}
const stream = await app.stream(
{ messages: [new HumanMessage("Research AI")] },
{ configurable: { thread_id: "t1" },
streamMode: "messages" }
);
for await (const [message, metadata] of stream) {
if (message.content) {
process.stdout.write(String(message.content));
}
}
import { MemorySaver } from "@langchain/langgraph";
const memory = new MemorySaver();
const app = graph.compile({
checkpointer: memory,
interruptBefore: ["act"],
});
// Resume after human approval
await app.invoke(
{ messages: [] }, // empty — resumes from checkpoint
{ configurable: { thread_id: "t1" } }
);
import { ChatOpenAI } from "@langchain/openai";
import { createReactAgent } from
"@langchain/langgraph/prebuilt";
import { TavilySearchResults } from
"@langchain/community/tools/tavily_search";
const llm = new ChatOpenAI({ model: "gpt-4o" });
const tools = [new TavilySearchResults()];
const agent = createReactAgent({
llm,
tools,
});
const result = await agent.invoke({
messages: [
{ role: "user", content: "What is LangGraph?" }
],
});
TypeScript graphs deploy to LangGraph Platform identically to Python.
Use langgraph.json to point at your TS entrypoint.
The compiled graph is a plain async function — wrap it in any Node.js server framework. Stream via SSE or WebSockets.
from typing import Annotated
def merge_dicts(current: dict, update: dict) -> dict:
"""Deep merge — update wins on conflict."""
return {**current, **update}
class ResearchState(TypedDict):
messages: Annotated[list, add_messages]
findings: Annotated[dict, merge_dicts]
sources: Annotated[list, lambda a, b: a + b]
from langgraph.types import Send
def fan_out(state: State) -> list[Send]:
"""Dispatch one worker per query."""
return [
Send("search_worker", {"query": q})
for q in state["queries"]
]
graph.add_conditional_edges(
"planner", fan_out
)
graph.add_edge("search_worker", "aggregator")
def supervisor(state: State) -> dict:
"""Central LLM decides which agent to call."""
response = llm.invoke([
SystemMessage("Route to: researcher, "
"coder, or FINISH"),
*state["messages"]
])
return {"next_agent": response.content}
graph = StateGraph(State)
graph.add_node("supervisor", supervisor)
graph.add_node("researcher", research_agent)
graph.add_node("coder", coding_agent)
graph.add_conditional_edges("supervisor",
lambda s: s["next_agent"],
{"researcher": "researcher",
"coder": "coder",
"FINISH": END})
graph.add_edge("researcher", "supervisor")
graph.add_edge("coder", "supervisor")
from langgraph.types import interrupt
def risky_action(state: State) -> dict:
approval = interrupt({
"action": "delete_file",
"path": state["target_path"],
})
if not approval.get("approved"):
return {"messages": [AIMessage("Aborted.")]}
# proceed with action...
return {"messages": [AIMessage("Done.")]}
Directed, potentially cyclic graphs with Pregel-style super-step execution. Supports fan-out, fan-in, conditional routing, and nested subgraphs.
Typed shared state with per-key reducer functions. Nodes return partial updates; LangGraph merges deterministically — no global state mutation.
Pluggable checkpointer backends. Enables fault tolerance, time travel, multi-session memory, and — as a prerequisite — human-in-the-loop interrupts.
LangGraph Documentation — docs.langchain.com
LangGraph GitHub — github.com/langchain-ai/langgraph
Malewicz et al., "Pregel: A System for Large-Scale Graph Processing," SIGMOD 2010
LangChain Academy — academy.langchain.com
LangGraph Platform — langchain.com/langgraph
LangSmith Deployment Guide — docs.smith.langchain.com