Blogs / LangChain vs LangGraph: When You Actually Need a Graph

LangChain vs LangGraph: When You Actually Need a Graph

Published
October 1, 2026
Author
Faizan Nadeem
Tags
LangGraph LangChain AI Agents Python
An abstract image of a network of interconnected dots
Photo by BoliviaInteligente on Unsplash

Ask a team why they moved from LangChain to LangGraph and you’ll usually get a shrug and “the agent kept doing weird stuff.” That answer is more accurate than it sounds, but it hides the actual mechanism. A AgentExecutor chain is a loop wearing a trench coat β€” it looks like it’s making decisions, but underneath it’s a fixed while-loop that calls a model, checks for a tool call, runs the tool, and repeats until the model stops asking for tools. There’s no way to pause it, no way to branch it, and no way to make it remember anything past what fits in the prompt on the next iteration.

That’s fine for a huge number of real use cases, and this is the part most “LangGraph is better” takes skip. The real question isn’t “which one is better” β€” it’s “does my control flow need to loop back on itself, fork into parallel branches, or survive a restart.” Most agents don’t. Some genuinely do, and forcing those into a linear chain doesn’t fail loudly β€” it fails as flaky behavior, silent state loss, and retries that quietly re-run side effects. This post is about knowing which category you’re in before you write a line of orchestration code.

You’ll learn:

  • What a LangChain chain and AgentExecutor actually do under the hood
  • The three concrete symptoms that mean you’ve outgrown a chain
  • How LangGraph models control flow as a graph with nodes, edges, and shared state
  • A side-by-side comparison of AgentExecutor and a StateGraph built for the same task
  • How LangGraph’s checkpointer gives you durable, resumable state that a chain doesn’t have
  • A decision framework for choosing between the two on your next project
  • What migrating an existing AgentExecutor agent to LangGraph actually involves

Table of Contents

  1. What LangChain Actually Is
  2. Where a Chain Stops Being Enough
  3. What LangGraph Adds
  4. AgentExecutor vs StateGraph: Same Agent, Two Ways
  5. Durable State and Why It Matters
  6. When to Use LangGraph β€” and When Not To
  7. Migrating from AgentExecutor to LangGraph
  8. Where to Go From Here

What LangChain Actually Is

LangChain’s core abstraction is the chain: a sequence of steps β€” a prompt template, a model call, an output parser β€” composed with the | operator via LCEL (LangChain Expression Language). Each step takes the previous step’s output as input. It’s a pipeline, and pipelines are a genuinely good model for anything that’s actually linear: retrieve documents, stuff them into a prompt, call the model, parse the response.

from langchain_anthropic import ChatAnthropic
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

prompt = ChatPromptTemplate.from_template("Summarize this in two sentences:\n\n{text}")
model = ChatAnthropic(model="claude-sonnet-5")
chain = prompt | model | StrOutputParser()

result = chain.invoke({"text": long_document})

Agents built on top of this use AgentExecutor, which wraps the same idea in a loop: call the model with a set of tools bound to it, check whether the response asks for a tool call, run the tool if so, feed the result back in, and repeat until the model returns a final answer instead of a tool call.

from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a helpful research assistant."),
    ("human", "{input}"),
    ("placeholder", "{agent_scratchpad}"),
])

agent = create_tool_calling_agent(model, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, max_iterations=10)

result = executor.invoke({"input": "What's the current weather in Lahore, and is it good for a run?"})

This works well for a single-agent, single-thread loop with a bounded number of tool calls. The loop is opaque, though β€” you don’t control what happens between iterations, you can’t pause it mid-loop and resume it later, and there’s no way to have two branches of reasoning run and then merge back together. max_iterations is the only real safety valve, and it’s a blunt one: it just stops the loop, it doesn’t let you redirect it.

Where a Chain Stops Being Enough

Three symptoms reliably show up right before a team reaches for LangGraph. None of them are exotic β€” they’re what happens naturally once an agent’s job gets slightly more real.

1. The agent needs to loop back on itself with a purpose, not just retry. A chain retries by re-running the same fixed sequence. But a genuinely useful pattern β€” write code, run tests, read the failure, fix the code, run tests again β€” requires the outcome of one step to determine which step runs next, potentially cycling several times before exiting. AgentExecutor can technically get there through repeated tool calls, but you have no visibility or control over the cycle itself; it’s just “the loop happened to call the tool again.”

2. The agent needs to branch and possibly run steps in parallel. Say a request should either go to a fast-path answer or a multi-step research path, decided by classifying the query first. A chain is one path. Getting a branch in AgentExecutor means writing custom logic outside the executor and re-invoking it conditionally β€” at which point you’re building your own orchestration on top of an abstraction that wasn’t designed to expose it.

3. The agent’s state needs to survive past the process that’s running it. If a chain’s process crashes, restarts, or a request needs to pause for minutes (a human review) or days (waiting on an external event), AgentExecutor has nowhere to put that state. It lives in memory, in Python variables, for the duration of a single .invoke() call. When that call ends, so does everything it knew.

If none of these three apply to what you’re building, a chain is not a lesser choice β€” it’s usually the right choice, because it’s simpler to reason about, cheaper to run (no graph traversal overhead), and easier to debug with a linear stack trace.

What LangGraph Adds

LangGraph reframes an agent’s control flow as an explicit graph: a set of nodes (each one a function that receives the current state and returns updates to it), edges (which connect nodes, either unconditionally or based on a routing function), and a shared state object that every node reads from and writes to. Cycles are not a workaround here β€” they’re the default case a graph naturally supports, because an edge can point back to a node you’ve already visited.

from typing import Annotated, TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode

class AgentState(TypedDict):
    messages: Annotated[list, add_messages]

def call_model(state: AgentState):
    response = model_with_tools.invoke(state["messages"])
    return {"messages": [response]}

def should_continue(state: AgentState):
    last_message = state["messages"][-1]
    return "tools" if last_message.tool_calls else END

builder = StateGraph(AgentState)
builder.add_node("agent", call_model)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", should_continue, {"tools": "tools", END: END})
builder.add_edge("tools", "agent")

graph = builder.compile()

This looks more verbose than AgentExecutor for the same basic loop, and it is β€” that’s the tradeoff. What you get in exchange is that the cycle (agent β†’ tools β†’ agent) is now a fact you can see, modify, and reason about instead of something buried inside a library’s internal loop. You can add a third node that does nothing but log the state, insert a conditional edge that routes to a human-review node instead of looping back automatically, or run two independent branches from the same node and merge their results β€” none of which fit naturally into AgentExecutor’s model.

The comparison people actually mean when they say “LangGraph vs AgentExecutor” is this: AgentExecutor gives you a fixed loop shape with tool-calling built in; StateGraph gives you an arbitrary graph shape where tool-calling is just one common pattern you build on top of, not a shape the framework hands you for free.

AgentExecutor vs StateGraph: Same Agent, Two Ways

To make the difference concrete, here’s the same “research and summarize” agent, once as an AgentExecutor, once as a StateGraph with an explicit review branch.

AgentExecutor version β€” linear, no branch:

executor = AgentExecutor(agent=agent, tools=[search_tool, summarize_tool])
result = executor.invoke({"input": "Research recent LLM pricing changes and summarize."})

Every run follows the same implicit path: model decides, tool runs, model decides again. If you wanted “summarize only if the research turned up more than three sources, otherwise ask a follow-up,” you’d need to post-process the executor’s output and possibly re-invoke it β€” glue code sitting outside the framework.

StateGraph version β€” same task, with an explicit conditional branch:

def route_after_research(state: AgentState) -> str:
    sources = state.get("source_count", 0)
    return "summarize" if sources > 3 else "ask_followup"

builder = StateGraph(AgentState)
builder.add_node("research", research_node)
builder.add_node("summarize", summarize_node)
builder.add_node("ask_followup", followup_node)
builder.add_edge(START, "research")
builder.add_conditional_edges("research", route_after_research)
builder.add_edge("summarize", END)
builder.add_edge("ask_followup", END)

graph = builder.compile()

The branch is now a first-class part of the graph definition β€” visible, testable in isolation (you can unit-test route_after_research as a pure function), and independent of how many tool calls the research step needed internally. That’s the actual delta: not “LangGraph is smarter,” but “the control flow decision moved from implicit (inside the loop) to explicit (a node in the graph).”

Parallel Branches: Something AgentExecutor Can’t Do At All

The research/summarize example above is a conditional branch β€” one path or the other. LangGraph also supports genuine fan-out/fan-in: multiple nodes running from the same predecessor, with the graph waiting for all of them before continuing. This has no real equivalent in AgentExecutor, whose loop is inherently single-threaded through the model.

def route_to_all_sources(state: AgentState) -> list[str]:
    return ["search_web", "search_docs", "search_internal_db"]

builder.add_conditional_edges("plan", route_to_all_sources)
builder.add_edge("search_web", "merge_results")
builder.add_edge("search_docs", "merge_results")
builder.add_edge("search_internal_db", "merge_results")
builder.add_node("merge_results", merge_node)

Each of the three search nodes runs independently and writes its findings into the shared state; merge_results only executes once all three have completed, and it sees the combined state. Trying to replicate this inside AgentExecutor means stepping outside the framework entirely β€” running your own asyncio.gather() over separate executor instances and stitching the results back together by hand, which is exactly the kind of orchestration code LangGraph is meant to absorb.

A Quick Side-by-Side

AgentExecutorLangGraph StateGraph
Control flow shapeFixed loop (call model, run tool, repeat)Arbitrary graph you define
BranchingNot native β€” requires external glue codeadd_conditional_edges, first-class
Parallel fan-outNot supportedNative, via multiple edges from one node
CyclesImplicit inside the loop, opaqueExplicit edges, fully visible
Persisted stateNone β€” lives in memory for one .invoke()Checkpointer (memory, SQLite, Postgres)
Resuming after a restartNot possiblethread_id + checkpointer
DebuggingStack trace through library internalsInspect state at any node boundary
Learning curveLower β€” a handful of function callsHigher β€” you define nodes, edges, and state explicitly

Durable State and Why It Matters

The other piece AgentExecutor genuinely lacks is a checkpointer. LangGraph can persist the full state object after every node executes, keyed by a thread_id, using a pluggable backend β€” in-memory for development, Postgres or SQLite for anything that needs to survive a process restart.

from langgraph.checkpoint.memory import MemorySaver

checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer)

config = {"configurable": {"thread_id": "user-42-session-1"}}
graph.invoke({"messages": [("user", "Start the research task")]}, config)

Because every node’s output is checkpointed, you can call graph.invoke() again later with the same thread_id and it picks up exactly where it left off β€” same message history, same intermediate state, no re-running of side effects that already happened. AgentExecutor has no equivalent: kill the Python process mid-run and the agent’s entire working memory is gone. This durability is also what makes patterns like human-in-the-loop review practical instead of theoretical β€” a graph can pause at a node, wait indefinitely, and resume with the state exactly as it left it. That’s the subject of its own deep dive, linked below.

When to Use LangGraph β€” and When Not To

Use a plain LangChain chain (with or without AgentExecutor) when:

  • The task is fundamentally linear: retrieve, augment, generate.
  • Tool calls are bounded and don’t need conditional branching based on their results.
  • The whole interaction completes within a single process lifetime β€” no pausing across restarts or waiting on a human for an extended period.
  • You value the smaller, more predictable surface area of a simpler abstraction.

Reach for LangGraph when at least one of these is true:

  • The agent needs a cycle whose exit condition depends on evaluated state, not just “the model stopped calling tools” (test-fix-retest loops, multi-round negotiation between sub-agents).
  • You need explicit branching β€” different paths for different classifications of the same input, or parallel branches that fan out and later merge.
  • The workflow must survive a process restart, or needs to pause for an unbounded amount of time (human approval, waiting on an async webhook) and resume later with full context intact.
  • You’re building a multi-agent system where several agents need to share and update a common state object rather than just passing text back and forth.

A useful gut check: if you can draw your agent’s logic as a single arrow going left to right, you don’t need a graph yet. The moment you draw an arrow that loops back, or two arrows that come out of the same box, you’re already describing a graph β€” LangGraph just gives that drawing an execution engine.

Migrating from AgentExecutor to LangGraph

If you’re migrating an existing agent rather than starting fresh, the mechanical part is smaller than it looks:

  1. Model your state. Take whatever AgentExecutor was threading through invocations (usually just messages) and define it as a TypedDict.
  2. Turn your tool-calling loop into two nodes. One node calls the model with tools bound; a ToolNode (LangGraph ships this prebuilt) executes any requested tool calls.
  3. Replace max_iterations with an explicit conditional edge. Instead of a hard cap, write a routing function that decides whether to loop back to the model node or exit β€” you can still cap iteration count inside that function if you want the same safety net.
  4. Add a checkpointer before you add anything else. Even MemorySaver in development gets you used to threading a thread_id through every call, which is the part most migrations forget until they need durability in production.
  5. Only then add the branch or cycle you actually needed. The graph shape you migrate to should be no more complex than the AgentExecutor loop you had β€” additional nodes are for the new behavior you’re adding, not required scaffolding.

Most of this is a few hours of work for a small agent, not a rewrite. The bigger shift is mental: you stop thinking of the agent as “a loop with tools” and start thinking of it as “a state machine with a model in some of the nodes.”

Where to Go From Here

This post covers the decision point β€” when a chain stops being enough and a graph starts paying for itself. The rest of this series picks up from there, going deeper into the pieces that only matter once you’ve made that jump:

Wrapping Up

AgentExecutor and StateGraph aren’t a “which is better” comparison β€” they’re solving for different amounts of control flow complexity. A chain is the right tool when your agent’s logic really is a straight line with some tool calls along the way, and reaching for a graph before you need one just adds nodes and edges you have to maintain for no behavioral gain. But the moment your agent needs to cycle with purpose, branch on evaluated state, or survive longer than a single process lifetime, a chain doesn’t gracefully degrade β€” it starts accumulating workarounds bolted on from outside the framework. LangGraph exists for exactly that moment, and knowing which side of it you’re on before you start writing orchestration code will save you a rewrite later.

Related: See how an MCP server supports software investigation in Reverse Engineer Anything (REA): setup and use cases.

Faizan Nadeem
Author
Faizan Nadeem

AI/GenAI engineer specialising in agentic RAG, LangGraph, and production LLM systems with Python and FastAPI. 2 years building AI agents, RAG pipelines, and the backends behind them.

More articles