Module 5: Introduction to LangGraph

Introduction: From Linear Agents to Workflows as Graphs

Overview

In Modules 1-4 you built complete agents with create_agent. One line of code and you had an autonomous agent that reasons, calls tools, observes results, and repeats until it answers. You added system prompts, custom state, streaming, structured output. You personalized its behavior with middleware without rewriting it. Everything worked.

Until you needed something the ReAct loop can't do: branch the execution based on the type of input. Or pause the workflow to ask for human approval before an expensive action. Or create a process where one node classifies, another researches, another validates, and another generates the final answer — each with its own logic. Or run a conditional loop that retries only if the output quality isn't good enough.

create_agent is powerful, but fundamentally linear: the model reasons, calls tools, observes, repeats. Always the same cycle. You can't change that flow — you can only intercept it with middleware.

LangGraph gives you full control. You define every step, every decision, every loop. There's no magic and no hidden abstractions. It's like going from driving on autopilot to designing the route yourself — more power, more responsibility.


Where are we in the guide?

This is Module 5 of the guide LangChain & LangGraph: From Chains to Agents. It's the first module of Block 2 (LangGraph Fundamentals) — the most important transition in the guide.

Block 1: LangChain Core (Modules 1-4)     ✅ Completed
Block 2: LangGraph Fundamentals (Modules 5-7)  ← YOU ARE HERE (Module 5)
Block 3: Advanced LangGraph (Modules 8-10)
Block 4: Production (Modules 11-12)
Your progress:

Block 1 — LangChain Core                    ✅ Completed
    │
    │  Module 1: Models and Providers        ✅
    │  Module 2: Tools and Tool Calling      ✅
    │  Module 3: Agents (create_agent)       ✅
    │  Module 4: Middleware and Customization ✅
    │
    ▼
Block 2 — LangGraph Fundamentals
    │
    │  Module 5: Introduction to LangGraph   ← YOU ARE HERE
    │  Module 6: Functional API              🔒 Next
    │  Module 7: Advanced Flows              🔒
    │
    ▼
Blocks 3-4 — Advanced + Production          🔒

In Block 1 you mastered the high level: models, tools, agents with create_agent, and middleware to customize them. Now you're going down to the level where you control every aspect of the execution flow.


The bridge: from automatic agents to workflows you design

What you already know

In Block 1 you got to build this with create_agent and middleware:

from dotenv import load_dotenv
load_dotenv()

from langchain.agents import create_agent
from langchain_core.tools import tool

@tool
def search(query: str) -> str:
    """Search the web for information."""
    return f"Results for '{query}': Python is a language created by Guido van Rossum."

@tool
def calculator(expression: str) -> str:
    """Evaluate a math expression."""
    return str(eval(expression))

agent = create_agent(
    "openai:gpt-4.1-mini",
    tools=[search, calculator],
    system_prompt="You are a research assistant."
)

result = agent.invoke({"messages": [("user", "Who created Python and what is 2**20?")]})
print(result["messages"][-1].content)
# Expected output: Python was created by Guido van Rossum. And 2^20 = 1,048,576.

The agent works. It reasons, calls tools, combines results. But the flow is always the same:

┌──────────┐     ┌──────────┐     ┌──────────┐
│  Reason  │────▶│   Act    │────▶│ Observe  │───┐
│ (Model)  │     │  (Tool)  │     │ (Result) │   │
└──────────┘     └──────────┘     └──────────┘   │
     ▲                                            │
     └────────────────────────────────────────────┘

You can't change this flow. You can't have one node classify and another one execute. You can't add a pause for human approval between the reasoning and the action. You can't make the agent take a different route depending on the type of question.

What you'll learn here

With LangGraph, you design the flow:

from langgraph.graph import StateGraph, START, END

graph_builder = StateGraph(MyState)
graph_builder.add_node("classify", classify_input)
graph_builder.add_node("qa_handler", handle_qa)
graph_builder.add_node("creative_handler", handle_creative)
graph_builder.add_node("code_handler", handle_code)
graph_builder.add_conditional_edges("classify", route_by_intent)

Every node is a function you define. Every connection is a decision you control. The flow branches, loops, pauses — whatever you need.


The limits of create_agent

create_agent solves 80% of cases. But it has structural limits:

Needcreate_agentLangGraph (StateGraph)
Agent that reasons and calls toolsOne line of codeTakes more code
Branching by input typeNot possibleConditional edges
Pausing for human approvalLimited (interrupt_before)Granular interrupts
Multiple specialized nodesA single ReAct loopIndependent nodes
Conditional loops (retry if quality < threshold)Not possibleEdges that go back
Multi-phase process (plan → execute → validate)Not possibleSequential nodes with their own logic
Complex custom stateBasic state_schemaTypedDict + full reducers
Flow visualizationFixed graph (model ↔ tools)draw_mermaid_png() of your design

create_agent isn't inferior — it's different. If you need an agent that reasons and calls tools, create_agent is the right and faster option. If you need a workflow with a custom flow, LangGraph is what you need.

The question isn't "which one is better?" but "what kind of flow do I need?"


The core concept: graphs as flowcharts

A graph in LangGraph has three components:

  1. Nodes — Functions that transform state. Each node receives the current state, does something (calls the model, runs logic, validates data), and returns the updates.

  2. Edges — Connections between nodes. They define the execution order. They can be fixed ("after A, always B") or conditional ("after A, go to B or C depending on the result").

  3. State — A typed dictionary that travels through the whole graph. Every node reads it and updates it. It's the workflow's shared memory.

Think of a flowchart you'd draw on a whiteboard:

            ┌─────────┐
            │  START  │
            └────┬────┘
                 │
                 ▼
          ┌──────────────┐
          │   Classify   │
          │    intent    │
          └──────┬───────┘
                 │
         ┌───────┼───────┐
         │       │       │
         ▼       ▼       ▼
     ┌───────┐ ┌─────┐ ┌──────┐
     │  Q&A  │ │Code │ │ Chat │
     └───┬───┘ └──┬──┘ └──┬───┘
         │        │       │
         └────────┼───────┘
                  │
                  ▼
            ┌─────────┐
            │   END   │
            └─────────┘

That's a LangGraph graph. Each box is a node (a function). Each arrow is an edge (a connection). The flow goes from START to END, passing through whichever nodes apply.

There's no advanced graph theory here. You don't need to know what a DAG is or study algorithms. If you can draw a flowchart, you can build a graph in LangGraph.


Side-by-side: the same problem, two approaches

Imagine you need an assistant that classifies the user's question and answers in a specialized way depending on the type.

With create_agent: a single path

from dotenv import load_dotenv
load_dotenv()

from langchain.agents import create_agent
from langchain_core.tools import tool

@tool
def classify_and_respond(question: str) -> str:
    """Classify the question and generate a specialized answer."""
    if "code" in question.lower() or "python" in question.lower():
        return f"[CODE] Technical answer about: {question}"
    elif "history" in question.lower() or "when" in question.lower():
        return f"[QA] Informative answer about: {question}"
    else:
        return f"[CHAT] Conversational answer about: {question}"

agent = create_agent(
    "openai:gpt-4.1-mini",
    tools=[classify_and_respond],
    system_prompt="Use the tool to classify and answer every question."
)

result = agent.invoke({"messages": [("user", "How do I write a for loop in Python?")]})
print(result["messages"][-1].content)
# Classification and answer both live inside a single tool.
# There's no real flow — just an agent calling one tool.

The classification is buried inside a tool. There's no real separation of concerns. You can't visualize the flow. You can't add a validation node between the classification and the answer.

With StateGraph: a flow you designed

from typing import TypedDict, Annotated, Literal
import operator
from langgraph.graph import StateGraph, START, END

class State(TypedDict):
    messages: Annotated[list[str], operator.add]
    intent: str

def classify(state: State) -> dict:
    last_message = state["messages"][-1].lower()
    if "code" in last_message or "python" in last_message:
        return {"intent": "code"}
    elif "history" in last_message or "when" in last_message:
        return {"intent": "qa"}
    return {"intent": "chat"}

def handle_code(state: State) -> dict:
    return {"messages": [f"[CODE] Technical answer about: {state['messages'][-1]}"]}

def handle_qa(state: State) -> dict:
    return {"messages": [f"[QA] Informative answer about: {state['messages'][-1]}"]}

def handle_chat(state: State) -> dict:
    return {"messages": [f"[CHAT] Conversational answer about: {state['messages'][-1]}"]}

def route_by_intent(state: State) -> Literal["code", "qa", "chat"]:
    return state["intent"]

graph_builder = StateGraph(State)
graph_builder.add_node("classify", classify)
graph_builder.add_node("code", handle_code)
graph_builder.add_node("qa", handle_qa)
graph_builder.add_node("chat", handle_chat)

graph_builder.add_edge(START, "classify")
graph_builder.add_conditional_edges("classify", route_by_intent)
graph_builder.add_edge("code", END)
graph_builder.add_edge("qa", END)
graph_builder.add_edge("chat", END)

graph = graph_builder.compile()

result = graph.invoke({"messages": ["How do I write a for loop in Python?"], "intent": ""})
print(result)
# {"messages": ["How do I write a for loop in Python?", "[CODE] Technical answer about: ..."], "intent": "code"}

Every responsibility has its own node. The flow is visible. You can add intermediate nodes (validation, logging, enrichment) without rewriting anything. You can visualize it with draw_mermaid_png().

More code, but more control. That's the fundamental difference.


What you'll master in this module

By the end of this module's 8 capsules, you'll be able to:

  • ✅ Create graphs with StateGraph and typed state (TypedDict + Annotated with reducers)
  • ✅ Define nodes as functions that transform state
  • ✅ Connect nodes with fixed edges (add_edge) and conditional edges (add_conditional_edges)
  • ✅ Compile graphs with graph.compile() and run them with invoke and stream
  • ✅ Visualize graphs with draw_mermaid_png() for debugging and documentation
  • ✅ Decide with good judgment when to use create_agent (80% of cases) vs StateGraph (full control)

Module map

CapsuleTopicWhat you'll learn
02StateGraph: your first graphStateGraph(State), TypedDict with Annotated and operator.add, START and END, compiling and running
03Nodes: functions that transform stateDefining nodes as functions, add_node(), nodes that call the model, nodes that run tools
04Edges and conditional edgesadd_edge() for fixed connections, add_conditional_edges() for dynamic routing, routing functions
05Typed state with TypedDict and AnnotatedDesigning state: what to include, reducers with Annotated, the prebuilt MessagesState, custom state fields
06Compilation and executiongraph.compile(), graph.invoke(), graph.stream(), visualization with draw_mermaid_png()
07create_agent vs StateGraphDecision table, examples of when to use each one, real trade-offs
08Project: Chatbot with state and conditional routingA chatbot that classifies user intent and routes to specialized nodes with conditional edges

Learning flow: You start by creating your first graph and understanding how state works (02). Then you go deeper into nodes (03) and how to connect them with fixed and conditional edges (04). After that you master state design with reducers and custom fields (05). With that foundation, you learn to compile, run, stream, and visualize graphs (06). Finally, you develop the judgment to choose between create_agent and StateGraph (07) and build the capstone project (08).


Connection with the project

This module's mini-project: Chatbot with State and Conditional Routing

In Capsule 08 you'll build a chatbot with StateGraph that:

  1. Uses 4+ nodes — intent classifier, Q&A handler, creative writing handler, code help handler, response node
  2. Implements conditional edges based on the classification of the user's intent
  3. Keeps typed state with TypedDict — messages, detected intent, process metadata
  4. Is visualized with draw_mermaid_png() — you see exactly how the decisions flow

This is the last standalone mini-project in the guide. From Module 6 on, everything you build will be part of the AI Research Assistant — an evolving project that grows with you module by module.

Connection with the full guide

What you learn about StateGraph here is the foundation for everything that follows:

  • Module 6 (Functional API): Introduces an alternative way to build workflows — using @entrypoint and @task instead of explicit graphs. Both APIs coexist; you'll pick based on the case.
  • Module 7 (Advanced Flows): Adds cycles, retries, parallel branching, subgraphs, and error handling — patterns that need the StateGraph foundation.
  • Modules 8-10: Persistent memory (checkpointing), human-in-the-loop (interrupts), and multi-agent (supervisor, handoffs). All built on top of graphs.
  • Modules 11-12: Deep Agents and production with LangSmith. StateGraph is the engine underneath everything.

Limits: what this module does NOT cover

  • Functional API (@entrypoint, @task) — Covered in Module 6. Here you work exclusively with the Graph API (StateGraph).
  • Advanced flows (cycles, retries, subgraphs) — Covered in Module 7. Here you master the fundamentals: nodes, edges, conditional edges.
  • Persistent memory (checkpointing) — Covered in Module 8. Here the state lives in memory during the execution.
  • Human-in-the-loop (interrupts, approvals) — Covered in Module 9. We'll mention that LangGraph enables it, but we won't go deep.
  • Multi-agent systems — Covered in Module 10. Here you work with a single graph, not with multiple coordinated agents.

Technical setup

Prerequisites

Before continuing, make sure you have:

  • Block 1 completed — you know how to create agents with create_agent, tools, middleware, system prompts, streaming, and structured output
  • Python 3.11+ installed
  • ✅ At least one API key from a provider (OpenAI or Anthropic recommended)

Installation

If you completed Block 1, you already have langchain, langgraph, and langchain-openai installed. Verify:

pip install langgraph langchain-openai python-dotenv

If you already have everything, confirm that langgraph is v1.0+:

import langgraph
print(langgraph.__version__)
# You need >= 1.0.0

Check that everything works

from typing import TypedDict, Annotated
import operator
from langgraph.graph import StateGraph, START, END

class State(TypedDict):
    messages: Annotated[list[str], operator.add]

def hello(state: State) -> dict:
    return {"messages": ["LangGraph works!"]}

graph_builder = StateGraph(State)
graph_builder.add_node("hello", hello)
graph_builder.add_edge(START, "hello")
graph_builder.add_edge("hello", END)

graph = graph_builder.compile()
result = graph.invoke({"messages": []})
print(result)
# Expected output: {"messages": ["LangGraph works!"]}

If you see the message, your setup is ready for LangGraph.

If something fails:

ErrorCauseFix
ModuleNotFoundError: No module named 'langgraph'langgraph not installedpip install langgraph
ImportError: cannot import name 'StateGraph'langgraph version too oldpip install --upgrade langgraph (you need v1.0+)
TypeError: 'type' object is not subscriptablePython < 3.9 with list[str]Upgrade to Python 3.11+ or use from __future__ import annotations

What success looks like

By the end of this module, you'll know you succeeded if:

  • ✅ You can create a StateGraph with typed state, nodes, and edges from scratch
  • ✅ You understand the difference between operator.add (accumulate) and no reducer (replace)
  • ✅ You can use conditional edges for dynamic routing based on state
  • ✅ Your graph renders correctly with draw_mermaid_png()
  • ✅ You know when to use create_agent and when to use StateGraph
  • ✅ Your chatbot project classifies intents and routes to specialized nodes

Preview: from the explicit graph to the Functional API

In this module you'll build workflows by defining explicit graphs: nodes, edges, conditional edges, compile, run. It's the most visual and explicit approach — you see exactly how the execution flows.

But LangGraph offers another way to build the same thing: the Functional API. In Module 6, you'll learn to write workflows as regular Python functions with @entrypoint and @task:

# Module 5: Graph API (what you'll learn here)
from langgraph.graph import StateGraph, START, END

graph_builder = StateGraph(State)
graph_builder.add_node("classify", classify_input)
graph_builder.add_node("respond", generate_response)
graph_builder.add_edge(START, "classify")
graph_builder.add_edge("classify", "respond")
graph_builder.add_edge("respond", END)
graph = graph_builder.compile()

# Module 6: Functional API (what comes next)
from langgraph.func import entrypoint, task

@task
def classify_input(message: str) -> str:
    ...

@task
def generate_response(intent: str, message: str) -> str:
    ...

@entrypoint()
def assistant(messages: list) -> str:
    intent = classify_input(messages[-1]).result()
    return generate_response(intent, messages[-1]).result()

Same logic, two ways to express it. The Graph API is ideal when the flow is visual and has branching. The Functional API is ideal when the flow is more sequential and you want to use Python control flow (loops, conditionals, try/except).

Module 6 also marks the start of the evolving project — from there on, each module builds on the previous one to create the AI Research Assistant.


Summary

  • In Block 1 you mastered LangChain Core: models, tools, agents with create_agent, and middleware to customize them
  • create_agent is powerful but fundamentally linear — always the same ReAct cycle. You can't branch, pause, or design custom flows
  • LangGraph gives you full control of the flow: you define every node, every connection, every decision
  • A graph has three components: nodes (functions that transform state), edges (connections), and state (a shared typed dictionary)
  • Think of a graph as a flowchart — you don't need graph theory or computer science
  • create_agent is still the right tool for 80% of cases (agents that reason and call tools). StateGraph is for when you need full control of the workflow
  • The tone of this module is transitional and empowering: "now you decide how the execution flows — that's more power and more responsibility"
  • This is the last module with a standalone mini-project. From Module 6 on, the evolving project (AI Research Assistant) takes center stage
  • Visualization with draw_mermaid_png() is a working tool, not a nice-to-have. Draw first, code second

Additional resources

  1. LangGraph Overview — LangGraph's main page with concepts and quickstart
  2. StateGraph Reference — Full reference for the StateGraph class
  3. LangGraph Quickstart — Official step-by-step tutorial
  4. LangGraph vs LangChain Agents — When to use each level of abstraction
  5. How to visualize your graph — Guide to draw_mermaid_png() and other visualization options
  6. TypedDict — Python docs — Reference for the TypedDict you'll use to define graph state

Module 5 — LangChain & LangGraph: From Chains to Agents

Next capsule: StateGraph: Your First Graph — you'll learn to create a graph from scratch, define state with TypedDict and Annotated, and understand why reducers are critical.