If you have ever built an AI agent with raw Python and hit a wall at "how do I keep state across steps, branch on the model's output, and run multiple agents in sequence", LangGraph is the answer you wish you had six months ago.
LangGraph is the graph-based orchestration layer built on top of LangChain. It turns an agent from a fragile while loop into a stateful, debuggable, multi-agent workflow. According to Pluralsight's 2026 Tech Learning Pulse, interest in LangGraph jumped 288% this year — faster than any other AI framework. This guide walks you from empty repo to a working multi-agent research assistant.
1. What Is LangGraph?
LangGraph models your AI application as a directed graph. Instead of one function calling the next, you define:
- State — a typed dictionary that flows through every node
- Nodes — functions that read state, do work, and return updated state
- Edges — connections between nodes (always-on or conditional)
- Checkpoints — saved snapshots that let you resume, branch, or rewind
The mental model is closer to a workflow engine (Airflow, Temporal) than to a chatbot. That is the point: production agents need resumability, observability, and deterministic control — not just a clever prompt.
2. Why Use LangGraph in 2026?
Three reasons:
- Multi-agent is the new default. Pluralsight ranks LangGraph as the #1 fastest-growing tech learning topic of 2026 (+288% YoY). Production teams use multi-agent patterns to split reasoning, tool use, and verification across specialized workers.
- It solves the state problem. A naive LangChain agent keeps state in a global list. LangGraph gives you a typed, validated state object that every node sees.
- It pairs with LangChain without replacing it. You still use ChatModels, Tools, and Retrievers — LangGraph just controls the flow between them.
3. Install LangGraph
LangGraph runs on Python 3.10 or later. Check your version first:
python --version
Install LangGraph with the standard LangChain companion packages:
pip install langgraph langchain langchain-openai python-dotenv tavily-python
What you get:
langgraph— the graph runtimelangchain+langchain-openai— model adapterspython-dotenv— loads your API keys from.envtavily-python— a web search tool built for agents
Set your keys in a .env file:
OPENAI_API_KEY=sk-...
TAVILY_API_KEY=tvly-...
Verify the install:
python -c "import langgraph; print(langgraph.__version__)"
You should see 0.3.x or later.
4. Your First State Graph
LangGraph has two core concepts: StateGraph (a builder for the graph structure) and a typed state schema (what flows through the graph). The simplest possible example is a two-node pipeline.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
# 1. Define the state schema
class State(TypedDict):
input: str
output: str
# 2. Define node functions
def step_one(state: State):
return {"output": state["input"].upper()}
def step_two(state: State):
return {"output": state["output"] + " — done"}
# 3. Build the graph
builder = StateGraph(State)
builder.add_node("step_one", step_one)
builder.add_node("step_two", step_two)
# 4. Wire edges
builder.add_edge(START, "step_one")
builder.add_edge("step_one", "step_two")
builder.add_edge("step_two", END)
# 5. Compile and run
graph = builder.compile()
result = graph.invoke({"input": "hello"})
print(result)
# {'input': 'hello', 'output': 'HELLO — done'}
Five steps you will repeat for every LangGraph app: define state, write nodes, register nodes, wire edges, compile. The graph object is callable — graph.invoke(...) runs it end to end.
5. Adding Tools
An agent that cannot do anything outside its training data is just a chatbot. Tools give your nodes real abilities: web search, file read, API call, calculator. Here is the canonical pattern: a node that decides whether to call a tool, and a node that executes it.
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, ToolMessage
from langgraph.graph import StateGraph, START, END
from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages
class State(TypedDict):
messages: Annotated[list, add_messages]
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# Define a tool
def get_weather(city: str) -> str:
"""Look up the current weather for a city."""
# In production, call a real API
return f"It is 22°C and sunny in {city}."
llm_with_tools = llm.bind_tools([get_weather])
def chatbot(state: State):
return {"messages": [llm_with_tools.invoke(state["messages"])]}
def tool_node(state: State):
last = state["messages"][-1]
tool_map = {"get_weather": get_weather}
result = tool_map[last.tool_calls[0]["name"]](**last.tool_calls[0]["args"])
return {"messages": [ToolMessage(content=result, tool_call_id=last.tool_calls[0]["id"])]}
def should_continue(state: State):
last = state["messages"][-1]
return "tools" if last.tool_calls else END
builder = StateGraph(State)
builder.add_node("chatbot", chatbot)
builder.add_node("tools", tool_node)
builder.add_edge(START, "chatbot")
builder.add_conditional_edges("chatbot", should_continue, {"tools": "tools", END: END})
builder.add_edge("tools", "chatbot")
graph = builder.compile()
result = graph.invoke({"messages": [HumanMessage(content="What is the weather in Tokyo?")]})
for m in result["messages"]:
print(m.content)
Two patterns worth noticing:
Annotated[list, add_messages]— this is a reducer. It tells LangGraph how to merge new messages into state (append, not overwrite). LangGraph ships several reducers; you can write your own.add_conditional_edges— the router function returns a node name (orEND). This is how you implement the "model decides what to do next" loop.
6. Building a Multi-Agent System
Single-agent loops work for short tasks. The moment a task has phases — research, then analyze, then write — you want specialized agents. LangGraph makes this a graph, not a mess of if statements.
Below is a three-agent research assistant: a Researcher that searches the web, an Analyst that interprets the findings, and a Writer that produces the final report. State flows through all three.
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage
from tavily import TavilyClient
import os
class State(TypedDict):
topic: str
messages: Annotated[list, add_messages]
research: str
draft: str
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
tavily = TavilyClient(api_key=os.environ["TAVILY_API_KEY"])
def researcher(state: State):
"""Web search agent — gathers raw material."""
results = tavily.search(query=state["topic"], max_results=5)
summary = "\n".join(f"- {r['title']}: {r['content'][:200]}" for r in results["results"])
return {"research": summary, "messages": [SystemMessage(content=f"Research gathered:\n{summary}")]}
def analyst(state: State):
"""Reads research, produces key findings."""
prompt = f"Based on this research, list the 5 most important findings:\n{state['research']}"
response = llm.invoke([HumanMessage(content=prompt)])
return {"messages": [response], "research": state["research"] + "\n\nFindings:\n" + response.content}
def writer(state: State):
"""Produces the final report."""
prompt = f"Write a 200-word report on {state['topic']} using this analysis:\n{state['research']}"
response = llm.invoke([HumanMessage(content=prompt)])
return {"draft": response.content, "messages": [response]}
builder = StateGraph(State)
builder.add_node("researcher", researcher)
builder.add_node("analyst", analyst)
builder.add_node("writer", writer)
builder.add_edge(START, "researcher")
builder.add_edge("researcher", "analyst")
builder.add_edge("analyst", "writer")
builder.add_edge("writer", END)
result = builder.compile().invoke({"topic": "LangGraph adoption in 2026", "messages": []})
print(result["draft"])
This is a real working pipeline. Each node has one job; state carries data forward. You can swap any node without touching the others.
7. Conditional Routing
Real agents rarely follow a fixed path. They branch: "if the user asked a question, route to Q&A; if they asked to write code, route to the coder." Use add_conditional_edges with a router function:
from typing import Literal
def route_after_research(state: State) -> Literal["analyst", "writer"]:
# If research is rich, send to analyst first
if len(state["research"]) > 500:
return "analyst"
return "writer"
builder.add_conditional_edges(
"researcher",
route_after_research,
{"analyst": "analyst", "writer": "writer"}
)
The third argument is an explicit mapping — safer than relying on string literals scattered across your code.
8. Adding Memory
By default, each graph.invoke() starts fresh. To make your agent remember past conversations, add a checkpointer:
from langgraph.checkpoint.memory import MemorySaver
memory = MemorySaver()
graph = builder.compile(checkpointer=memory)
config = {"configurable": {"thread_id": "user-123"}}
# First turn
graph.invoke({"messages": [HumanMessage(content="My name is Alex.")]}, config=config)
# Second turn — agent remembers
result = graph.invoke({"messages": [HumanMessage(content="What is my name?")]}, config=config)
print(result["messages"][-1].content)
# "Your name is Alex."
The thread_id is your conversation ID. Swap MemorySaver for SqliteSaver or PostgresSaver to persist across restarts.
9. Tips for Production
Tip 1: Keep Nodes Pure When Possible
Nodes should take state, do work, return state. Avoid hidden globals, side effects, or random seeds that change between runs. Pure nodes are easier to test and debug.
Tip 2: Use a Typed State Schema
TypedDict with reducers gives you type safety and makes the data flow explicit. Avoid passing untyped dictionaries between nodes — you will lose track of what each step produced.
Tip 3: Visualize Before Shipping
Every compiled graph can be visualized:
from langgraph.graph import StateGraph
# ... after compiling ...
print(graph.get_graph().draw_mermaid())
Paste the output into a Mermaid renderer. Visualizing catches loops and dead ends before they hit production.
Tip 4: Stream Long-Running Agents
Multi-agent workflows can take minutes. Use streaming to surface intermediate progress:
for chunk in graph.stream({"topic": "AI agents", "messages": []}, stream_mode="updates"):
print(chunk)
Tip 5: Add a Human-in-the-Loop Node
For sensitive workflows (payments, deployments, deletions), insert a node that pauses for human approval:
from langgraph.types import interrupt
def approval_node(state: State):
decision = interrupt("Approve deployment? (yes/no)")
return {"messages": [HumanMessage(content=decision)]}
The graph halts until a human responds via graph.invoke(Command(resume="yes"), config).
10. Common Mistakes
Mistake 1: Forgetting a Reducer on List Fields
If your state has a messages: list without a reducer, every node that returns a message will overwrite the list. You will see only the last message. Use Annotated[list, add_messages].
Mistake 2: Infinite Loops
A conditional edge that never returns END will run forever. Always have a hard stop: a maximum iteration count, a final node, or an explicit END branch.
Mistake 3: Mixing LLM and Tool State
Keep model text in messages and structured data in separate state fields (research, draft). If everything goes into messages, downstream nodes will be parsing prose to extract numbers.
Mistake 4: Not Persisting Threads
MemorySaver vanishes on restart. For any user-facing chatbot, use SqliteSaver or PostgresSaver. Without persistence, your "memory" feature is just session cache.
Mistake 5: Overusing Multi-Agent
Multi-agent has overhead: each agent is a separate LLM call. For a one-step task, a single prompt is faster and cheaper. Use multi-agent when phases genuinely differ — research vs writing, code review vs deployment.
Frequently Asked Questions
Is LangGraph replacing LangChain?
No. LangGraph sits on top of LangChain. You still use ChatModels, Tools, Retrievers, and Embeddings from LangChain. LangGraph controls how those components are wired together.
Do I need LangChain to use LangGraph?
Technically no — LangGraph nodes can be any Python function. But in practice, you almost always pair it with LangChain for models and tools. Pure Python nodes without LangChain lose the model integration benefit.
How does LangGraph compare to AutoGen or CrewAI?
AutoGen and CrewAI are opinionated multi-agent frameworks with predefined role patterns. LangGraph is lower-level — you define the graph structure yourself. This gives more control but more setup. Pick LangGraph when you need custom routing; pick CrewAI when a standard role-play pattern fits.
Can LangGraph handle streaming output?
Yes. Use graph.stream(...) instead of graph.invoke(...). Stream mode "updates" yields each node's output; "values" yields the full state after each step. For token-level streaming from the LLM, use "messages" mode.
Does LangGraph work with open-source models?
Yes. Any LangChain-compatible ChatModel works — including Ollama, Anthropic Claude, Mistral, and local HuggingFace models. Just swap ChatOpenAI for ChatOllama or ChatAnthropic.
How do I deploy a LangGraph app?
Three options: (1) langgraph up for a local dev server, (2) LangGraph Cloud for managed hosting, (3) wrap graph.ainvoke() in a FastAPI endpoint and deploy anywhere. Option 3 gives the most control and zero vendor lock-in.
Bottom Line
LangGraph is the orchestration layer you reach for when an agent outgrows a single LLM loop. It gives you typed state, conditional routing, persistent memory, and the ability to compose multiple specialized agents — without giving up LangChain's model and tool ecosystem.
Start with a two-node pipeline. Add a tool. Add a router. Add a checkpointer. By the time you have four nodes, the patterns will click and you will not go back to while True: llm.invoke(...).