Skip to content

LangGraph: An Agent as a State Machine

10 min read · updated August 4, 2026

LangGraph is what happens when you accept that an agent is a state machine rather than a chain. Its real product is not the graph syntax — that is a fortnight of work — it is the checkpointer, which persists the state after every step and lets a run be paused, inspected, resumed or forked. If you do not need that, you do not need LangGraph.

The mental model: state, nodes, edges

Three concepts, and the whole library follows from them.

ConceptDescription
StateA single typed object shared by the whole graph — typically a TypedDict. Every node receives it and returns a partial update rather than the whole thing. There is no hidden context: if it is not in the state, it does not exist between nodes.
NodeA plain function from state to a partial state update. It can call a model, run a tool, query a database or do arithmetic. Nodes are ordinary code, which is why this framework is easier to debug than chain-based ones.
EdgeWhere control goes next. Fixed edges always go to the same node; conditional edges call a function on the state and return the name of the next node. Conditional edges are how a model gets to decide what happens next without you giving up control of what is possible.
CheckpointerPersistence for the state after every step, keyed by a thread identifier. In-memory for tests, a database in production. This is the feature the other three exist to enable.

The consequence worth internalising: a cycle in the graph is legal and normal. An agent is a node that calls the model, a conditional edge that asks “did it request a tool?”, a node that runs the tool, and an edge back. That is the standard agent loop drawn as a graph, and drawing it that way is what makes it inspectable.

State is a schema with merge rules

The one non-obvious idea, and the one that causes the most confusion on day one. Because nodes return partial updates, the library needs to know how to merge each field. By default a returned field replaces the old value. That is wrong for message history, where you want the new messages appended, so a field can be annotated with a reducer function that says how to combine old and new.

# The state schema. Annotating a field with a reducer is the
# difference between a message list that grows and one that is
# overwritten by every node that touches it.

class State(TypedDict):
    messages: Annotated[list, add_messages]   # appended
    plan: str                                 # replaced
    attempts: int                             # replaced

# A node returns only what it changed:
def call_model(state: State) -> dict:
    reply = model.invoke(state["messages"])
    return {"messages": [reply]}

Two rules keep this from going wrong. Keep the state small and serialisable — it is written to storage after every step, so a database handle or an open file in the state is a bug waiting for the first checkpointed run. And never mutate the state object in place; return an update. In-place mutation appears to work until a checkpointed replay produces a different result from the original run, at which point you have a very unpleasant afternoon ahead.

Building the smallest useful graph

The construction API has been stable for a long time: add nodes, add edges, set an entry point, compile. Compilation is what turns the description into something runnable, and it is where the checkpointer is attached.

builder = StateGraph(State)

builder.add_node("agent", call_model)
builder.add_node("tools", run_tools)

builder.set_entry_point("agent")
builder.add_conditional_edges("agent", should_continue)  # -> "tools" or END
builder.add_edge("tools", "agent")                       # the cycle

graph = builder.compile(checkpointer=checkpointer)

config = {"configurable": {"thread_id": "conversation-42"}}
graph.invoke({"messages": [user_message]}, config)

The thread_id is the part to pay attention to. It is the identity of this conversation or this job, and it is what the checkpointer keys on. Two calls with the same thread identifier continue the same run; two with different identifiers are independent. Getting this wrong — reusing an identifier across users, most commonly — is a data-leak-shaped bug, so derive it from something that is already scoped, never from a counter.

Checkpoints are the point

After every node, the checkpointer writes the state. That single behaviour is what you are adopting the library for, and it buys four things that are individually hard.

  • Durability. A deploy, a crash or a timeout in the middle of a twelve-step run does not lose the eleven steps that completed. For anything that takes minutes, this is the difference between a feature and a demo.
  • Human in the loop. The graph can stop before a node, hand you the state, wait for an approval that arrives an hour later in a different process, and continue. Without persistence this requires you to invent a job queue and a state store; with it, it is a pause.
  • Time travel. Because every step is stored, you can read the state as of step four, change something, and run forward again. This is the best debugging tool in any agent framework and almost nobody uses it.
  • Memory across sessions. A thread resumed tomorrow has yesterday’s messages, without you writing a persistence layer for conversation memory.
The in-memory checkpointer is for tests only. It is the default in most tutorials, which means a lot of applications reach production with no persistence at all and discover it during the first restart. Use a database-backed checkpointer anywhere that matters, and check its retention behaviour — checkpoint tables grow quickly, because every step of every run writes a row.

Resuming a run that failed

This is the workflow that justifies the framework, so it is worth being concrete about the sequence rather than the exact call names, which vary by version.

  1. The run fails at step seven. A tool raised, a provider returned a 500, or the process was killed. Steps one to six are already on disk, keyed by the thread identifier.
  2. Read the last checkpoint. The graph object exposes the state for a thread. Look at it. This is where you find out that the failure was a malformed tool argument two steps earlier, not the step that actually raised.
  3. Decide: resume, repair or fork. Resume re-runs from the last completed step with the same state. Repair means writing a corrected state back for that thread and then resuming, which is how you get past a poisoned message without discarding the run. Forking starts a new branch from an earlier checkpoint, leaving the original intact.
  4. Invoke again with the same thread identifier and no new input. With state already present, this continues rather than starting over. That property is the whole feature.

Design your nodes for it. A node that is idempotent — one that can run twice without charging a card twice or sending an email twice — is one you can resume freely. A node with a side effect needs the effect recorded in the state so a replay can skip it. This is ordinary workflow-engine discipline and it applies here unchanged; the same reasoning appears in agent error recovery.

Where a graph is the wrong shape

Three honest limits. First, if your flow is a straight line with no branches and no cycles, the graph is ceremony: you have written a function call chain with more syntax. Second, if the set of steps is genuinely open-ended — the model decides not just which of your nodes to run but what the steps are — then you are back to a plain tool loop and the graph is a formality around a single cycle.

Third, and most practically: the graph is a poor fit for high-throughput, low-latency work. Every step writes a checkpoint, and for a two-node graph serving thousands of requests a second that write is a substantial fraction of the total cost. Checkpointing earns its keep on runs measured in seconds and minutes, not milliseconds.

There is also a version caution worth stating plainly: this project iterates quickly, and while the state-nodes-edges-checkpointer model has been stable, the names of the interrupt, streaming and store APIs have not been. Write your nodes as ordinary functions of state, keep the graph definition in one file, and a version bump touches one file rather than forty.