Skip to main content
Articles/langchain/from-langchain-to-langgraph-when-agents-need-state-machines
Series: Stateful Graph Agentsβ€’Part 1 of 4
Scenario Article

From LangChain to LangGraph: When Agents Need State Machines

When a Working Agent Hits a Wall

Abstract Algorithmsβ€’Mar 28, 2026
XLinkedInReddit
πŸ’‘ Executive Summary

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

1

Stage 1 β€” Five Places Where AgentExecutor Runs Out of Road

Action / Decision:

AgentExecutor implements a ReAct loop: reason, act (pick a tool), observe (read the output), repeat.

Component Behavior:

That loop covers a large fraction of real-world agent tasks β€” but five patterns break it reliably.

Observable Output: None of these is a bug in LangChain.
Read Full Stage in Deep Dive ↓
πŸ“–

Deep Dive

Detailed Architectural Analysis

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.
Series Navigation β€’ Part 1 of 4

Stateful Graph Agents

View Full Series Overview β†’
First article in seriesNext Article β†’

Share this case study

Discuss architecture trade-offs and lessons with your team.

Share:XLinkedInReddit