LangGraph has quietly become the production backbone for some of the most advanced AI agent systems shipped in 2026. Klarna, LinkedIn, Uber and Replit all run agent workflows on it, and the framework crossed 33,100 monthly Google searches in April 2026, eclipsing nearly every other Python agent toolkit on the market. This LangGraph tutorial walks through every step required to ship a stateful, checkpoint-backed AI agent in Python – from pip install to PostgreSQL-backed time travel – using LangGraph 1.2.0 released on May 11, 2026.
Unlike a basic LangChain chain, a LangGraph workflow is a typed state graph. Nodes mutate shared state, edges branch on conditions, and every transition is persisted so that an agent can pause, ask a human, or replay from any checkpoint. By the end of this guide you will have a working research agent that uses tool calls, streams partial output, survives a kernel restart, and can be debugged visually in LangGraph Studio.
Don't miss new tech stories on Google
Add Tech Insider once in the Google app and our stories appear in your news suggestions.
Why LangGraph in 2026: The Stateful Agent Shift
For most of 2023 and 2024 the LLM ecosystem treated agents as glorified for-loops: prompt the model, parse a tool call, execute it, and re-prompt. That pattern, popularised by the original ReAct paper, hit a wall the moment teams pushed it into production. Stateless retries lost context, long-running workflows died when a Kubernetes pod restarted, and human approval steps required custom middleware nobody wanted to maintain. LangGraph 1.2 – released by the LangChain team on May 11, 2026 – fixes all three problems by treating an agent run as a durable graph execution rather than a Python function call.
The core idea is simple. You define a StateGraph with a typed state schema, register nodes that read and write that state, and connect them with edges – some deterministic, some conditional. Every transition is automatically checkpointed through a pluggable saver (Memory, SQLite or Postgres), which makes pause/resume, time-travel debugging, and multi-instance horizontal scaling first-class features rather than aftermarket bolt-ons. This is the same machinery Klarna uses to keep its customer-service agent stateful across millions of concurrent sessions, and the same primitive LinkedIn uses to coordinate multi-agent hiring workflows.
What makes LangGraph a tutorial-worthy topic in April 2026 is the maturity curve. Three weeks before this guide was written, the team shipped the v1.2 release with content-block-aware streaming, improved interrupt() semantics, and full Python 3.10–3.14 support. The companion langgraph-cli 0.4.26 produces deployment-ready Docker images in under a minute, and LangGraph Studio gives you a visual graph debugger you can point at any local langgraph.json manifest. The framework finally feels like a finished product, which is why the GitHub repository now ranks alongside CrewAI and AutoGen as one of the three serious choices for production agents.
LangGraph vs LangChain vs CrewAI: Pick the Right Tool
Before installing anything, it helps to know exactly which problem LangGraph solves and which problems it does not. LangChain remains the toolkit layer – prompts, models, retrievers, output parsers – and LangGraph orchestrates them. CrewAI is a higher-level role/task abstraction good for quick demos, AutoGen leans into conversational multi-agent research, and the OpenAI Agents SDK is a thin wrapper around OpenAI’s own tool-calling primitives. The table below summarises the trade-offs as of April 2026.
| Framework | Best for | State model | Checkpoint/replay | Latest version | License |
|---|---|---|---|---|---|
| LangGraph 1.2 | Production stateful agents | Typed shared state | Built-in (Memory/SQLite/Postgres) | 1.2.0 (May 11, 2026) | MIT |
| LangChain | LLM component glue | Stateless chains | None native | 0.3.x line | MIT |
| CrewAI | Role-based teams | Implicit per-agent | None native | 0.x | MIT |
| AutoGen | Research dialogues | Message history | Limited | 0.x | MIT |
| OpenAI Agents SDK | OpenAI-only stacks | Thread-based | Provider-side | SDK 2026 | MIT |
If your application needs durable execution, human-in-the-loop approvals, fan-out parallelism, or time-travel debugging, pick LangGraph. If you are building a small chatbot that fits in one prompt, you do not need any of this and plain LangChain or even raw openai SDK calls are faster to ship.
Prerequisites and Version Requirements
This tutorial targets LangGraph 1.2.0 on Python 3.11 – the version with the broadest LSP, debugger and library ecosystem support in 2026. Make sure your environment matches the table below before you run a single line of code. Mixing older Python releases or pre-1.0 LangGraph builds will produce confusing type errors around state schemas and the new content-block streaming API.
| Dependency | Required version | Why |
|---|---|---|
| Python | 3.10, 3.11, 3.12, 3.13 or 3.14 | 3.9 was dropped in LangGraph 1.1 |
| langgraph | 1.2.0 | Stable v1 API, content-block streaming |
| langgraph-checkpoint-sqlite | 2.x | Durable local state |
| langgraph-checkpoint-postgres | 2.x | Production state |
| langgraph-cli[inmem] | 0.4.26 | Studio + local dev server |
| langchain-openai | 0.3.x | GPT model bindings |
| langchain-anthropic | 0.3.x | Claude Opus 4.6 bindings |
| OS | Linux, macOS 14+, Windows 11 + WSL2 | PostgresSaver needs libpq |
| Docker (optional) | 24+ | Local Postgres for checkpoints |
| OpenAI or Anthropic API key | Active billing | Required for model nodes |
You also need a basic understanding of Python type hints, since LangGraph 1.x leans heavily on TypedDict and Annotated reducers for state. If you have never used typing.Annotated before, skim the official Python documentation before continuing.
Step 1: Create the Project and Virtual Environment
Start by creating a clean project directory with a dedicated virtual environment. Mixing LangGraph into a global Python installation almost always breaks at deployment time because the framework pins specific versions of langchain-core that conflict with whatever else is sitting on your system.
mkdir langgraph-research-agent
cd langgraph-research-agent
python3.11 -m venv .venv
source .venv/bin/activate # On Windows: .venvScriptsactivate
python -m pip install --upgrade pip
Verify the Python version after activation. python --version should report 3.11.x or newer. If you accidentally landed on Python 3.9, recreate the virtual environment with an explicit interpreter path because LangGraph 1.2 will refuse to import.
Step 2: Install LangGraph 1.2 and Companion Packages
LangGraph is intentionally modular. The base langgraph package contains the graph engine, while checkpoint backends, the CLI, and prebuilt agents ship as separate distributions. Installing the full toolkit at once avoids the “missing checkpointer” runtime errors that catch most newcomers.
pip install
"langgraph==1.2.0"
"langgraph-cli[inmem]==0.4.26"
"langgraph-checkpoint-sqlite"
"langgraph-checkpoint-postgres"
"langchain-openai"
"langchain-anthropic"
"langchain-community"
"python-dotenv"
"tavily-python"
After the install, run pip list | grep langgraph. You should see at least four packages: langgraph, langgraph-cli, langgraph-checkpoint, and langgraph-prebuilt. The last one is pulled in transitively and provides create_react_agent, which we use in Step 7 as a baseline before building a custom graph.
Step 3: Configure API Keys and Environment Variables
LangGraph itself is model-agnostic, but the model nodes we will define need credentials. Create a .env file in the project root and load it with python-dotenv. Hard-coding keys directly in source files is the single most common security mistake we see in published LangGraph examples on GitHub.
# .env
OPENAI_API_KEY=sk-proj-...
ANTHROPIC_API_KEY=sk-ant-...
TAVILY_API_KEY=tvly-...
LANGSMITH_API_KEY=ls__... # optional, for tracing
LANGSMITH_TRACING=true # optional
LANGSMITH_PROJECT=langgraph-tutorial
Add .env to your .gitignore immediately. The Tavily key is optional – it powers the web-search tool in Step 5 – but you will need at least one model key (OpenAI or Anthropic) to run anything. LangSmith tracing is free for up to 5,000 traces per month and gives you a hosted timeline view of every node execution, which pays for itself within an hour.
Step 4: Define the State Schema
Every LangGraph application starts with a typed state schema. The state is the shared memory that flows through the graph: nodes read it, return partial updates, and LangGraph merges those updates using reducers declared via typing.Annotated. For a research agent the state typically contains the conversation messages plus any domain-specific fields like research notes or a final report.
# state.py
from typing import Annotated, TypedDict
from langgraph.graph.message import add_messages
from langchain_core.messages import BaseMessage
class AgentState(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
research_notes: list[str]
iteration: int
The add_messages reducer is the secret sauce. Instead of overwriting the message list at every step, it appends new messages and deduplicates by ID, which is exactly what you want for chat-style agents. For the research_notes and iteration fields we deliberately omit a reducer so nodes can overwrite them – a useful pattern when one node owns a piece of state outright.
Step 5: Define Tools the Agent Can Call
Tools are plain Python functions decorated with @tool from langchain_core. LangGraph does not invent its own tool format; it reuses the LangChain tool spec, which means every existing LangChain integration – Tavily, Wikipedia, SQL, Slack – works out of the box. We will give our research agent two tools: a web search via Tavily and a simple calculator for any numeric questions that come up.
# tools.py
import os
from langchain_core.tools import tool
from tavily import TavilyClient
tavily = TavilyClient(api_key=os.environ["TAVILY_API_KEY"])
@tool
def web_search(query: str) -> str:
"""Search the public web for a query and return a short summary."""
response = tavily.search(query=query, max_results=5, search_depth="basic")
snippets = [f"- {r['title']}: {r['content']}" for r in response["results"]]
return "n".join(snippets)
@tool
def calculator(expression: str) -> str:
"""Evaluate a basic arithmetic expression. Supports + - * / and parentheses."""
allowed = set("0123456789+-*/(). ")
if not set(expression) <= allowed:
return "Error: only digits and + - * / ( ) . are allowed."
try:
return str(eval(expression, {"__builtins__": {}}, {}))
except Exception as exc:
return f"Error: {exc}"
TOOLS = [web_search, calculator]
The calculator deliberately whitelists its input characters before calling eval, because raw eval on user-supplied strings is one of the OWASP Top 10 sins. A safer alternative in production is the asteval package or numexpr; for tutorial purposes the whitelist is sufficient.
Step 6: Bind Tools to the Model
LangGraph nodes are just Python callables that take the state and return a partial state. The most common node is a model node that calls an LLM with the current conversation. We use ChatOpenAI and bind our two tools so the model knows it is allowed to invoke them. The same pattern works with ChatAnthropic if you prefer Claude Opus 4.6.
# nodes.py
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage
from state import AgentState
from tools import TOOLS
SYSTEM_PROMPT = """You are a careful research assistant.
Always cite the URL or source when reporting a fact.
Use the web_search tool for current events and the calculator for math.
Stop calling tools once you have enough information and produce a final answer."""
llm = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
llm_with_tools = llm.bind_tools(TOOLS)
def call_model(state: AgentState) -> dict:
messages = [SystemMessage(content=SYSTEM_PROMPT)] + state["messages"]
response = llm_with_tools.invoke(messages)
return {
"messages": [response],
"iteration": state.get("iteration", 0) + 1,
}
The function returns a partial state update. We only touch messages and iteration; LangGraph merges them into the existing state using the reducers we declared earlier. Returning the full state from every node is a common beginner mistake that quietly breaks the add_messages deduplication and produces ballooning conversation histories.
Step 7: Build the StateGraph and Wire Tool Execution
With state, tools and the model node in place, we can wire them into an actual graph. The ToolNode helper from langgraph.prebuilt executes any pending tool calls on the last AI message and returns tool result messages. We loop between the model and the tool node until the model produces a response with no tool calls, then exit.
# graph.py
from langgraph.graph import StateGraph, START, END
from langgraph.prebuilt import ToolNode
from state import AgentState
from tools import TOOLS
from nodes import call_model
def route_after_model(state: AgentState) -> str:
last = state["messages"][-1]
if getattr(last, "tool_calls", None):
return "tools"
if state.get("iteration", 0) >= 10:
return END # hard safety stop
return END
builder = StateGraph(AgentState)
builder.add_node("model", call_model)
builder.add_node("tools", ToolNode(TOOLS))
builder.add_edge(START, "model")
builder.add_conditional_edges("model", route_after_model, ["tools", END])
builder.add_edge("tools", "model")
graph = builder.compile()
The route_after_model function is a classic conditional edge: based purely on state, it decides which node runs next. The hard iteration cap at 10 is critical – without it a misbehaving model could loop forever and burn through your API budget in minutes. We have seen production teams cap iterations at 25 with an additional cost tracker that aborts when spend exceeds $0.50 per request.
Step 8: Run the Agent and Inspect Output
The compiled graph is invocable like any LangChain runnable. Pass it an initial state with a HumanMessage and it will execute until the conditional edge routes to END. For a first run we recommend asking something that requires both tools, like a current-events question with a numeric follow-up.
# main.py
from dotenv import load_dotenv
from langchain_core.messages import HumanMessage
load_dotenv()
from graph import graph # import after env vars are loaded
def main() -> None:
initial = {
"messages": [HumanMessage(content=
"What was Nvidia's Q1 2026 revenue, and how much is that per day?")],
"research_notes": [],
"iteration": 0,
}
final = graph.invoke(initial)
print("n--- FINAL ANSWER ---")
print(final["messages"][-1].content)
print(f"nIterations used: {final['iteration']}")
if __name__ == "__main__":
main()
Run python main.py. With a working OpenAI key and a Tavily key, the expected console output looks similar to the example below. The exact text varies because the model and the live web both change between runs.
--- FINAL ANSWER ---
Nvidia reported Q1 FY2026 revenue of approximately $44.1 billion
(source: Nvidia investor relations Q1 FY2026 release). Spread across
the 91 days in the quarter, that is roughly $484.6 million per day
(44.1e9 / 91).
Iterations used: 3
Three iterations is healthy: one to call web_search, one to call calculator, and one to produce the final synthesised answer. If you see ten iterations you have probably forgotten to bind the tools or your system prompt is contradictory.
Step 9: Add Durable Checkpointing with SqliteSaver
So far the agent forgets everything between runs. To make it survive process restarts, attach a checkpointer at compile time. LangGraph ships three first-party savers – MemorySaver for tests, SqliteSaver for local development, and PostgresSaver for production. We start with SQLite because it requires no external services.
# graph.py (updated)
from langgraph.graph import StateGraph, START, END
from langgraph.prebuilt import ToolNode
from langgraph.checkpoint.sqlite import SqliteSaver
from state import AgentState
from tools import TOOLS
from nodes import call_model
def route_after_model(state):
last = state["messages"][-1]
return "tools" if getattr(last, "tool_calls", None) else END
def build_graph(db_path: str = "checkpoints.sqlite"):
builder = StateGraph(AgentState)
builder.add_node("model", call_model)
builder.add_node("tools", ToolNode(TOOLS))
builder.add_edge(START, "model")
builder.add_conditional_edges("model", route_after_model, ["tools", END])
builder.add_edge("tools", "model")
checkpointer = SqliteSaver.from_conn_string(db_path)
return builder.compile(checkpointer=checkpointer)
Every invocation now needs a thread_id in its config so the saver knows which conversation thread to read and write. Threads are how LangGraph isolates concurrent users – give each user (or each chat session) a distinct UUID and the checkpoints stay separated automatically.
from graph import build_graph
graph = build_graph()
config = {"configurable": {"thread_id": "user-42"}}
graph.invoke({"messages": [HumanMessage(content="Hi, my name is Ada.")]},
config=config)
graph.invoke({"messages": [HumanMessage(content="What is my name?")]},
config=config)
The second call answers with “Ada” because the checkpointer restored the prior message history before the model node ran. Restart Python and the answer is still correct as long as checkpoints.sqlite exists on disk.
Step 10: Stream Output in Real Time
For any user-facing application you want streaming, not blocking. LangGraph 1.2 supports three stream modes – values, updates and messages – each suited to a different consumer. values emits full snapshots of state, updates emits only the diff, and messages streams individual content blocks from the model.
for chunk in graph.stream(
{"messages": [HumanMessage(content="Summarise the latest on the Vera Rubin GPU.")]},
config=config,
stream_mode="updates",
):
for node_name, node_update in chunk.items():
print(f"[{node_name}] -> keys: {list(node_update.keys())}")
print("n--- token stream ---")
for msg_chunk, metadata in graph.stream(
{"messages": [HumanMessage(content="One more question about Blackwell.")]},
config=config,
stream_mode="messages",
):
if msg_chunk.content:
print(msg_chunk.content, end="", flush=True)
The messages mode is what you wire to a Server-Sent Events endpoint when building a ChatGPT-style web UI. The updates mode is what you log to your observability stack because it shows exactly which node emitted which change.
Step 11: Add a Human-in-the-Loop Approval Step
One of LangGraph’s flagship features is built-in human-in-the-loop. Call interrupt() inside any node and the graph pauses, persists state, and waits until you resume it with a Command(resume=...). This is how Klarna’s customer-service agent escalates refund requests to a human operator without losing the conversation.
# hil_nodes.py
from langgraph.types import interrupt, Command
from state import AgentState
def review_before_tool(state: AgentState) -> dict:
last = state["messages"][-1]
pending = getattr(last, "tool_calls", []) or []
if not pending:
return {}
decision = interrupt({
"question": "Approve tool call?",
"tool_calls": [{"name": tc["name"], "args": tc["args"]} for tc in pending],
})
if decision == "approve":
return {}
return {"messages": [{"role": "tool", "content": "Cancelled by human reviewer"}]}
Insert this node on the edge between model and tools. When the graph hits interrupt(), the call returns a special sentinel and the run halts. Your application – say, a Slack bot or a web dashboard – reads the pending decision, asks a human, and resumes the run by calling graph.invoke(Command(resume="approve"), config=config).
Step 12: Fan-Out with the Send API
For map/reduce style workflows – researching ten subtopics in parallel, scoring fifty candidates, summarising a long document chunk by chunk – LangGraph provides the Send API. A conditional edge can return a list of Send objects, each dispatching to the same node with different input. The runtime executes them concurrently and collects results back into shared state.
from langgraph.types import Send
def fanout_subtopics(state: AgentState) -> list[Send]:
subtopics = state.get("subtopics", [])
return [Send("research_one", {"subtopic": s}) for s in subtopics]
def research_one(payload: dict) -> dict:
note = f"[{payload['subtopic']}] researched" # call your tools here
return {"research_notes": [note]}
# In the builder:
builder.add_node("research_one", research_one)
builder.add_conditional_edges("planner", fanout_subtopics, ["research_one"])
The receiving node needs a reducer on its target field – typically operator.add on a list – so concurrent appends merge cleanly. Without a reducer you will hit a runtime error about overlapping updates. This pattern alone is why teams move from CrewAI to LangGraph: parallelism is a first-class graph primitive rather than something you bolt on with asyncio.gather.
Step 13: Visualise and Deploy with LangGraph Studio
The final step is to take your agent off your laptop. The langgraph-cli package ships a local dev server plus a managed Studio frontend. Create a langgraph.json manifest at the project root that points at your compiled graph and the saver of your choice.
{
"dependencies": ["."],
"graphs": {
"agent": "./graph.py:build_graph"
},
"env": ".env",
"python_version": "3.11"
}
Run langgraph dev from the project root. The CLI launches a local server on http://127.0.0.1:2024 and prints a Studio URL like https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024. Open it in the browser and you get a live graph diagram, a state inspector for every checkpoint, and a replay button that re-executes from any prior step – exactly the time-travel feature LangGraph is famous for.
For production, langgraph build -t my-agent:latest produces a Docker image that listens on the same protocol as Studio. Push it to your registry, deploy on Kubernetes or Fly.io, point your application at the resulting HTTP endpoint, and you have a self-hosted LangGraph runtime with PostgresSaver-backed durability.
Switching to PostgresSaver for Production
SqliteSaver is fine for single-process work but breaks under concurrent writes from multiple containers. For real deployments swap to PostgresSaver, which uses connection pooling and proper transactions. The migration is two lines.
from langgraph.checkpoint.postgres import PostgresSaver
from psycopg_pool import ConnectionPool
pool = ConnectionPool(
conninfo="postgresql://user:pass@localhost:5432/langgraph",
max_size=20,
kwargs={"autocommit": True, "prepare_threshold": 0},
)
checkpointer = PostgresSaver(pool)
checkpointer.setup() # one-time, creates the tables
graph = builder.compile(checkpointer=checkpointer)
The .setup() call is idempotent and creates the four tables LangGraph needs: checkpoints, checkpoint_writes, checkpoint_blobs, and checkpoint_migrations. For an Amazon RDS or Supabase instance with default settings, expect roughly 1–2 ms of latency per checkpoint write – negligible compared to the model call.
Common Pitfalls and How to Fix Them
Five mistakes account for nearly every “LangGraph is broken” thread on GitHub. Memorise these before you ship.
- Forgetting the
add_messagesreducer. Without it, every node overwrites the message list. The model loses memory after the first tool call and you spend hours wondering why the agent has amnesia. - Returning the full state from a node. Return only the keys you changed. Returning everything bypasses reducers and produces silent state corruption.
- No iteration cap on conditional edges. A model with poor stop conditions will loop forever. Always add a hard guard like
iteration >= 25 -> END. - Using MemorySaver in production. The first time your container restarts, every active conversation evaporates. SQLite for laptops, Postgres for everything else.
- Mixing async and sync nodes carelessly. If any node is
async defthe whole graph runs on the event loop and synchronous I/O blocks the loop. Pick one mode and stay consistent. - Schema drift across checkpoint versions. Adding a required field to
AgentStateafter you have checkpoints in production will crash old runs. Use Optional fields and defensivestate.get()reads.
Advanced Tips for Production Agents
Once your graph compiles and your tests pass, the next set of improvements lifts you from working to genuinely reliable. The four patterns below are the ones that show up in Klarna, LinkedIn and Uber’s published LangGraph postmortems.
- Subgraphs for modularity. A complex agent quickly outgrows a single flat graph. Wrap a research workflow as its own subgraph and embed it as a node in your top-level orchestrator. Subgraphs are compiled independently and can have their own state schema.
- Per-thread retention policies. Long-running threads accumulate huge message histories. Use the built-in
RemoveMessageprimitive in a periodic compaction node to trim or summarise history while keeping the latest N turns verbatim. - Cost and latency tracking via LangSmith. Hook the
LANGSMITH_TRACING=trueenvironment variable on and every node call is traced with token counts, dollar costs and timing. Without this you are flying blind on production economics. - Structured tool outputs with Pydantic. Return Pydantic models from tools rather than free-form strings. The model receives clean JSON, which dramatically reduces the “forgot to mention the price” class of errors in downstream reasoning.
- Idempotent nodes. Design every node to be safe to rerun. Checkpoints replay nodes on resume, and a non-idempotent payment node will double-charge customers. Use external idempotency keys for any side-effectful operation.
Troubleshooting: 8 Errors and Their Fixes
| Error message | Root cause | Fix |
|---|---|---|
InvalidUpdateError: At key 'messages': can receive only one value | Two parallel nodes wrote to messages without a reducer that supports concurrent appends. | Confirm add_messages annotation is present; use Send API for fan-out instead of duplicate edges. |
GraphRecursionError: Reached recursion limit | Model never stops calling tools. Iteration cap missing. | Add an iteration guard in your routing function or pass {"recursion_limit": 50} in config. |
Cannot find checkpoint for thread_id | Trying to resume a thread on a different saver or DB than where it was written. | Use the same connection string everywhere; verify .setup() has been run. |
BadRequestError: 400 Tool input invalid | Tool schema mismatch between bound tools and what the model produced. | Pin the langchain-openai and langgraph versions; do not mix major versions. |
RuntimeError: This event loop is already running | Calling graph.invoke() inside an existing async context. | Switch to await graph.ainvoke(...) in async code. |
ImportError: cannot import name 'SqliteSaver' | SQLite saver moved to its own package in LangGraph 1.x. | pip install langgraph-checkpoint-sqlite. |
ValidationError: input should be a valid string | State field declared as str but a node returned None. | Use Optional[str] and guard with defaults in node functions. |
psycopg.OperationalError: SSL connection has been closed | Long-lived Postgres connection killed by a load balancer. | Set keepalives=1 in the connection string and enable connection pool recycling at 5 minutes. |
Pricing: LangGraph Cloud and Self-Hosted Costs
LangGraph itself is MIT-licensed and free to self-host. The optional managed runtime, LangGraph Platform, has tiers documented on the LangChain pricing page. Most teams start on the developer plan during prototyping and migrate to a paid plan once they need autoscaling or SLA-backed uptime. Either way the dominant cost in production is the model API spend, not the LangGraph infrastructure.
- Self-hosted on your own infra: $0 LangGraph license + Postgres + container compute. Typical cost for a small agent service: $30–80/month of cloud spend.
- LangGraph Platform Developer tier: free, suited for prototyping. Limited compute and one user.
- LangGraph Platform paid tiers: usage-based pricing on node executions; check the official pricing page for current rates.
- LangSmith tracing: 5,000 traces/month free, then per-trace pricing. Worth it for any team larger than one developer.
Complete Working Project: File Layout
For reference, the final directory layout of the agent we built in this tutorial looks like the structure below. Each file has a single responsibility, which is the layout pattern recommended in the official LangGraph examples repository.
langgraph-research-agent/
âââ .env # API keys (gitignored)
âââ .gitignore
âââ langgraph.json # CLI manifest for Studio / langgraph build
âââ pyproject.toml # optional, for packaging
âââ requirements.txt # pinned dependencies
âââ state.py # AgentState TypedDict
âââ tools.py # web_search + calculator
âââ nodes.py # call_model node and prompt
âââ hil_nodes.py # interrupt-based approval node
âââ graph.py # StateGraph builder + checkpointer
âââ main.py # entry point
Pin every dependency in requirements.txt. LangGraph’s release cadence is fast – three minor versions shipped between January and May 2026 alone – and an unpinned install will eventually break in CI.
Extending the Agent: Three Next Steps
The 13-step agent above is a complete production starter, but most real systems need at least one of the extensions below. Each is a few hours of work given the foundation we have built.
- RAG over your own documents. Replace the
web_searchtool with a retriever that hits a vector database – pgvector, Pinecone, Qdrant, or Chroma – and add a re-ranker node before the model. The graph topology stays identical. - Multi-agent supervisor pattern. Build a top-level supervisor graph whose nodes are themselves agent subgraphs (Researcher, Writer, Critic). This is the same architecture LinkedIn published in its 2026 engineering blog on AI-powered hiring workflows.
- Slack or Telegram interface. Wire
graph.stream(stream_mode="messages")to a Slack bolt app or a Telegram bot, using thethread_idas the channel-or-DM identifier. You get a stateful Slack assistant in roughly 100 lines of glue code.
Frequently Asked Questions
Is LangGraph the same as LangChain?
No. LangChain is a toolkit of LLM components – prompts, models, retrievers, output parsers – and LangGraph is a stateful workflow engine that orchestrates those components. A LangGraph node usually uses LangChain primitives, but the two projects have distinct release cycles and APIs. Most modern agent codebases pull both in side by side.
Which Python version should I use for LangGraph 1.2?
Python 3.11 or 3.12 are the safest choices in 2026. The framework officially supports 3.10 through 3.14, but 3.9 was dropped in LangGraph 1.1 and many ecosystem libraries (notably typing helpers) ship newer features first on 3.11+. Avoid 3.13 until your other dependencies confirm support.
Can I use LangGraph with Claude or Gemini instead of OpenAI?
Yes. Swap ChatOpenAI for ChatAnthropic (Claude Opus 4.6, Sonnet 4.6 or Haiku 4.5) or ChatGoogleGenerativeAI (Gemini family). All three model wrappers support .bind_tools() in their 2026 releases, so the rest of the graph code is unchanged.
How is LangGraph different from CrewAI?
CrewAI models agents as roles with tasks; LangGraph models them as nodes in a typed state graph. CrewAI is faster to prototype, LangGraph offers far more control over execution, persistence and parallelism. Teams routinely start on CrewAI and migrate to LangGraph once they need durable execution or human-in-the-loop. See our CrewAI tutorial for the alternative approach.
Does LangGraph support async?
Yes. Every public API has an async counterpart – graph.ainvoke(), graph.astream(), AsyncSqliteSaver, AsyncPostgresSaver. Pick async if you are inside a FastAPI or aiohttp service so node I/O does not block the event loop. Avoid mixing sync and async nodes in the same graph.
How do I deploy a LangGraph agent to production?
Three options: (1) self-host with langgraph build producing a Docker image you run on Kubernetes, ECS or Fly.io; (2) use the managed LangGraph Platform hosted by LangChain; or (3) embed the graph inside an existing FastAPI service. For any option, swap SqliteSaver for PostgresSaver first and add LangSmith tracing for observability.
Is there a JavaScript or TypeScript version of LangGraph?
Yes. @langchain/langgraph on npm tracks roughly the same API as the Python package. The Python release is usually a week or two ahead on new features, so production teams that need feature parity tend to standardise on Python.
What is time-travel debugging in LangGraph?
Because every node transition is checkpointed, you can request the full state at any prior step using graph.get_state_history(config) and re-execute from that point with a modified input. This is invaluable for reproducing nondeterministic bugs and for “what-if” exploration of alternative agent paths. LangGraph Studio exposes this feature visually.
Related Coverage
Further Reading on tech-insider.org
- Build a RAG Chatbot with LangChain Python in 13 Steps – the perfect prerequisite if you are new to the LangChain ecosystem.
- CrewAI Multi-Agent Tutorial – when role-based abstractions beat explicit graphs.
- OpenAI Agents SDK Tutorial – the OpenAI-native alternative to LangGraph.
- Claude API Tutorial in 13 Steps – wire Anthropic models into any LangGraph node.
- FastAPI REST API Tutorial – the recommended way to expose your LangGraph agent over HTTP.
- Anthropic vs OpenAI 2026 – pick the right model provider for your agent.
- AI Coding Tools Guide – broader context on the 2026 AI developer stack.
External Authority Sources
- Official LangGraph documentation – canonical API reference.
- LangGraph GitHub repository – source, issues and release notes.
- LangGraph on PyPI – version history.
- LangChain engineering blog – case studies from Klarna, LinkedIn and Uber.
- Python.org downloads – get Python 3.11 or 3.12.
Conclusion: LangGraph as the Default Agent Runtime
LangGraph 1.2 is the agent framework the industry settled on for 2026 because it solves the unglamorous problems – durability, replay, human approvals, fan-out – that every production team eventually runs into. The 13-step agent in this guide is the same skeleton that powers Klarna’s customer service, LinkedIn’s hiring workflows and Uber’s internal copilots; only the tools, prompts and supervising graphs differ. Clone the repo layout, swap in your own tools, point Postgres at it, and you have a stateful AI agent ready to scale.
If you take one thing away, let it be this: agents are not functions, they are graphs of state transitions, and LangGraph is the first widely adopted Python framework that treats them that way. Start with MemorySaver while you are exploring, move to SqliteSaver as soon as you want to survive a kernel restart, and switch to PostgresSaver on the day you deploy. Everything else – streaming, interrupts, time-travel – is the same code you wrote in Step 7.





