This is the first post in our LangGraph fundamentals series. Later posts cover building a stateful agent, conditional edges, human-in-the-loop, and multi-agent systems.
The LangChain agent from our previous series already loops — calling tools, feeding results back, deciding a next step, repeating. That loop works well right up until the workflow it's modeling needs something a simple loop can't naturally express: revisiting an earlier step based on a later one's result, running two branches in parallel and merging them, or pausing indefinitely for a human decision before continuing. LangGraph exists specifically for that harder shape of problem.
Why a chain's linear shape becomes limiting
A chain (from our LangChain series) is fundamentally A → B → C — each step runs once, in a fixed order, and there's no natural way to say "if C's result looks wrong, go back to A" or "run B and D at the same time, then combine their results before C." An AgentExecutor's loop handles some of this by repeatedly deciding "call another tool, or finish" — but it's still one loop, not an explicit, inspectable structure describing genuinely different possible paths through a workflow.
The graph model: nodes, edges, and shared state
LangGraph represents a workflow as an explicit graph: nodes are steps (each an ordinary Python function), edges define which node runs next, and a shared state object flows through every node, with each node able to read and update it.
from typing import TypedDict
from langgraph.graph import StateGraph, END
class AgentState(TypedDict):
question: str
context: str
answer: str
def retrieve(state: AgentState) -> AgentState:
state["context"] = "Retrieved: full refunds within 30 days of purchase."
return state
def generate(state: AgentState) -> AgentState:
state["answer"] = f"Based on our policy: {state['context']}"
return state
graph = StateGraph(AgentState)
graph.add_node("retrieve", retrieve)
graph.add_node("generate", generate)
graph.set_entry_point("retrieve")
graph.add_edge("retrieve", "generate")
graph.add_edge("generate", END)
app = graph.compile()
result = app.invoke({"question": "Can I get a refund?", "context": "", "answer": ""})
print(result["answer"])Based on our policy: Retrieved: full refunds within 30 days of purchase.
This specific example — retrieve then generate, in a straight line — is functionally identical to the RAG chain built with plain LangChain in our previous series. Written as a graph, it's more verbose for this exact case, and that's an honest trade-off worth naming directly: a straight-line workflow doesn't need a graph. The graph model earns its extra structure the moment a workflow actually needs branching, looping, or pausing — which is exactly what the remaining posts in this series build toward.
The state object: what actually makes this different from a chain
In LCEL (LangChain's chain-composition syntax from our previous series), each step's output becomes the next step's input directly. In LangGraph, every node reads from and writes to the same shared state object, which persists across the entire graph's execution — a node three steps later can still see something a much earlier node wrote, without it having been explicitly threaded through every intermediate step's input and output along the way. This is the structural feature that makes revisiting earlier context, and the branching and looping covered in the next posts in this series, genuinely natural to express, rather than awkward workarounds bolted onto a fundamentally linear pipeline.
Compiling and running
app = graph.compile()compile() validates the graph's structure (every node reachable, no dangling edges to nodes that don't exist) and produces a runnable object — conceptually similar to compiling a Terraform configuration before apply, or building a Dockerfile into an image before running it: a validation and preparation step, distinct from actually executing the thing it describes.
When to reach for LangGraph over a plain LangChain chain
- A straight-line pipeline (retrieve, then generate; format, then call, then parse) — a plain LCEL chain is simpler, more transparent, and entirely sufficient. Don't reach for a graph by default.
- A workflow that genuinely branches, loops back, or needs to pause and resume (an approval step, a retry loop with a different strategy on failure, multiple independent paths that later merge) — this is where LangGraph's explicit graph structure earns its complexity, covered directly in the remaining posts in this series.
What to actually remember from this post
- A chain runs a fixed, linear sequence; a graph can branch, loop, and pause — a structurally different capability, not just a more complex-sounding wrapper around the same thing.
- Nodes are functions; edges define what runs next; state is shared and persists across the whole execution — the three concepts everything else in LangGraph builds from.
- A straight-line workflow doesn't need LangGraph — reach for it specifically when a workflow's actual shape isn't linear.
Next in the series: Building Your First Stateful Agent with LangGraph, where we build something a plain chain genuinely can't express cleanly: a loop that revisits an earlier step based on a later one's result.
