LangGraph — agents as graphs
LangGraph lets you build an AI agent as a map of steps and decisions, so it can branch, loop and retry instead of running in a straight line.
- 12 min read
- 3 reading levels
- Published
Read these first
On this page 6
One lesson, three depths. Pick the one that fits you today — you can switch any time.
Beginner — No maths. Plain English.
LangGraph is a tool for building AI agents as a map of steps, where the agent decides which path to take next.
Think of a local train journey with a junction in the middle. At the junction, the signal decides which track your train takes. Different tracks lead to different stations, and some tracks loop back the way you came. LangGraph lets you draw that railway map for an AI agent — and the agent's own answers work the signals.
Why it exists
The first wave of LLM tools chained steps in a straight line: do this, then this, then this. LangChain made those pipelines easy to build.
Straight lines break on real problems. A support agent needs to ask "is this about a refund or a delivery?" and behave differently for each. A writing agent needs to draft, check its own draft, and go back and redraft. That going-back part is a loop — a path that returns to an earlier step — and a straight-line pipeline cannot express it.
LangGraph was built for exactly this. It comes from the LangChain team, but it solves a different problem: not "call a model, then a tool", but "decide, branch, loop, and remember where you are".
How it works
You describe your agent as three kinds of pieces.
- A node is one step of work — call a model, call a tool, run some code. A station on the map.
- An edge is an arrow saying which step comes next. The track between stations.
- The state is a shared bag of information that travels through the graph. Every node can read the bag and add things to it.
┌──────────┐
question ─────► │ classify │
└────┬─────┘
"refund" │ "delivery"
┌──────────┴──────────┐
▼ ▼
┌──────────┐ ┌──────────┐
│ refund │ │ delivery │
│ helper │ │ helper │
└────┬─────┘ └────┬─────┘
▼ ▼
END ENDThe classify step reads the question and writes a label into the bag. The edge after it reads that label and picks the next station. That is the whole trick.
Where you have already seen this shape
- A bank's support chat asks what your problem is, then follows a different script for cards, loans or UPI.
- Google Maps recalculates your route when you miss a turn — the plan changes based on what actually happened.
- A food delivery app's help flow loops: it offers a fix, asks "did this solve it?", and tries again if you say no.
None of these are straight lines. All of them are maps with decisions.
The honest part
The word agent gets used loosely everywhere. Here it means software that uses a model's output to choose its own next step, instead of following a fixed script. That freedom is the power and the danger. An agent that can loop can also loop forever, and an agent that can branch can branch wrongly.
Graphs do not remove that danger. They make it visible, limitable and debuggable — which is why teams building serious agents reach for them.
Remember this
- LangGraph describes an agent as nodes (steps), edges (arrows) and state (the shared bag).
- Its whole reason to exist is branching and looping, which straight pipelines cannot do.
- The model's answers decide the path through the map, at run time, on every run.
What to learn next
- State and checkpoints in LangGraph — memory, threads, and resuming a crashed agent.
- MCP — the Model Context Protocol — the standard plug for giving agents tools.
- Build an AI agent — apply the loop pattern end to end.
Developer — Code and libraries.
Setup
pip install langgraphThis lesson was written against langgraph 1.2 (1.0 was the first stable release, from October 2025). The install pulls in langchain-core and friends — about 50 MB on disk, no model weights, no GPU. If you are on an old 0.x tutorial, the core imports below are the same, but check anything else against the current docs.
One thing makes LangGraph unusually cheap to learn: nodes are plain Python functions. No model is required. So the examples below use a stub function where a model would go, run entirely offline, and print the same thing on your machine as on this page.
A branching agent you can run
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
# ---- a stub model, so this runs offline and costs nothing ----
# LangGraph never talks to a model itself. Nodes are plain functions,
# so anything callable can play the model's part.
def stub_llm(prompt: str) -> str:
if "Classify" in prompt:
return "refund" if "money" in prompt or "refund" in prompt else "delivery"
if "refund" in prompt:
return "Your refund is on its way. It takes five working days."
return "Your parcel is with the courier. Expect it in two days."
# ---- the shared state every node reads and writes ----
class State(TypedDict):
question: str
category: str
answer: str
# ---- nodes: plain functions from state to a partial update ----
def classify(state: State) -> dict:
label = stub_llm(f"Classify this question: {state['question']}")
return {"category": label}
def handle_refund(state: State) -> dict:
return {"answer": stub_llm(f"refund question: {state['question']}")}
def handle_delivery(state: State) -> dict:
return {"answer": stub_llm(f"delivery question: {state['question']}")}
# ---- the routing decision lives on an edge, not in a node ----
def route(state: State) -> str:
return state["category"]
builder = StateGraph(State)
builder.add_node("classify", classify)
builder.add_node("refund", handle_refund)
builder.add_node("delivery", handle_delivery)
builder.add_edge(START, "classify")
builder.add_conditional_edges("classify", route,
{"refund": "refund", "delivery": "delivery"})
builder.add_edge("refund", END)
builder.add_edge("delivery", END)
graph = builder.compile()
result = graph.invoke({"question": "I want my money back for the broken mixer."})
print("category:", result["category"])
print("answer: ", result["answer"])
result = graph.invoke({"question": "Where is my parcel?"})
print("category:", result["category"])
print("answer: ", result["answer"])category: refund answer: Your refund is on its way. It takes five working days. category: delivery answer: Your parcel is with the courier. Expect it in two days.
To use a real model instead, replace the body of stub_llm with a call to your provider — or a local model via Ollama. The graph does not change at all. That separation is the point.
Line by line, the parts that are not obvious
class State(TypedDict) — the state is a typed dictionary you design. Every node receives the whole state and returns only the keys it wants to change. classify returns {"category": label}, not a full copy. LangGraph merges the update in for you.
builder.add_conditional_edges("classify", route, {...}) — this is the junction. After classify runs, LangGraph calls route(state), which returns a string. The dictionary maps each possible string to a destination node. Decisions live on edges; work lives in nodes. Keeping those separate keeps the graph readable.
START and END — special markers, not nodes you write. START says where invoking the graph enters. END says the run is finished. Forget the entry edge and compile() refuses:
ValueError: Graph must have an entrypoint: add at least one edge from START to another node
builder.compile() — checks the wiring and returns a runnable graph. Compile once, then invoke as many times as you like.
The move pipelines cannot make: a loop
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
draft: str
attempts: int
def write(state: State) -> dict:
# each pass makes the draft one word longer, standing in for "improve it"
new = state["draft"] + " better"
return {"draft": new, "attempts": state["attempts"] + 1}
def good_enough(state: State) -> str:
# a real system would ask a model to judge the draft here
if len(state["draft"].split()) >= 4 or state["attempts"] >= 5:
return "done"
return "again"
builder = StateGraph(State)
builder.add_node("write", write)
builder.add_edge(START, "write")
builder.add_conditional_edges("write", good_enough, {"again": "write", "done": END})
graph = builder.compile()
result = graph.invoke({"draft": "hello", "attempts": 0})
print(result){'draft': 'hello better better better', 'attempts': 3}The edge from write points back at write. Draft, judge, redraft — the exact draft-critique-revise pattern real writing agents use, in fifteen lines. Notice the attempts cap in good_enough. Every loop you build needs an escape hatch you wrote yourself.
Common mistakes
Forgetting to return the update. A node that changes variables but returns None is treated as "no update". The graph runs, nothing errors, and the state comes out unchanged. If a node's work is vanishing, check its return first.
A loop with no exit. If your condition can never return "done", the graph does not hang forever — it hits LangGraph's step limit and raises:
langgraph.errors.GraphRecursionError: Recursion limit of 10007 reached without hitting a stop condition. You can increase the limit by setting the `recursion_limit` config key.
You can pass graph.invoke(inputs, {"recursion_limit": 50}) to tighten it. A tight limit that fails fast beats a loose one that burns tokens for an hour. That number is the observed default on langgraph 1.2; older versions stopped at 25.
Routing on text a model made up. With a real model, route reads a label the model wrote. Models drift: ask for refund and one day you get Refund. with a capital letter and a full stop. Normalise before routing (label.strip().lower()), and always include a fallback branch for "none of the expected labels". Structured output makes this far more reliable than free text.
Doing the deciding inside nodes. You can write one giant node full of if-else that calls everything. It runs — and you have thrown away the graph: no visible structure, nothing to checkpoint between steps, nothing to draw. If a node contains routing logic, it probably wants to be an edge.
Try it yourself
Add a third branch: questions that mention "cancel" should reach a new cancellation node. You will touch exactly three places — the stub, one add_node, and the mapping in add_conditional_edges. Then print graph.get_graph().draw_mermaid() and paste the result into mermaid.live to see your map drawn for you.
What to learn next
- State and checkpoints in LangGraph — memory, threads, and resuming a crashed agent.
- MCP — the Model Context Protocol — the standard plug for giving agents tools.
- Build an AI agent — apply the loop pattern end to end.
Researcher — Mathematics and papers.
The execution model is Pregel, not a workflow engine
LangGraph's runtime is modelled on Pregel (Malewicz et al., 2010), Google's bulk-synchronous graph processing system, which in turn builds on the bulk synchronous parallel model (Valiant, 1990). Execution proceeds in discrete supersteps:
- All nodes activated in this superstep run, each reading a snapshot of the state.
- Their returned updates are collected as messages.
- Updates are applied to state channels, which determines the set of nodes activated next.
- Repeat until no node is activated, or the step limit is reached.
Formally, the state is a set of channels ${c_1, \dots, c_k}$. Where:
- $c_i$ — one named field of the state, with a current value $v_i$.
- A node is a function $f: (v_1, \dots, v_k) \mapsto U$, with $U$ a partial assignment of new values to some channels.
- Each channel has a reducer $r_i(v_{old}, v_{new}) \to v$ — the rule that combines an existing value with an incoming update. The default reducer is last-write-wins.
The reducer formulation matters when two nodes run in the same superstep and write the same channel. With last-write-wins that is a conflict error; with an associative, commutative reducer (list concatenation, set union, addition) parallel branches merge deterministically. This is the same reasoning that underlies CRDTs, arrived at independently.
Cost: a run of $s$ supersteps with at most $w$ nodes active per step performs $O(sw)$ node executions, and checkpointing (next lesson) adds one state serialisation per superstep.
Why cycles need this machinery at all
A DAG engine (Airflow, Prefect, plain function composition) topologically sorts the graph once and executes it. Topological sort does not exist for a graph with cycles. A cyclic agent needs a runtime that decides the next node dynamically from state, which is why "LangChain with extra steps" is the wrong mental model — the execution semantics are different, not the API surface.
The patterns the graphs encode
The agent architectures people build in LangGraph are older than the library:
- ReAct — Yao et al., 2022, ReAct: Synergizing Reasoning and Acting in Language Models (arxiv.org/abs/2210.03629). Reason, act, observe, loop. As a graph: model node → tool node → back to model node.
- Reflexion — Shinn et al., 2023, Reflexion: Language Agents with Verbal Reinforcement Learning (arxiv.org/abs/2303.11366). The draft-critique-revise loop, with the critique fed back as state.
- Plan-and-execute — decompose, execute subtasks, replan on failure; popularised by Wang et al., 2023, Plan-and-Solve Prompting (arxiv.org/abs/2305.04091).
LangGraph ships create_react_agent as a prebuilt for the first pattern. There is no LangGraph paper; the primary sources are the documentation and the framework code itself.
State of the art and alternatives
As of mid-2026 the graph-of-state approach sits alongside several competing framings: conversation-driven multi-agent systems (AutoGen), role-based crews (CrewAI), code-acting agents (smolagents, following CodeAct, Wang et al., 2024, arxiv.org/abs/2402.01030), and thin SDKs over provider APIs (OpenAI Agents SDK, Claude's agent tooling). The genuine differentiators of the graph approach are durable execution — a checkpointed graph can crash and resume mid-run — and human-in-the-loop interrupts at arbitrary points, both covered in the next lesson.
The open research problem underneath all of them is unchanged: reliable multi-step behaviour. Compounding error over long horizons is unsolved. A per-step success rate $p$ compounds to a completion rate near $p^n$ over $n$ independent steps — at $p = 0.95$ and $n = 20$, that is roughly $0.36$ — and every framework in this section is, at bottom, scaffolding to force $n$ down and $p$ up.
What to learn next
- State and checkpoints in LangGraph — memory, threads, and resuming a crashed agent.
- MCP — the Model Context Protocol — the standard plug for giving agents tools.
- Build an AI agent — apply the loop pattern end to end.