Coderhouse goes global

🌍

Read more

LangGraph Tutorial: How to Build a Stateful AI Agent Step by Step

AI Engineering

Build and ship real applications with LLMs, agents and automation.

8 weeks · Live online · View course →

Data Analytics

Go from zero to job-ready analyst with SQL, Python and BI tools.

10 weeks · Live online · View course →

Dan Patiño

AI Strategy & Innovation at Coderhouse

Artificial Intelligence

LangGraph Tutorial: How to Build a Stateful AI Agent Step by Step

LangGraph is an open-source Python framework from the LangChain team for building AI agents as graphs. Each step of the agent is a node, the connections between steps are edges, and a shared state object travels through the graph so every node can read and update it. That structure gives you something a single prompt or a simple loop cannot: explicit control over what happens next, memory that persists between turns, and the ability to pause for a human before an action runs.

Why it matters: most agent demos work once and break in production because the logic lives inside one long prompt. When you model the workflow as a graph, you can see every branch, test each node on its own, and resume a run where it stopped. This tutorial walks through a small stateful agent step by step, so you understand the moving parts before you scale up.

When LangGraph is the right tool

You do not need a graph for every LLM feature. A single call with structured output is enough for classification or extraction. LangGraph earns its place when the workflow has at least one of these traits:

  • Loops: the model calls a tool, reads the result, and decides whether to call another one.

  • Branches: different inputs need different paths, such as routing a support ticket to billing or technical help.

  • State across turns: the agent must remember what happened earlier in the conversation or task.

  • Human approval: some actions, like sending an email or updating a record, should wait for a person.

If tool use itself is new to you, start with how function calling works, because LangGraph agents are built on top of it.

The core concepts in five lines

  • State: a typed dictionary that holds everything the agent knows, usually a list of messages plus any custom fields.

  • Nodes: Python functions that receive the state and return an update.

  • Edges: fixed transitions from one node to the next.

  • Conditional edges: functions that look at the state and choose the next node.

  • Checkpointer: a persistence layer that saves the state after each step, so a conversation thread can continue later.

The official LangGraph documentation covers each concept in depth; the steps below show how they fit together.

Step 1: Define the state

Start with what the agent needs to remember. For a chat agent, a message list is enough. The add_messages reducer tells LangGraph to append new messages instead of overwriting the list:

from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages

class State(TypedDict):
    messages: Annotated[list, add_messages]
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages

class State(TypedDict):
    messages: Annotated[list, add_messages]
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages

class State(TypedDict):
    messages: Annotated[list, add_messages]
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages

class State(TypedDict):
    messages: Annotated[list, add_messages]

Keep the state small and explicit. If a field is only used inside one node, it probably does not belong in the shared state.

Step 2: Give the agent tools

Tools are ordinary Python functions with a clear name, typed arguments, and a docstring the model can read. Bind them to a chat model so it can request them:

from langchain_core.tools import tool

@tool
def get_order_status(order_id: str) -> str:
    """Return the shipping status for an order ID."""
    return lookup_status(order_id)  # your own function

tools = [get_order_status]
llm_with_tools = llm.bind_tools(tools)
from langchain_core.tools import tool

@tool
def get_order_status(order_id: str) -> str:
    """Return the shipping status for an order ID."""
    return lookup_status(order_id)  # your own function

tools = [get_order_status]
llm_with_tools = llm.bind_tools(tools)
from langchain_core.tools import tool

@tool
def get_order_status(order_id: str) -> str:
    """Return the shipping status for an order ID."""
    return lookup_status(order_id)  # your own function

tools = [get_order_status]
llm_with_tools = llm.bind_tools(tools)
from langchain_core.tools import tool

@tool
def get_order_status(order_id: str) -> str:
    """Return the shipping status for an order ID."""
    return lookup_status(order_id)  # your own function

tools = [get_order_status]
llm_with_tools = llm.bind_tools(tools)

Write docstrings as if a new teammate had to choose the tool from its description alone. Vague descriptions are one of the most common reasons agents pick the wrong tool.

Step 3: Add the nodes

You need two nodes: one that calls the model and one that runs whatever tools the model requested. LangGraph ships a prebuilt ToolNode for the second one:

from langgraph.prebuilt import ToolNode

def agent(state: State):
    return {"messages": [llm_with_tools.invoke(state["messages"])]}

tool_node = ToolNode(tools)
from langgraph.prebuilt import ToolNode

def agent(state: State):
    return {"messages": [llm_with_tools.invoke(state["messages"])]}

tool_node = ToolNode(tools)
from langgraph.prebuilt import ToolNode

def agent(state: State):
    return {"messages": [llm_with_tools.invoke(state["messages"])]}

tool_node = ToolNode(tools)
from langgraph.prebuilt import ToolNode

def agent(state: State):
    return {"messages": [llm_with_tools.invoke(state["messages"])]}

tool_node = ToolNode(tools)

Step 4: Wire the graph with a conditional edge

This is where the loop appears. After the agent node runs, a condition checks whether the model asked for a tool. If it did, control goes to the tool node and then back to the agent. If not, the run ends:

from langgraph.graph import StateGraph, START
from langgraph.prebuilt import tools_condition

builder = StateGraph(State)
builder.add_node("agent", agent)
builder.add_node("tools", tool_node)
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", tools_condition)
builder.add_edge("tools", "agent")
from langgraph.graph import StateGraph, START
from langgraph.prebuilt import tools_condition

builder = StateGraph(State)
builder.add_node("agent", agent)
builder.add_node("tools", tool_node)
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", tools_condition)
builder.add_edge("tools", "agent")
from langgraph.graph import StateGraph, START
from langgraph.prebuilt import tools_condition

builder = StateGraph(State)
builder.add_node("agent", agent)
builder.add_node("tools", tool_node)
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", tools_condition)
builder.add_edge("tools", "agent")
from langgraph.graph import StateGraph, START
from langgraph.prebuilt import tools_condition

builder = StateGraph(State)
builder.add_node("agent", agent)
builder.add_node("tools", tool_node)
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", tools_condition)
builder.add_edge("tools", "agent")

Drawing this on paper before you code helps. If you cannot sketch the graph, the agent's behavior will be hard to predict.

Step 5: Add memory with a checkpointer

Without persistence, every call starts from zero. Compile the graph with a checkpointer and pass a thread_id so LangGraph knows which conversation to load:

from langgraph.checkpoint.memory import MemorySaver

graph = builder.compile(checkpointer=MemorySaver())
config = {"configurable": {"thread_id": "customer-42"}}
graph.invoke({"messages": [("user", "Where is order 1881?")]}, config)
from langgraph.checkpoint.memory import MemorySaver

graph = builder.compile(checkpointer=MemorySaver())
config = {"configurable": {"thread_id": "customer-42"}}
graph.invoke({"messages": [("user", "Where is order 1881?")]}, config)
from langgraph.checkpoint.memory import MemorySaver

graph = builder.compile(checkpointer=MemorySaver())
config = {"configurable": {"thread_id": "customer-42"}}
graph.invoke({"messages": [("user", "Where is order 1881?")]}, config)
from langgraph.checkpoint.memory import MemorySaver

graph = builder.compile(checkpointer=MemorySaver())
config = {"configurable": {"thread_id": "customer-42"}}
graph.invoke({"messages": [("user", "Where is order 1881?")]}, config)

The in-memory saver is fine for learning. In production you would swap it for a database-backed checkpointer so state survives restarts. The difference between short-term thread state and long-term memory is a design decision of its own; the guide to building AI agents from scratch gives useful background.

Step 6: Pause for a human before risky actions

Checkpoints also make human review possible. Inside a node, LangGraph's interrupt function stops the run and saves the state. Your app shows the pending action to a person, and the run resumes with their answer. Use this for anything that writes, sends, pays, or deletes. The reasoning behind this pattern is covered in why human-in-the-loop still matters.

Step 7: Test and trace the agent

An agent that runs is not the same as an agent that works. Before you ship, check three things:

  1. Node tests: call each node function with a fixed state and assert on the update. These are fast and do not need a live model if you mock the call.

  2. Trajectory checks: for a set of sample requests, confirm the agent called the expected tools in a sensible order and stopped.

  3. Tracing: record every step, tool call, token count, and latency so you can see why a run failed. LangSmith is one option; OpenTelemetry-based tools are another.

For grading answer quality at scale, many teams add an automated reviewer; see how LLM-as-a-judge works.

Common mistakes when starting with LangGraph

  • Putting everything in one node. If a node does retrieval, reasoning, and formatting, you lose the visibility the graph was meant to give you.

  • No exit condition. Set a recursion limit and make sure the model has a clear way to finish.

  • Too many tools at once. Start with two or three well-described tools and add more only when tests show the agent picks them correctly.

  • Skipping persistence design. Decide early what a thread is: a user, a ticket, or a task.

LangGraph vs a simple loop vs other frameworks

Best for a single tool call: plain function calling with no framework. Best for stateful, branching workflows with human review: LangGraph, because state, checkpoints, and interrupts are built in. Best for role-based multi-agent prototypes: higher-level frameworks can be quicker to start, at the cost of less control. Many teams prototype fast and move the parts that need reliability into an explicit graph. For patterns with several cooperating agents, read about multi-agent architectures.

FAQ

Do I need to know LangChain to use LangGraph?

It helps but is not required. LangGraph can call any model or function; LangChain components just make common pieces, like tool binding, quicker to set up.

Is LangGraph only for Python?

No. There is also a JavaScript/TypeScript version, though this tutorial uses Python.

How long does it take to learn?

If you are comfortable with Python and have called an LLM API before, you can build the agent above in an afternoon. Designing reliable, tested agents takes longer and is mostly about evaluation and failure handling.

Go from tutorial to production-style agents

If you want guided practice, the AI Engineering Course is an eight-week live program for developers comfortable with Python. Its stack includes LangChain and LangGraph, LangSmith, vector databases, and FastAPI, and it covers tool use, agentic workflows, evaluation, observability, and deployment, ending with a capstone you build, break, and improve. If your goal is analysis rather than building AI systems, the data track is the better fit.

Dan Patiño

I'm Dan Patiño, head of AI Strategy & Innovation at Coderhouse. My day-to-day work involves merging the tactical management of e-commerce (CRO, Email Marketing and SEO) with the development of disruptive solutions. I specialize in building internal AI-powered apps to automate tasks and boost innovation within the team. I firmly believe that technology is strategy's best ally. To dive deeper into my professional journey, I'll be waiting for you on my LinkedIn profile.