Introduction to LangGraph¶
1. Why this matters¶
LangChain's LCEL (prompt | model | parser) is great for DAGs — straight-line workflows where each step runs once. But real agents need:
- Loops — call a tool, look at the result, decide to call another tool, retry on failure.
- Branching — different paths based on classification or tool output.
- State across invocations — a chatbot that remembers, an agent that resumes after a crash.
- Human approval — pause before sending the email, wait for the user to click "approve".
- Streaming intermediate steps — show the user what the agent is thinking, not just the final answer.
Plain chains can't express any of that cleanly. LangGraph can.
2. Mental model¶
LangGraph borrows from two ideas:
- State machines — your app is a graph of nodes; the engine picks the next node based on the current state.
- Pregel / message-passing (the model behind Google's graph compute) — each node is a function
state → state_update. The engine applies updates and routes to the next node.
flowchart LR
S((START)) --> A[Node A<br/>reads state,<br/>returns update]
A --> B[Node B]
B --> C{Condition}
C -->|x| D[Node D]
C -->|y| B
D --> E((END))
Three things make LangGraph distinctive vs LangChain:
| LangChain (LCEL) | LangGraph |
|---|---|
| DAG (no cycles) | Arbitrary graph — cycles allowed |
| State is implicit (data flows in pipes) | State is explicit — a TypedDict you define |
| No built-in persistence | Built-in checkpointing to memory/SQLite/Postgres |
| One-shot or chat history | Pause/resume, time-travel debugging, HITL interrupts |
3. Architecture / Flow¶
flowchart TB
subgraph Build [Build time]
S[Define State<br/>TypedDict] --> N[Add Nodes<br/>functions]
N --> E[Add Edges<br/>+ conditional edges]
E --> CP[Compile<br/>+ checkpointer]
end
subgraph Run [Run time]
I[Initial state] --> ENG[LangGraph Engine]
ENG -->|reads/writes| ST[State Snapshot<br/>checkpointed]
ENG --> OUT[Final state OR stream of updates]
end
CP -.compiled graph.-> ENG
4. Core concepts¶
StateGraph(StateSchema)— the builder. You define a state schema (aTypedDict) and the graph operates on instances of it.- Node — a Python function:
state -> partial_state_update. Whatever you return gets merged into the state. - Edge — a directed connection saying "after node A, go to node B".
- Conditional edge —
add_conditional_edges(source, router_fn, mapping)—router_fn(state)returns a string that picks the next node. START/END— the entry and exit pseudo-nodes.compile()— turns the builder into a runnable graph; takes a checkpointer for persistence.- Reducer — when multiple updates touch the same state key, a reducer decides how to merge (replace, append, etc.). Set via
Annotated[T, reducer_fn]. - Checkpointer — where state snapshots get stored (
MemorySaver,SqliteSaver,PostgresSaver). - Thread — a session ID. State is keyed by
thread_idso you can have many independent conversations.
5. Code — minimal working example¶
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
# 1. Define the shape of state
class State(TypedDict):
topic: str
output: str
# 2. Define nodes — each is state -> partial state update
def greet(state: State):
return {"output": f"Let's learn about {state['topic']}!"}
# 3. Build the graph
builder = StateGraph(State)
builder.add_node("greet", greet)
builder.add_edge(START, "greet")
builder.add_edge("greet", END)
graph = builder.compile()
# 4. Run
print(graph.invoke({"topic": "LangGraph"}))
# {'topic': 'LangGraph', 'output': "Let's learn about LangGraph!"}
That's the entire model. Every LangGraph app is a richer version of this.
6. Code — real-world pattern¶
Three-node workflow with an LLM in the middle and a conditional branch:
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
from dotenv import load_dotenv
load_dotenv()
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
class ReviewState(TypedDict):
review: str
sentiment: str
response: str
def classify(state: ReviewState):
out = llm.invoke([
SystemMessage("Classify the review sentiment as 'positive' or 'negative'. Reply with one word."),
HumanMessage(state["review"]),
])
return {"sentiment": out.content.strip().lower()}
def positive_reply(state: ReviewState):
out = llm.invoke([HumanMessage(f"Write a warm thank-you reply to: {state['review']}")])
return {"response": out.content}
def negative_reply(state: ReviewState):
out = llm.invoke([HumanMessage(f"Write a sympathetic apology + offer to help: {state['review']}")])
return {"response": out.content}
def route(state: ReviewState) -> Literal["positive_reply", "negative_reply"]:
return "positive_reply" if "positive" in state["sentiment"] else "negative_reply"
builder = StateGraph(ReviewState)
builder.add_node("classify", classify)
builder.add_node("positive_reply", positive_reply)
builder.add_node("negative_reply", negative_reply)
builder.add_edge(START, "classify")
builder.add_conditional_edges("classify", route)
builder.add_edge("positive_reply", END)
builder.add_edge("negative_reply", END)
graph = builder.compile()
print(graph.invoke({"review": "The product broke in two days. Avoid."}))
You couldn't write this as a single LCEL pipe — the conditional branch makes it a graph, not a chain.
7. Common pitfalls¶
- ❗ Returning the full state from a node instead of a partial update. Nodes should return only the keys they changed. The engine merges into the existing state.
- ❗ Forgetting
STARTandENDedges. Every graph needs an entry edge fromSTARTand at least one terminal edge toEND. - ❗ Mutating state in place. Always return a new dict — the engine relies on immutability for checkpointing.
- ❗ Thinking LangGraph replaces LangChain. It doesn't — you still use LangChain models, prompts, parsers, retrievers, tools inside LangGraph nodes. LangGraph adds the orchestration layer on top.
- ❗ Compiling without a checkpointer when you need persistence. Without one, state is lost between
.invoke()calls — you can't have a chatbot.
8. When to use vs not use¶
| Use LangGraph when | Use plain LangChain (LCEL) when |
|---|---|
| You need loops or branching | Linear DAG is enough |
| You need pause/resume / HITL | One-shot request/response |
| State must persist across requests | Stateless or simple chat history |
| You're building a real agent | You're building a RAG pipeline |
| You want time-travel debugging | LangSmith tracing alone is sufficient |
9. Cheatsheet¶
# Install
# pip install langgraph langchain langchain-openai
# Core imports
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import MemorySaver
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.prebuilt import create_react_agent, ToolNode
# Build
builder = StateGraph(StateSchema)
builder.add_node("name", node_fn)
builder.add_edge(SRC, "name") # static edge
builder.add_conditional_edges( # router edge
"name",
router_fn,
{"opt1": "node_a", "opt2": "node_b", END: END},
)
graph = builder.compile(checkpointer=MemorySaver())
# Run
graph.invoke(initial_state, config={"configurable": {"thread_id": "abc"}})
graph.stream(initial_state, config=...)
graph.get_state(config) # current snapshot
graph.get_state_history(config) # all checkpoints
graph.update_state(config, {...}) # manually edit state
10. Q&A — recall test¶
-
Q: What's the single most important difference between LangChain (LCEL) and LangGraph? A: LCEL is a DAG (no cycles, implicit state). LangGraph is an arbitrary graph (cycles allowed) with explicit, checkpointable state.
-
Q: What does a node return? A: A partial state update — a dict containing only the keys it changed. The engine merges it into the running state.
-
Q: What is a
Reducer? A: A merge function applied when multiple updates write the same key (e.g.,add_messagesfor appending instead of replacing). Set viaAnnotated[T, reducer]on the state field. -
Q: Why do you need
compile()at all? A: It validates the graph (entry/exit edges, node references), binds a checkpointer, and returns aRunnableyou can.invoke()/.stream(). The builder itself isn't runnable. -
Q: Should I use LangGraph for a one-shot RAG question-answering app? A: Probably not — an LCEL chain is simpler. Reach for LangGraph when you need cycles, branching, state persistence, or HITL.
Practice¶
What does this print?
Expected: True
Pick LangGraph (not LCEL) when you need a loop
Expected: True
Quiz — Quick check¶
What you remember
Q1. What does LangGraph give you that LCEL doesn't?
- Cyclic workflows (loops, retries), branching, state persistence, human-in-the-loop
- Faster execution
- Smaller bundle size
- Better LLM models
Why: LCEL builds linear pipelines or DAGs. LangGraph adds stateful, cyclic workflows — essential for agents that retry, plan, or pause for approval.
Q2. What are the three core concepts in LangGraph?
- State, Nodes, Edges
- Models, Prompts, Tools
- Inputs, Outputs, Errors
- Chains, Agents, Tools
Why: State is the shared data passed between nodes. Nodes are functions that read state and return updates. Edges decide which node runs next based on the state.
Q3. When should you reach for LangGraph instead of LangChain?
- Multi-step agents with loops, branching logic, persistence, or human approval
- Single LLM calls
- Simple RAG
- Embedding-only workflows
Why: For linear "embed → search → answer" chains, LangChain is plenty. The moment you need a tool-using agent that loops until done, or a workflow with human-in-the-loop, you're in LangGraph territory.
Common doubts¶
Is LangGraph a separate library or part of LangChain?
Separate package (pip install langgraph) but built by the same team. They integrate seamlessly — LangGraph nodes are often LangChain Runnables. Use LangChain for the components (prompts, models, parsers); use LangGraph for the orchestration when you need cycles.
Why graphs and not just chains?
Chains are linear or branching DAGs. Real workflows have cycles: a tool-calling agent loops "decide → call tool → observe → decide" until done. Cycles need graphs. Plus state persistence — checkpoints between every step — comes naturally with graphs.
Can I migrate from LangChain agents to LangGraph?
Yes — LangGraph has compatible primitives. The migration usually simplifies your code because state and routing become explicit instead of implicit. LangChain's older AgentExecutor is being phased out in favor of LangGraph.