Part one of this series ended on a specific boundary:LangChain's chain model, even with LCEL's clean pipe syntax, is built for pipelines that flow forward, not ones that loop, branch based on a runtime decision, or need to pause and resume days later. That boundary is exactly where LangGraph starts, and it is worth being precise about what actually changes, because the difference is not "LangGraph is a more powerful LangChain," it is a genuinely different way of modeling a workflow.
From a chain to a graph
A chain, as LCEL expresses it, is a straight line, step one feeds step two feeds step three. LangGraph replaces that shape entirely with a directed graph: named nodes, each one a function that does some work, connected by edges that say which node runs next. The difference that actually matters is that edges do not have to move only forward. A node can have an edge pointing back to an earlier node, which is what makes a real loop possible, and an edge can be conditional, decided at runtime based on the current state, rather than fixed at the moment the pipeline was written.
That last part, an explicit, shared state object that every node reads from and writes back to, is the other core difference. A chain passes output from one step directly into the next. A graph maintains one persistent state across the entire run, and every node's job is to read what it needs from that state and return an update to it.
Nodes, edges, and where the loop actually lives
The basic building blocks, stripped down. A LangGraph graph starts with a state definition, a node or two, and edges connecting them to the built-in START and END markers:
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
url: str
result: str
def call_api(state: State):
result = fetch(state["url"])
return {"result": result}
builder = StateGraph(State)
builder.add_node("call_api", call_api)
builder.add_edge(START, "call_api")
builder.add_edge("call_api", END)
graph = builder.compile()
This alone is not more powerful than a simple LCEL chain, it is a single node, run once. The actual value shows up once you add a second kind of edge.
Conditional edges are where branching enters the picture.
Instead of a fixed edge, add_conditional_edges connects a node to a routing function that inspects the current state and decides, at runtime, which node should run next:
def route_by_category(state):
return state["category"]
workflow.add_conditional_edges(
"classify",
route_by_category,
{
"billing_support": "billing_support",
"technical_support": "technical_support",
"general_support": "general_support",
},
)
The graph does not know in advance which support node will run, that decision genuinely depends on what the model produced in the classify step and gets resolved fresh on every run. This is the kind of branching a linear chain has no clean way to express, because a chain's structure is fixed at write time, not decided at run time.
The loop itself is just an edge pointing backward.
This is the part that actually solves the problem part one ended on. A tool-calling agent, the classic think, act, observe pattern, is expressed as a conditional edge from the agent node that either goes to a tool node or exits, combined with a normal edge sending the tool node's result straight back to the agent:
workflow.add_node("email_agent", call_agent_model_node)
workflow.add_node("email_tools", tool_node)
workflow.add_edge(START, "email_agent")
workflow.add_conditional_edges(
"email_agent",
route_agent_graph_edge,
["email_tools", END],
)
workflow.add_edge("email_tools", "email_agent")
Trace the actual path: email_agent decides whether a tool call is needed. If yes, execution moves to email_tools, which runs the tool and then unconditionally routes straight back to email_agent. That last edge is the loop, drawn explicitly as a graph structure instead of hidden inside a while statement somewhere in application code. The agent keeps deciding, acting, and observing until its own routing function finally returns END instead of the tool node.
State and persistence turn a single run into something that can pause and resume.
Attaching a checkpointer to a compiled graph saves its state after every step, tied to a thread_id:
from langgraph.checkpoint.memory import InMemorySaver
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "session-1"}}
graph.invoke({"url": "https://example.com"}, config)
This is what makes a graph resumable across separate calls instead of only existing for the duration of one Python process. It is also what makes a real human-in-the-loop pattern possible: a node can call interrupt(), which pauses graph execution entirely and waits, potentially for a long time, for an external value before continuing exactly where it left off:
def human_review(state):
answer = interrupt("Do you approve?")
return {"messages": [{"role": "user", "content": answer}]}
A chain has no equivalent to this. Once an LCEL chain starts running, it runs to completion or it fails, there is no built-in notion of pausing mid-pipeline and picking back up later with new information.
Why this is a different tool, not a strictly better one
Everything LangGraph adds, explicit state, conditional routing, cycles, checkpointed persistence, comes with more to design and more to reason about compared to a linear chain. A straightforward RAG pipeline gains nothing from being modeled as a graph, it has no branches, no loops, and no need to pause mid-run, so wrapping it in StateGraph just adds ceremony around something LCEL already expresses more simply. LangGraph earns its complexity specifically when a workflow has a genuine loop, a genuine runtime branch, or a genuine need to persist and resume, not by default for anything agent-shaped.
What this means practically
⁘ Model a workflow as a graph the moment it needs to loop, branch based on a runtime decision, or survive being paused and resumed, not before.
⁘ A conditional edge pointing back to an earlier node is the actual mechanism behind a tool-calling agent loop, worth recognizing directly in the graph structure rather than treating agent behavior as a black box.
⁘ A checkpointer and thread_id are what separate a single in-memory run from a workflow that can genuinely pause for human input and resume later, which matters for anything that cannot or should not run start to finish unattended.
⁘ Reaching for LangGraph on a workflow that is genuinely linear adds structure without adding capability. The graph model earns its cost specifically on the class of problem chains cannot express.
Conclusion
The gap identified at the end of part one, loops, runtime branching, and persistent state, is exactly what LangGraph exists to close, by replacing a fixed, forward-only chain with an explicit graph where edges can point backward and state persists across steps and even across sessions. Part one's chain and this post's graph are not competing answers to the same question, they are the right tool for two different shapes of problem, and knowing which shape you actually have is most of the decision.
Top comments (0)