Skip to content

Introduction to LangGraph

1. Why this matters

LangChain's LCEL (prompt | model | parser) is great for DAGs — straight-line workflows where each step runs once. But real agents need:

  • Loops — call a tool, look at the result, decide to call another tool, retry on failure.
  • Branching — different paths based on classification or tool output.
  • State across invocations — a chatbot that remembers, an agent that resumes after a crash.
  • Human approval — pause before sending the email, wait for the user to click "approve".
  • Streaming intermediate steps — show the user what the agent is thinking, not just the final answer.

Plain chains can't express any of that cleanly. LangGraph can.

2. Mental model

LangGraph borrows from two ideas:

  1. State machines — your app is a graph of nodes; the engine picks the next node based on the current state.
  2. Pregel / message-passing (the model behind Google's graph compute) — each node is a function state → state_update. The engine applies updates and routes to the next node.
flowchart LR
    S((START)) --> A[Node A<br/>reads state,<br/>returns update]
    A --> B[Node B]
    B --> C{Condition}
    C -->|x| D[Node D]
    C -->|y| B
    D --> E((END))

Three things make LangGraph distinctive vs LangChain:

LangChain (LCEL) LangGraph
DAG (no cycles) Arbitrary graph — cycles allowed
State is implicit (data flows in pipes) State is explicit — a TypedDict you define
No built-in persistence Built-in checkpointing to memory/SQLite/Postgres
One-shot or chat history Pause/resume, time-travel debugging, HITL interrupts

3. Architecture / Flow

flowchart TB
    subgraph Build [Build time]
      S[Define State<br/>TypedDict] --> N[Add Nodes<br/>functions]
      N --> E[Add Edges<br/>+ conditional edges]
      E --> CP[Compile<br/>+ checkpointer]
    end
    subgraph Run [Run time]
      I[Initial state] --> ENG[LangGraph Engine]
      ENG -->|reads/writes| ST[State Snapshot<br/>checkpointed]
      ENG --> OUT[Final state OR stream of updates]
    end
    CP -.compiled graph.-> ENG

4. Core concepts

  • StateGraph(StateSchema) — the builder. You define a state schema (a TypedDict) and the graph operates on instances of it.
  • Node — a Python function: state -> partial_state_update. Whatever you return gets merged into the state.
  • Edge — a directed connection saying "after node A, go to node B".
  • Conditional edgeadd_conditional_edges(source, router_fn, mapping)router_fn(state) returns a string that picks the next node.
  • START / END — the entry and exit pseudo-nodes.
  • compile() — turns the builder into a runnable graph; takes a checkpointer for persistence.
  • Reducer — when multiple updates touch the same state key, a reducer decides how to merge (replace, append, etc.). Set via Annotated[T, reducer_fn].
  • Checkpointer — where state snapshots get stored (MemorySaver, SqliteSaver, PostgresSaver).
  • Thread — a session ID. State is keyed by thread_id so you can have many independent conversations.

5. Code — minimal working example

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

# 1. Define the shape of state
class State(TypedDict):
    topic: str
    output: str

# 2. Define nodes — each is state -> partial state update
def greet(state: State):
    return {"output": f"Let's learn about {state['topic']}!"}

# 3. Build the graph
builder = StateGraph(State)
builder.add_node("greet", greet)
builder.add_edge(START, "greet")
builder.add_edge("greet", END)

graph = builder.compile()

# 4. Run
print(graph.invoke({"topic": "LangGraph"}))
# {'topic': 'LangGraph', 'output': "Let's learn about LangGraph!"}

That's the entire model. Every LangGraph app is a richer version of this.

6. Code — real-world pattern

Three-node workflow with an LLM in the middle and a conditional branch:

from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
from dotenv import load_dotenv

load_dotenv()
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

class ReviewState(TypedDict):
    review: str
    sentiment: str
    response: str

def classify(state: ReviewState):
    out = llm.invoke([
        SystemMessage("Classify the review sentiment as 'positive' or 'negative'. Reply with one word."),
        HumanMessage(state["review"]),
    ])
    return {"sentiment": out.content.strip().lower()}

def positive_reply(state: ReviewState):
    out = llm.invoke([HumanMessage(f"Write a warm thank-you reply to: {state['review']}")])
    return {"response": out.content}

def negative_reply(state: ReviewState):
    out = llm.invoke([HumanMessage(f"Write a sympathetic apology + offer to help: {state['review']}")])
    return {"response": out.content}

def route(state: ReviewState) -> Literal["positive_reply", "negative_reply"]:
    return "positive_reply" if "positive" in state["sentiment"] else "negative_reply"

builder = StateGraph(ReviewState)
builder.add_node("classify", classify)
builder.add_node("positive_reply", positive_reply)
builder.add_node("negative_reply", negative_reply)
builder.add_edge(START, "classify")
builder.add_conditional_edges("classify", route)
builder.add_edge("positive_reply", END)
builder.add_edge("negative_reply", END)

graph = builder.compile()
print(graph.invoke({"review": "The product broke in two days. Avoid."}))

You couldn't write this as a single LCEL pipe — the conditional branch makes it a graph, not a chain.

7. Common pitfalls

  • Returning the full state from a node instead of a partial update. Nodes should return only the keys they changed. The engine merges into the existing state.
  • Forgetting START and END edges. Every graph needs an entry edge from START and at least one terminal edge to END.
  • Mutating state in place. Always return a new dict — the engine relies on immutability for checkpointing.
  • Thinking LangGraph replaces LangChain. It doesn't — you still use LangChain models, prompts, parsers, retrievers, tools inside LangGraph nodes. LangGraph adds the orchestration layer on top.
  • Compiling without a checkpointer when you need persistence. Without one, state is lost between .invoke() calls — you can't have a chatbot.

8. When to use vs not use

Use LangGraph when Use plain LangChain (LCEL) when
You need loops or branching Linear DAG is enough
You need pause/resume / HITL One-shot request/response
State must persist across requests Stateless or simple chat history
You're building a real agent You're building a RAG pipeline
You want time-travel debugging LangSmith tracing alone is sufficient

9. Cheatsheet

# Install
# pip install langgraph langchain langchain-openai

# Core imports
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import MemorySaver
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.prebuilt import create_react_agent, ToolNode

# Build
builder = StateGraph(StateSchema)
builder.add_node("name", node_fn)
builder.add_edge(SRC, "name")           # static edge
builder.add_conditional_edges(           # router edge
    "name",
    router_fn,
    {"opt1": "node_a", "opt2": "node_b", END: END},
)
graph = builder.compile(checkpointer=MemorySaver())

# Run
graph.invoke(initial_state, config={"configurable": {"thread_id": "abc"}})
graph.stream(initial_state, config=...)
graph.get_state(config)                  # current snapshot
graph.get_state_history(config)          # all checkpoints
graph.update_state(config, {...})        # manually edit state

10. Q&A — recall test

  • Q: What's the single most important difference between LangChain (LCEL) and LangGraph? A: LCEL is a DAG (no cycles, implicit state). LangGraph is an arbitrary graph (cycles allowed) with explicit, checkpointable state.

  • Q: What does a node return? A: A partial state update — a dict containing only the keys it changed. The engine merges it into the running state.

  • Q: What is a Reducer? A: A merge function applied when multiple updates write the same key (e.g., add_messages for appending instead of replacing). Set via Annotated[T, reducer] on the state field.

  • Q: Why do you need compile() at all? A: It validates the graph (entry/exit edges, node references), binds a checkpointer, and returns a Runnable you can .invoke() / .stream(). The builder itself isn't runnable.

  • Q: Should I use LangGraph for a one-shot RAG question-answering app? A: Probably not — an LCEL chain is simpler. Reach for LangGraph when you need cycles, branching, state persistence, or HITL.

Practice

What does this print?

Expected: True

# LangGraph models workflows as graphs: nodes do work, edges route control
has_nodes = True
has_edges = True
has_state = True
print(has_nodes and has_edges and has_state)

Pick LangGraph (not LCEL) when you need a loop

Expected: True

needs_loop = True
use_lcel = True             # bug: LCEL doesn't support cycles — use LangGraph
print(not use_lcel)

Quiz — Quick check

What you remember

Q1. What does LangGraph give you that LCEL doesn't?

  • Cyclic workflows (loops, retries), branching, state persistence, human-in-the-loop
  • Faster execution
  • Smaller bundle size
  • Better LLM models

Why: LCEL builds linear pipelines or DAGs. LangGraph adds stateful, cyclic workflows — essential for agents that retry, plan, or pause for approval.

Q2. What are the three core concepts in LangGraph?

  • State, Nodes, Edges
  • Models, Prompts, Tools
  • Inputs, Outputs, Errors
  • Chains, Agents, Tools

Why: State is the shared data passed between nodes. Nodes are functions that read state and return updates. Edges decide which node runs next based on the state.

Q3. When should you reach for LangGraph instead of LangChain?

  • Multi-step agents with loops, branching logic, persistence, or human approval
  • Single LLM calls
  • Simple RAG
  • Embedding-only workflows

Why: For linear "embed → search → answer" chains, LangChain is plenty. The moment you need a tool-using agent that loops until done, or a workflow with human-in-the-loop, you're in LangGraph territory.

Common doubts

Is LangGraph a separate library or part of LangChain?

Separate package (pip install langgraph) but built by the same team. They integrate seamlessly — LangGraph nodes are often LangChain Runnables. Use LangChain for the components (prompts, models, parsers); use LangGraph for the orchestration when you need cycles.

Why graphs and not just chains?

Chains are linear or branching DAGs. Real workflows have cycles: a tool-calling agent loops "decide → call tool → observe → decide" until done. Cycles need graphs. Plus state persistence — checkpoints between every step — comes naturally with graphs.

Can I migrate from LangChain agents to LangGraph?

Yes — LangGraph has compatible primitives. The migration usually simplifies your code because state and routing become explicit instead of implicit. LangChain's older AgentExecutor is being phased out in favor of LangGraph.