Home

Chapter 2 · Lesson 0050

LangGraph — the agent runtime

30–40 min read · Hands-on coding

Motivation

Every time you called create_agent in the last lesson, something was running underneath it that you never saw: LangGraph. It’s the engine LangChain 1.0 is built on — and the moment your workflow needs something create_agent’s defaults don’t cover (real branching across multiple paths, a process that must survive a server restart, a human approval step that pauses for hours), you drop down and use that engine directly. This lesson opens the hood.

Prerequisites

  • Lesson 17: LangChain — you’ll see what’s actually running underneath create_agent
  • Lessons 13–14: evaluator-optimizer, routers, and agentic loops — LangGraph models these natively
  • Install: uv add langgraph langchain-openai

LangGraph: the runtime underneath

Recall the three-tier split from the last lesson: framework (LangChain, fast to start), runtime (LangGraph, low-level control), harness (Deep Agents SDK, opinionated and autonomous). LangGraph is a separate package from LangChain — same company, different layer — that models a workflow as a graph: nodes connected by edges, where each node is a Python function that reads and updates a shared state object.

You already know this pattern. The agentic loops, evaluator-optimizer, and routers you built from scratch in Lessons 13–14 are all graphs in everything but name: a step that does work, a decision about what happens next, possibly a loop back. LangGraph gives that pattern explicit, inspectable structure instead of nested if-statements, and adds production infrastructure on top:

State, Nodes, and Edges

Every LangGraph workflow has exactly three parts:

  1. State — a TypedDict that flows through every node
  2. Nodes — Python functions that read from state and return state updates
  3. Edges — connections between nodes, fixed (add_edge) or conditional (add_conditional_edges)

Here’s the evaluator-optimizer loop from Lesson 14, rebuilt as a graph:

from langgraph.graph import StateGraph, START, END
from langchain_openai import ChatOpenAI
from typing import TypedDict

llm = ChatOpenAI(model="gpt-4o-mini")

# ── State ─────────────────────────────────────────────────────
class State(TypedDict):
    document: str
    summary: str
    quality_score: int
    iterations: int

# ── Nodes ─────────────────────────────────────────────────────
def summarise(state: State) -> dict:
    feedback = f" Improve on: {state.get('feedback', '')}" if state.get('feedback') else ""
    response = llm.invoke(f"Summarise this document in 3 sentences.{feedback}\n\n{state['document']}")
    return {"summary": response.content, "iterations": state["iterations"] + 1}

def evaluate(state: State) -> dict:
    response = llm.invoke(
        f"Rate this summary 1-10 for accuracy and clarity. Reply with just the number.\n\nSummary: {state['summary']}"
    )
    try:
        score = int(response.content.strip())
    except ValueError:
        score = 5
    return {"quality_score": score}

def should_retry(state: State) -> str:
    if state["quality_score"] < 7 and state["iterations"] < 3:
        return "retry"
    return "done"

# ── Graph ─────────────────────────────────────────────────────
graph = StateGraph(State)
graph.add_node("summarise", summarise)
graph.add_node("evaluate",  evaluate)

graph.add_edge(START,       "summarise")
graph.add_edge("summarise", "evaluate")
graph.add_conditional_edges("evaluate", should_retry, {
    "retry": "summarise",   # loop back
    "done":  END,
})

app = graph.compile()

result = app.invoke({
    "document": "Your document text here...",
    "summary": "",
    "quality_score": 0,
    "iterations": 0,
})
print(f"Final summary (score {result['quality_score']}/10):")
print(result["summary"])

add_conditional_edges is the part that matters most: it takes a routing function (should_retry) that reads state and returns a string key, then maps that key to the next node. This single mechanism is how LangGraph expresses every branching pattern from Lessons 13–14 — evaluator-optimizer retries, router dispatch to a specialist node, an agentic loop deciding to continue or stop — as an explicit, visualisable edge instead of logic buried inside a function.

Durable execution and persistence

A plain Python loop loses all progress the moment the process crashes or restarts. LangGraph solves this with a checkpointer — the same mechanism LangChain’s short-term memory (Lesson 17) is built on, because create_agent is itself a compiled LangGraph graph:

from langgraph.checkpoint.memory import InMemorySaver

checkpointer = InMemorySaver()
app = graph.compile(checkpointer=checkpointer)

config = {"configurable": {"thread_id": "doc-batch-42"}}
result = app.invoke({"document": "...", "summary": "", "quality_score": 0, "iterations": 0}, config)

After every node runs, the graph’s full state is saved against thread_id. If the process crashes mid-run, invoking the graph again with the same thread_id resumes from the last completed node instead of starting over. InMemorySaver is for development; production deployments use a database-backed checkpointer (Postgres, for example) so state survives a server restart, not just a single process.

Human-in-the-loop

Some workflows need a person to approve a step before the graph continues — sending an email, executing a refund, deploying code. LangGraph supports this with interrupt, which pauses graph execution at a specific point and waits — for seconds or for days — until you resume it with a human decision:

from langgraph.types import interrupt, Command

def request_approval(state: State) -> dict:
    decision = interrupt({"summary": state["summary"], "question": "Approve this summary?"})
    return {"approved": decision}

Calling app.invoke(...) on a graph containing this node stops execution at interrupt and returns control to your application. Later — after a human reviews state["summary"] through whatever interface you build — you resume the exact same thread with app.invoke(Command(resume=True), config). The graph picks up exactly where it paused, with full state intact. This is the kind of long-lived pause create_agent’s middleware (Lesson 17) cannot do on its own — it requires the runtime’s checkpointing underneath.

When to drop down to LangGraph directly

Situation Recommendation
Single agent, tools, no real branching LangChain create_agent (Lesson 17) — you’re already running on LangGraph anyway
Workflow with explicit branching across multiple paths LangGraph directly — add_conditional_edges makes the logic visible
Must survive a crash or restart mid-workflow LangGraph — checkpointing gives you durable execution
Approval step that pauses for minutes, hours, or days LangGraph — interrupt/resume
Need to see and control every node transition LangGraph — create_agent’s loop is fixed; a custom graph is fully yours
Career note: Companies running agents in production for over a year — Uber, LinkedIn, Klarna among them — run LangGraph underneath, whether or not their application code calls LangChain's create_agent or builds the graph directly. Understanding what's happening at the runtime layer is what separates "I used LangChain" from "I can debug why an agent hung in production."

Assignment

  1. Read: LangGraph: Multi-Agent Workflows by LangChain blog — about 10 minutes. The original LangGraph announcement from the team that built it, explaining the graph-based model and why it was designed as a separate, lower-level runtime rather than another layer inside LangChain. Direct context for the Framework/Runtime distinction from the previous lesson.
  2. Do: Reimplement the document intelligence pipeline from P004 using LangGraph.
    1. Define a PipelineState TypedDict with the same fields as DocumentIntelligence
    2. Create nodes: ingest, extract, classify, summarise, evaluate
    3. Add a conditional edge from evaluate: retry summarise if score < 7, else end
    4. Run on three documents and compare the output to your P004 version

    Expected output: Same DocumentIntelligence JSON as P004, produced by a LangGraph workflow. Implementing the same pipeline in both styles builds direct intuition for where the runtime adds value (explicit state, branching, persistence) and where it adds friction (more setup than a plain for-loop).

Knowledge Check

Answer each question from memory, then click to reveal.

Additional Resources