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:
| Need | create_agent | LangGraph (StateGraph) |
|---|---|---|
| Agent that reasons and calls tools | One line of code | Takes more code |
| Branching by input type | Not possible | Conditional edges |
| Pausing for human approval | Limited (interrupt_before) | Granular interrupts |
| Multiple specialized nodes | A single ReAct loop | Independent nodes |
| Conditional loops (retry if quality < threshold) | Not possible | Edges that go back |
| Multi-phase process (plan → execute → validate) | Not possible | Sequential nodes with their own logic |
| Complex custom state | Basic state_schema | TypedDict + full reducers |
| Flow visualization | Fixed 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:
-
Nodes — Functions that transform state. Each node receives the current state, does something (calls the model, runs logic, validates data), and returns the updates.
-
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").
-
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
StateGraphand 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 withinvokeandstream - ✅ Visualize graphs with
draw_mermaid_png()for debugging and documentation - ✅ Decide with good judgment when to use
create_agent(80% of cases) vsStateGraph(full control)
Module map
| Capsule | Topic | What you'll learn |
|---|---|---|
| 02 | StateGraph: your first graph | StateGraph(State), TypedDict with Annotated and operator.add, START and END, compiling and running |
| 03 | Nodes: functions that transform state | Defining nodes as functions, add_node(), nodes that call the model, nodes that run tools |
| 04 | Edges and conditional edges | add_edge() for fixed connections, add_conditional_edges() for dynamic routing, routing functions |
| 05 | Typed state with TypedDict and Annotated | Designing state: what to include, reducers with Annotated, the prebuilt MessagesState, custom state fields |
| 06 | Compilation and execution | graph.compile(), graph.invoke(), graph.stream(), visualization with draw_mermaid_png() |
| 07 | create_agent vs StateGraph | Decision table, examples of when to use each one, real trade-offs |
| 08 | Project: Chatbot with state and conditional routing | A 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:
- Uses 4+ nodes — intent classifier, Q&A handler, creative writing handler, code help handler, response node
- Implements conditional edges based on the classification of the user's intent
- Keeps typed state with TypedDict — messages, detected intent, process metadata
- 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
@entrypointand@taskinstead 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:
| Error | Cause | Fix |
|---|---|---|
ModuleNotFoundError: No module named 'langgraph' | langgraph not installed | pip install langgraph |
ImportError: cannot import name 'StateGraph' | langgraph version too old | pip install --upgrade langgraph (you need v1.0+) |
TypeError: 'type' object is not subscriptable | Python < 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
StateGraphwith 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_agentand when to useStateGraph - ✅ 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_agentis 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_agentis still the right tool for 80% of cases (agents that reason and call tools).StateGraphis 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
- LangGraph Overview — LangGraph's main page with concepts and quickstart
- StateGraph Reference — Full reference for the StateGraph class
- LangGraph Quickstart — Official step-by-step tutorial
- LangGraph vs LangChain Agents — When to use each level of abstraction
- How to visualize your graph — Guide to
draw_mermaid_png()and other visualization options - 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.