← Writing
3 min read

LangGraph: When Your Agent Needs a State Machine

LCEL pipelines are straight lines. Real agents loop, branch, retry, and remember. LangGraph models an LLM app as a graph of nodes over shared state — here's the mental model and a working ReAct-style loop.

AIGenAILangGraphLLM

A LangChain pipeline is a straight line: input goes in one end, output comes out the other. That's perfect until your app needs to loop — call a tool, look at the result, decide whether to call another one, and only then answer. A straight line can't express "go back and try again."

LangGraph solves that by modeling your app as a graph: nodes that do work, edges that decide where to go next, and a shared state object that flows through it all. It hit stable v1.0 in late 2025 and is what companies like LinkedIn and Uber run their agent workflows on.

Three primitives

That's the whole vocabulary:

  • State — a TypedDict (or Pydantic model) that every node reads from and writes to. It's the app's memory as it moves through the graph.
  • Nodes — plain functions. Take the current state, do work (call an LLM, hit a database, run a tool), return an update to the state.
  • Edges — routing. A normal edge always goes A → B. A conditional edge runs a function to decide the next node, which is how you get loops and branches.

The smallest useful graph

graph.py
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END
from operator import add
 
class State(TypedDict):
    messages: Annotated[list, add]   # new messages append, not overwrite
 
def call_model(state: State):
    reply = model.invoke(state["messages"])
    return {"messages": [reply]}
 
builder = StateGraph(State)
builder.add_node("model", call_model)
builder.add_edge(START, "model")
builder.add_edge("model", END)
 
graph = builder.compile()
graph.invoke({"messages": [("user", "Hello")]})

The Annotated[list, add] part matters: it's a reducer. It tells LangGraph how to merge a node's update into state — here, append to the list instead of replacing it. Without a reducer, each node's return would clobber the previous messages.

The loop that makes it an agent

Here's where the graph earns its keep. A ReAct-style agent alternates between the model and its tools, and keeps going until the model stops asking for tools. That "keep going until" is a conditional edge pointing back at itself:

agent.py
def should_continue(state: State):
    last = state["messages"][-1]
    return "tools" if last.tool_calls else END
 
builder.add_node("model", call_model)
builder.add_node("tools", run_tools)
 
builder.add_edge(START, "model")
builder.add_conditional_edges("model", should_continue)  # model → tools OR end
builder.add_edge("tools", "model")                        # tools always loop back

Read the edges as a control-flow diagram: the model runs, and if it requested tools we go run them and come back to the model; otherwise we finish. That cycle — impossible in a linear chain — is the entire point of LangGraph.

Why state-as-a-graph beats a while loop

You could write the loop by hand with a while and some flags. LangGraph gives you three things that hand-rolling doesn't:

  • Persistence. Compile with a checkpointer and the state is saved at every step. You can pause a run, resume it later, or recover after a crash — essential for long-running or human-in-the-loop agents.
  • Streaming. You can stream state updates as each node finishes, so a UI shows "searching… reading… answering" instead of a frozen spinner.
  • Inspectability. The graph is a data structure. You can visualize it, log every transition, and debug which node misbehaved instead of staring at a loop.

When to reach for it

My rule: LCEL for straight lines, LangGraph for anything with a cycle or a decision. A summarizer is a pipeline. A research agent that searches, reads, decides it needs more, and searches again is a graph. Multi-agent systems — a supervisor routing to specialist workers — are just bigger graphs.

Don't start here for a one-shot prompt. But the day your prompt-chain grows a "and if that didn't work, try…", stop bolting conditionals onto a chain and model it as a graph. That's the moment LangGraph pays for itself.