From LangChain to LangGraph: When Agents Need State Machines
When a Working Agent Hits a Wall
LangChain's AgentExecutor is a solid starting point β but it has five hard limits (no branching, no pause/resume, no parallelism, no human-in-the-loop, no crash recovery).
π― System Scenario & Requirements
System Goal
LangChain's AgentExecutor is a solid starting point β but it has five hard limits (no branching, no pause/resume, no parallelism, no human-in-the-loop, no crash recovery).
Actors & Components
- Application or API tier
- Core service under design
- Operator or SRE observer
Functional Requirements
- TLDR: LangChain's AgentExecutor is a solid starting point β but it has five hard limits (no branching, no pause/resume, no parallelism, no human-in-the-loop, no crash recovery).
- LangGraph replaces the implicit loop with an explicit graph, unlocking every one of those capabilities.
- This post explains the mental model, compares both approaches in code, and helps you decide when to upgrade.
Constraints & Assumptions
- A common misconception: "LangGraph replaces LangChain." It does not.
- LangGraph is built on top of LangChain .
β‘ Execution Stages & Decision Trace
Stage 1 β Five Places Where AgentExecutor Runs Out of Road
AgentExecutor implements a ReAct loop: reason, act (pick a tool), observe (read the output), repeat.
That loop covers a large fraction of real-world agent tasks β but five patterns break it reliably.
Deep Dive
Scenario context
LangChain's AgentExecutor is a solid starting point β but it has five hard limits (no branching, no pause/resume, no parallelism, no human-in-the-loop, no crash recovery). LangGraph replaces the implicit loop with an explicit graph, unlocking every one of those capabilities. This post explains the mental model, compares both approaches in code, and helps you decide when to upgrade.
You built a LangChain agenβ¦
This walkthrough focuses on When a Working Agent Hits a Wall.
Before making a design choice, clarify:
- It queries a knowledge base, calls a few tools, and returns useful answers.
- Then the product requirements arrive.
- "We need it to ask for user approval before it sends the email." You patch it with a flag.
Concepts in play: llm-agents-and-langgraph, graph-traversal-algorithms, sorting-and-searching-algorithms, database-indexing
Stage 1 β Five Places Where AgentExecutor Runs Out of Road
- AgentExecutor implements a ReAct loop: reason, act (pick a tool), observe (read the output), repeat.
- That loop covers a large fraction of real-world agent tasks β but five patterns break it reliably.
- None of these is a bug in LangChain.
- AgentExecutor was designed as a general-purpose, easy-to-use agent loop.
Observable outcome: AgentExecutor was designed as a general-purpose, easy-to-use agent loop.
Stage 2 β The State Machine Mental Model: From Recipes to Flowcharts
- The key mental shift is this: LangChain chain = a recipe.
- Execute step 1, then step 2, then step 3.
- Perfect for linear transformations.
- LangGraph StateGraph = a flowchart.
Observable outcome: LangGraph StateGraph = a flowchart.
Stage 3 β The Same Task, Two Ways: Research + Draft an Email
- The LangChain AgentExecutor Version This is clean and gets the job done.
- The LLM will call search web , read the results, then decide to call send email .
- But notice: there is no point in this code where you can intercept the agent before it sends the email.
- Adding an approval step means restructuring the entire calling code.
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_react_agent
from langchain_core.tools import tool
from langchain import hub
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
@tool
def search_web(query: str) -> str:
"""Search the web for information on a topic."""
# Replace with a real search integration (Tavily, Serper, etc.)
return f"[Search results for: {query}] β Key facts retrieved."
@tool
def send_email(recipient: str, subject: str, body: str) -> str:
"""Send an email. USE THIS ONLY AFTER RESEARCH IS COMPLETE."""
# In reality: sends the email. Here: simulates it.
return f"Email sent to {recipient} with subject '{subject}'."
tools = [search_web, send_email]
prompt = hub.pull("hwchase17/react") # standard ReAct prompt
agent = create_react_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
# This works β but you cannot pause for approval before send_email runs,
# cannot branch to a fallback if search_web returns empty,
# and cannot recover if the process crashes mid-run.
result = executor.invoke({
"input": "Research climate change impacts and draft an email to the team."
})
print(result["output"])
import operator
from typing import Annotated
from typing_extensions import TypedDict
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# ββ 1. Typed state schema ββββββββββββββββββββββββββββββββββββββββββββββββββββ
class EmailAgentState(TypedDict):
topic: str
research_results: str
draft: str
human_feedback: str
approved: bool
retry_count: Annotated[int, operator.add]
# ββ 2. Nodes β each is a plain Python function ββββββββββββββββββββββββββββββ
def research_node(state: EmailAgentState) -> dict:
"""Fetch research on the topic. Falls back gracefully if results are thin."""
response = llm.invoke([
HumanMessage(content=f"Summarize key facts about: {state['topic']}")
])
return {"research_results": response.content}
def draft_node(state: EmailAgentState) -> dict:
"""Draft the email body using research results."""
context = state["research_results"]
feedback = state.get("human_feedback", "")
prompt = f"Write a professional team email about {state['topic']}.\nResearch: {context}"
if feedback:
prompt += f"\nPrevious feedback to incorporate: {feedback}"
response = llm.invoke([HumanMessage(content=prompt)])
return {"draft": response.content, "approved": False}
def human_approval_node(state: EmailAgentState) -> dict:
"""
In production: suspend here and wait for a real human API call.
For this demo: auto-approve after the first retry to avoid an infinite loop.
"""
if state["retry_count"] > 0:
# Simulate approval after one retry
return {"approved": True, "human_feedback": ""}
# Simulate a rejection on first pass with feedback
return {
"approved": False,
"human_feedback": "Please make the tone more concise.",
"retry_count": 1, # operator.add accumulates this
}
def send_email_node(state: EmailAgentState) -> dict:
"""Send the approved draft β only reachable after human_approval_node approves."""
print(f"\nβ
Email sent!\n\n{state['draft']}")
return {}
# ββ 3. Routing functions βββββββββββββββββββββββββββββββββββββββββββββββββββββ
def route_after_approval(state: EmailAgentState) -> str:
"""Branch on approval flag β either send or loop back to drafting."""
return "send_email_node" if state["approved"] else "draft_node"
# ββ 4. Build the graph βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
builder = StateGraph(EmailAgentState)
builder.add_node("research_node", research_node)
builder.add_node("draft_node", draft_node)
builder.add_node("human_approval_node", human_approval_node)
builder.add_node("send_email_node", send_email_node)
builder.set_entry_point("research_node")
builder.add_edge("research_node", "draft_node")
builder.add_edge("draft_node", "human_approval_node")
builder.add_conditional_edges(
"human_approval_node",
route_after_approval,
{"send_email_node": "send_email_node", "draft_node": "draft_node"},
)
builder.add_edge("send_email_node", END)
# ββ 5. Compile with a checkpointer for crash recovery βββββββββββββββββββββββ
checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer)
# ββ 6. Run it ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
config = {"configurable": {"thread_id": "email-run-001"}}
result = graph.invoke(
{"topic": "climate change team update", "retry_count": 0,
"research_results": "", "draft": "", "human_feedback": "", "approved": False},
config=config,
)
Observable outcome: Adding an approval step means restructuring the entire calling code.
Stage 4 β Real-World Applications: Where the Graph Model Earns Its Complexity
- The recipe-vs-flowchart analogy holds in practice.
- Consider three production patterns that LangGraph handles and AgentExecutor cannot: Document approval pipelines β A legal review agent drafts a clause, routes it through compliance review, routes approved clauses to theβ¦
- route back for revision" decision is a conditional edge reading a review status field in state.
- Multi-source research with fallback β A researcher first queries an internal database node.
Observable outcome: Multi-source research with fallback β A researcher first queries an internal database node.
Stage 5 β LangGraph: The Open-Source Project and Its Architecture
- LangGraph is an open-source Python library from LangChain, Inc., released under the MIT license.
- Read them in order for the full progression, or jump directly to the capability you need.
Observable outcome: Read them in order for the full progression, or jump directly to the capability you need.
End-to-end outcome
It queries a knowledge base, calls a few tools, and returns useful answers.
Alternatives considered
Visualizing the Contrast: Linear Loop vs. Branching Graph
The diagrams below show the same "research and draft an email" task implemented in each model.
LangChain's core abstraction is the Runnable β a composable unit with invoke, stream, and batch methods. Chains are built by piping Runnables together: prompt | llm | parser. This is expressive and concise for sequential pipelines.
LangGraph's core abstraction is the StateGraph β a directed graph where nodes are Python functions and edges encode the routing logic.
The Internals: Superstep Execution and State Merging
When you call StateGraph.compile() , LangGraph validates the graph (checks for disconnected nodes, unreachable END , missing entry points) and produces a Pregel-style executor.
Performance Considerations: When Graph Overhead Is Worth It
LangGraph adds measurable overhead compared to a direct LCEL chain: For the typical agent (state = text + a few flags), serialization adds under a millisecond per superstep.
Decision Guide: Which Tool for Which Task?
The trigger: the moment your agent needs to decide where to go next based on what it just learned , you have outgrown a pipeline and need a graph.
Failure and degraded modes
Trade-offs and Failure Modes: When LangGraph Adds Overhead and When It Doesn't
- Detection: A common misconception: "LangGraph replaces LangChain." It does not.
- User or operator impact: A common misconception: "LangGraph replaces LangChain." It does not.
