All posts
Agentic AI10 min read·

LangGraph Tutorial: Build Stateful AI Agents in Python (2026 Guide)

LangGraph is the production standard for AI agents in 2026. This guide covers state, nodes, edges, memory, and conditional branching with real working code.

MF

Muhammad Farhan

AI Engineer · Founder of Datraxa

If you have been building AI agents with plain LangChain chains, you have likely hit the same wall: the moment an agent needs to loop, branch, or remember what it did last turn, the chain abstraction breaks down. LangGraph is how that problem gets solved. It is the graph-based extension of LangChain that adds cycles, persistent state, and conditional routing to agent workflows. By 2026 it has become the standard framework for production agentic systems. This is a complete guide to using it.

Why LangGraph Instead of Plain LangChain

LangChain LCEL chains are linear: input flows through a sequence of steps and produces output. That model works for simple pipelines. It breaks for agents, which need to act, observe results, decide what to do next, and sometimes loop back to earlier steps. LangGraph models agent execution as a directed graph where nodes are functions and edges are transitions. Graphs can have cycles. Chains cannot.

FrameworkControl flowStateBest for
LangChain chainLinear onlyNone (stateless)Simple pipelines, single-pass tasks
LangGraph graphCycles + branchingFull persistent stateAgents, multi-step workflows, memory

The other critical difference is state. In LangGraph, every node reads from and writes to a shared state object that persists across the entire graph execution. You always know what the agent has done, what tools it called, and what it found. Debugging a LangGraph agent is dramatically easier than debugging a LangChain chain because the state is explicit.

Core Concepts: State, Nodes, Edges

State

State is a TypedDict that flows through every node in your graph. You define it upfront and every node can read from and write updates back to it. The graph merges updates automatically using the reducer you specify. For message history, the standard pattern is to use operator.add as the reducer so new messages are appended rather than replacing old ones.

from typing import TypedDict, Annotated, Sequence
from langchain_core.messages import BaseMessage
import operator

class AgentState(TypedDict):
    messages: Annotated[Sequence[BaseMessage], operator.add]
    current_task: str
    tool_results: list

Nodes

Nodes are plain Python functions (or async functions) that take the current state and return a dictionary of updates to apply. The return value does not replace the state - it is merged in. You only return the keys you want to update.

from langchain_groq import ChatGroq

llm = ChatGroq(model="llama-3.3-70b-versatile", temperature=0)
llm_with_tools = llm.bind_tools(tools)

def agent_node(state: AgentState) -> dict:
    response = llm_with_tools.invoke(state["messages"])
    return {"messages": [response]}  # appended via operator.add

Edges

Edges connect nodes. Fixed edges always route to the same destination. Conditional edges call a function that inspects the state and returns the name of the next node to go to. This is where branching logic lives.

from langgraph.graph import END

def should_continue(state: AgentState) -> str:
    last = state["messages"][-1]
    if last.tool_calls:
        return "tools"   # route to tool execution
    return END           # finish

Building a Full ReAct Agent

The ReAct pattern (Reason + Act) is the foundation of most production agents. The agent reasons about what to do, calls a tool, observes the result, reasons again, and continues until it has an answer. Here is a complete working implementation using Groq for fast inference and DuckDuckGo search as a tool.

from langgraph.graph import StateGraph, END
from langgraph.prebuilt import ToolNode
from langchain_groq import ChatGroq
from langchain_community.tools import DuckDuckGoSearchRun
from typing import TypedDict, Annotated, Sequence
from langchain_core.messages import BaseMessage, HumanMessage
import operator

# 1. Define state
class AgentState(TypedDict):
    messages: Annotated[Sequence[BaseMessage], operator.add]

# 2. Define tools and LLM
tools = [DuckDuckGoSearchRun()]
llm = ChatGroq(model="llama-3.3-70b-versatile", temperature=0)
llm_with_tools = llm.bind_tools(tools)

# 3. Define nodes
def agent_node(state: AgentState) -> dict:
    response = llm_with_tools.invoke(state["messages"])
    return {"messages": [response]}

def should_continue(state: AgentState) -> str:
    if state["messages"][-1].tool_calls:
        return "tools"
    return END

# 4. Build and compile graph
graph = StateGraph(AgentState)
graph.add_node("agent", agent_node)
graph.add_node("tools", ToolNode(tools))
graph.set_entry_point("agent")
graph.add_conditional_edges("agent", should_continue)
graph.add_edge("tools", "agent")
app = graph.compile()

# 5. Run
result = app.invoke({
    "messages": [HumanMessage(content="What is the latest news on AI agents?")]
})
print(result["messages"][-1].content)

Adding Memory with Checkpointing

By default LangGraph agents have no memory between runs. Each invocation starts fresh. Adding a checkpointer gives your agent persistent memory - it can remember previous conversations and pick up where it left off. The simplest checkpointer is SQLite, which requires no external infrastructure.

from langgraph.checkpoint.sqlite import SqliteSaver

# Create a SQLite-backed checkpointer
with SqliteSaver.from_conn_string("agent_memory.db") as checkpointer:
    app = graph.compile(checkpointer=checkpointer)

    # thread_id links runs into a continuous conversation
    config = {"configurable": {"thread_id": "user-123"}}

    # First run
    result1 = app.invoke(
        {"messages": [HumanMessage(content="My name is Farhan.")]},
        config=config,
    )

    # Second run - agent remembers the first
    result2 = app.invoke(
        {"messages": [HumanMessage(content="What is my name?")]},
        config=config,
    )
    print(result2["messages"][-1].content)  # "Your name is Farhan."

For production, swap SqliteSaver for PostgresSaver to get a checkpointer that scales across multiple workers and survives server restarts. The API is identical - only the connection string changes.

Parallel Nodes with Send

LangGraph supports running nodes in parallel using the Send API. This is useful for multi-agent patterns where you want several specialized sub-agents to work simultaneously. A common pattern is a supervisor node that fans out to parallel workers, then collects results.

from langgraph.constants import Send

def supervisor_node(state: AgentState):
    tasks = ["research", "write", "review"]
    # Fan out to three parallel nodes
    return [Send("worker", {"task": t, "messages": state["messages"]}) for t in tasks]

LangGraph vs LangChain: When to Use Which

  • -Use LangChain LCEL for simple, linear pipelines: document summarization, single-turn Q&A, prompt formatting chains. Fast to build, easy to debug.
  • -Use LangGraph for anything with loops, branching, multi-turn memory, human-in-the-loop steps, or multiple agents collaborating. The added complexity pays off immediately for these cases.
  • -Use LangGraph with a checkpointer any time your agent needs to resume across sessions, handle long-running tasks, or support a user who comes back later expecting context.

Production Patterns I Use

Human-in-the-loop with interrupt_before

For high-stakes actions like sending emails, making API calls, or writing to databases, you can pause the graph before a node executes and wait for human approval. Pass interrupt_before to compile() with the node names that require approval. The graph pauses, you inspect state, confirm or modify it, and resume.

app = graph.compile(
    checkpointer=checkpointer,
    interrupt_before=["execute_action"]  # pause before this node
)

# Run until the interrupt
result = app.invoke(inputs, config=config)

# Inspect what the agent wants to do
print(result["pending_action"])

# Resume after human approval
app.invoke(None, config=config)

Error Recovery with Try/Except in Nodes

Nodes that call external tools or APIs will fail. Wrap tool calls in try/except and update state with the error so the agent can reason about what went wrong and retry with different parameters rather than crashing the whole graph.

Frequently Asked Questions

Does LangGraph work without LangChain?

LangGraph has a dependency on langchain-core for message types and some base abstractions, but you do not need to use LangChain LCEL chains inside your nodes. You can call any Python code, any API, and any LLM client directly in your node functions. The graph structure is LangGraph; what runs inside the nodes is up to you.

How does LangGraph compare to CrewAI and AutoGen?

CrewAI and AutoGen are higher-level frameworks that abstract the agent orchestration layer. LangGraph is lower-level and gives you more control. CrewAI is faster to get started with for role-based multi-agent setups. LangGraph is better when you need fine-grained control over state, routing logic, and production reliability. Most serious production systems end up on LangGraph because the abstraction of CrewAI and AutoGen becomes a constraint when requirements get complex.

What LLM should I use with LangGraph?

Any LLM with tool-calling support works. For fast iteration, use Groq with Llama 3.3 70B - it is the best combination of speed and capability available at low cost. For the highest accuracy on complex reasoning tasks, use Claude Sonnet or GPT-4o. LangGraph is LLM-agnostic; switching providers is a one-line change to the LLM constructor.

Share this article

Work with me

Need This Built?

I am available for freelance projects, consulting, and remote AI engineering roles. If you need an agentic system, CV pipeline, or scraping infrastructure built properly - let's talk.

Usually responds within 24 hours  ·  Based in Islamabad, open to remote globally