LLM Agents Academy
Module 8: Agent Frameworks & SDKs

LangGraph

Graph-based agent workflows: nodes, edges, state, and conditional routing

~14 min readIncludes code

Overview

In the previous two lessons, we explored the OpenAI Agents SDK and the Anthropic Claude Agent SDK -- frameworks that provide high-level abstractions for building agents with their respective models. Both use a fundamentally linear execution model: the agent enters a loop, decides on an action, executes it, observes the result, and repeats until done. This works well for straightforward tasks, but what happens when your workflow needs to branch, loop back to a previous step, pause for human approval, or compose multiple sub-workflows?

LangGraph takes a radically different approach. Instead of an implicit loop, you define your agent's workflow as an explicit graph -- a directed structure where nodes are functions, edges are transitions, and a shared state object flows through the entire execution. This makes complex workflows visible, debuggable, and composable in ways that linear loops cannot match.

This lesson introduces LangGraph's core abstractions, walks through building a ReAct agent as a graph, and shows how features like conditional routing, checkpointing, and human-in-the-loop integrate naturally into the graph model. By the end, you will understand when a graph-based approach pays for its added complexity -- and when a simpler framework is the better choice.

The Graph Mental Model

Before diving into code, let's build intuition for how LangGraph thinks about agent execution. In a traditional agent loop, the control flow is implicit -- it lives inside a while True loop in your code, and the LLM decides when to break out. In LangGraph, the control flow is explicit -- you draw it as a graph with named steps and transitions between them.

graph TD
  START(["__start__"]) --> agent["agent<br/>(call LLM)"]
  agent --> should_continue{"should_continue?"}
  should_continue -->|"has tool calls"| tools["tools<br/>(execute tools)"]
  should_continue -->|"no tool calls"| END(["__end__"])
  tools --> agent
  style START fill:#e6fffa,stroke:#38b2ac
  style END fill:#e6fffa,stroke:#38b2ac
  style agent fill:#ebf8ff,stroke:#3182ce
  style tools fill:#ebf8ff,stroke:#3182ce
  style should_continue fill:#fefcbf,stroke:#d69e2e

This is the classic ReAct pattern expressed as a graph. The agent node calls the LLM. A conditional edge checks whether the LLM wants to use tools. If yes, execution flows to the tools node, which runs the requested tools and loops back to agent. If no, execution flows to __end__. Every step is a named, inspectable node. Every decision point is an explicit conditional edge. The state -- including the full message history -- flows through automatically.

Compare this to the while-loop version from earlier modules: the logic is the same, but now you can see it, test individual nodes, and modify the flow without rewriting your loop control structure.

Core Abstractions

LangGraph is built on four core abstractions. Understanding these is essential before writing any code.

StateGraph is the container for your entire workflow. You create a StateGraph parameterized by a state type -- a TypedDict or Pydantic model that defines the data flowing through your graph. Every node reads from and writes to this shared state.

Nodes are Python functions that perform work. Each node receives the current state as input and returns a partial state update as output. A node might call an LLM, execute tools, transform data, or make a decision. Nodes are pure functions of state -- they do not need to know about the graph structure around them.

Edges connect nodes together. A normal edge is a fixed transition: "after node A, always go to node B." A conditional edge evaluates a function against the current state and routes to different nodes based on the result. This is where branching logic lives.

State is the shared data object that accumulates as execution progresses. The most common pattern uses a messages field with a special reducer that appends new messages rather than overwriting. This means each node can add messages to the conversation history without worrying about what other nodes have done.

stateDiagram-v2
  [*] --> Idle: Graph created
  Idle --> Running: .invoke() or .stream()
  Running --> NodeExec: Execute next node
  NodeExec --> StateUpdate: Node returns partial state
  StateUpdate --> EdgeEval: Apply reducers, evaluate edges
  EdgeEval --> NodeExec: Conditional/normal edge selects next node
  EdgeEval --> Completed: Edge routes to __end__
  Completed --> [*]
  Running --> Interrupted: Checkpoint + human-in-the-loop
  Interrupted --> Running: Resume with updated state

This state diagram shows the lifecycle of a LangGraph execution. Notice the Interrupted state -- this is where checkpointing and human-in-the-loop enter the picture. We will return to those after building our first graph.

Building a ReAct Agent with LangGraph

Let's build a complete ReAct agent step by step. We will create an agent that can search the web and perform calculations -- a simple but representative example that shows all of LangGraph's core features.

First, we define the state and the tools:

state_and_tools.py

The state definition is the foundation. The Annotated[list[AnyMessage], add_messages] pattern is a LangGraph convention: the add_messages reducer tells the graph how to merge state updates. When a node returns {"messages": [new_message]}, the reducer appends new_message to the existing list rather than replacing it. This is how conversation history accumulates across nodes.

Next, we define the nodes -- the functions that do the actual work:

nodes.py

Notice that call_model is a plain Python function. It takes state, does work, and returns a partial state update. It does not know or care about the graph structure. The ToolNode is a prebuilt convenience -- it inspects the last AI message for tool calls, executes them, and returns tool result messages.

Now we wire everything together with the graph definition and conditional routing:

graph_definition.py

The graph.compile() call transforms your graph definition into an executable runnable. This is where LangGraph validates the structure -- it checks that all edges lead to defined nodes, that conditional edges cover all possible return values, and that the graph is well-formed.

The add_conditional_edges call is the key differentiator from linear frameworks. Instead of hardcoding if/else logic inside a loop, you declare routing as a named function that the framework calls at the right time. This makes the decision point explicit, testable, and visible in graph visualizations.

State Management and Reducers

State management is where LangGraph's design pays the biggest dividends. In a while-loop agent, state is typically a local variable -- a list of messages that you manually append to. If you want to track additional information (say, the number of tool calls made, or a list of sources found), you bolt it onto the loop with more local variables.

In LangGraph, you declare your state schema upfront, and reducers control how updates merge:

rich_state.py

Each reducer defines a merge strategy: add_messages appends messages intelligently (handling deduplication by message ID), add sums integers, and list concatenation collects items from multiple nodes. Fields without a reducer use simple overwrite semantics. This eliminates an entire class of bugs where two parts of your code fight over the same mutable state.

Checkpointing and Persistence

If you worked through Module 6, Lesson 6 on idempotency and checkpointing, you already know why persisting agent state matters: long-running agents fail, networks drop, and users close their browsers. Without checkpointing, a failure after step 47 of 50 means starting over from scratch.

LangGraph builds checkpointing directly into the graph runtime. By passing a checkpointer when you compile the graph, every state transition is automatically persisted:

checkpointing.py

This is the checkpoint-and-resume pattern from Module 6 implemented as a first-class framework feature. The graph saves its full state -- messages, tool call counts, custom fields, everything in your TypedDict -- after every node execution. If the process crashes, you resume from the last successful node, not from the beginning. The thread_id in the config acts as the session key, letting you maintain separate conversations for different users or tasks.

Human-in-the-Loop

Checkpointing unlocks one of LangGraph's most powerful features: human-in-the-loop workflows. You can configure the graph to pause before executing certain nodes, present the pending action to a human for approval, and resume only after they approve (or modify) the action.

human_in_the_loop.py

The interrupt_before=["tools"] parameter tells the graph to checkpoint and pause whenever execution would enter the tools node. This is not a hack -- it is a natural consequence of the graph model. Because every transition is explicit, the framework knows exactly where to pause. Because state is checkpointable, it knows how to resume. In a linear loop, adding human-in-the-loop requires you to restructure your entire control flow. In LangGraph, it is a single parameter.

Subgraphs: Composing Complex Workflows

Real-world agent systems rarely have a single flat workflow. You might have a research agent that delegates to a specialized code-analysis subagent, or a customer service agent that hands off to a billing specialist. LangGraph supports this through subgraphs -- complete graphs that can be used as nodes inside a parent graph.

subgraphs.py

The subgraph runs as a self-contained unit inside the parent. Its internal state transitions are invisible to the parent graph -- the parent only sees the subgraph's input and output. This is the same encapsulation principle you use when composing functions: hide internal complexity, expose a clean interface. Subgraphs can have their own checkpointers, their own interrupt points, and their own conditional routing. This is how you scale from a single-agent prototype to a multi-agent production system.

Graph-Based vs. Linear: When to Choose Which

LangGraph adds structure, but structure has a cost. Let's be honest about when the graph-based approach justifies its overhead and when a simpler framework is the better choice.

Choose a graph-based approach when:

  • Your workflow has branching logic -- different paths depending on the agent's observations
  • You need human-in-the-loop approval at specific steps
  • You want checkpointing and resumability for long-running workflows
  • Your system involves multiple specialized agents that hand off to each other
  • You need to visualize and debug the control flow for complex workflows
  • The workflow has cycles with complex exit conditions that are hard to express in a while loop

Choose a linear loop when:

  • The agent follows a simple observe-think-act cycle with no branching
  • The workflow completes in a few steps and does not need persistence
  • You are prototyping and want the fastest path to a working agent
  • The team is more comfortable with imperative control flow

Key insight: LangGraph does not replace the ReAct pattern -- it gives you a more expressive way to implement it. The ReAct loop is still there in our graph (agent -> tools -> agent). The graph just makes it explicit, extensible, and observable.

Streaming and Observability

One practical advantage of the graph model is built-in streaming. Because the framework controls the execution flow, it can emit events at every transition -- giving you real-time visibility into what the agent is doing:

streaming.py

This event-driven streaming means you can build responsive UIs that show the user exactly where the agent is in its workflow, which tools it is calling, and what results it is getting -- all without polling or custom instrumentation.

Summary

LangGraph reimagines agent execution as an explicit, inspectable graph rather than an implicit loop. The core abstractions are:

  • StateGraph -- the container that holds your workflow definition
  • Nodes -- Python functions that read state, do work, and return partial state updates
  • Edges -- fixed transitions and conditional routing functions that control execution flow
  • State with reducers -- a typed, shared data object where reducers define how updates merge across nodes

These abstractions unlock capabilities that are difficult to achieve in linear frameworks: conditional routing for branching workflows, checkpointing for persistence and resumability (the same pattern you learned in Module 6), human-in-the-loop via interrupt points, and subgraphs for composing complex multi-agent systems.

The tradeoff is real: LangGraph requires more upfront structure than a simple while loop. But for workflows that need branching, persistence, human oversight, or multi-agent composition, the graph model pays for itself in debuggability, testability, and maintainability. In the next lesson, we will explore CrewAI and AutoGen -- frameworks that take yet another approach, organizing agents around roles and conversations rather than graphs.

Check Your Understanding

Question 1 of 4

What is the core abstraction that distinguishes LangGraph from linear agent frameworks?