All articles
August 3, 2025 6 min read

Building production agents with LangGraph: state, checkpoints, and control

A practical walkthrough of modelling an agent as an explicit graph — nodes, conditional edges, shared state, and checkpoints that make sessions resumable and debuggable.

Written forEngineering
LangGraphAgentsOrchestration

The earlier LangGraph post made the case for modelling an agent as a graph. This one is the hands-on version: how the nodes, edges, state, and checkpoints actually fit together, and why that structure is what makes an agent safe to run in production.

STARTagent · LLMtoolscontinue?noENDloopcheckpoints
The agent node loops through tools via a conditional edge until it's done; state is checkpointed at each node, so a session can pause, resume, and be inspected.

Model the agent as a graph

Nodes are steps (an LLM call, a tool executor, a bit of logic). Edges are transitions. A shared state object is threaded through every node, so each one reads and updates a common memory. You build the graph, then compile it.

A minimal agent graph
from langgraph.graph import StateGraph, END

g = StateGraph(AgentState)
g.add_node('agent', call_model)
g.add_node('tools', run_tools)
g.set_entry_point('agent')

def route(state):                     # conditional edge
    return 'tools' if state['tool_calls'] else END

g.add_conditional_edges('agent', route)
g.add_edge('tools', 'agent')          # loop back to reason again

app = g.compile(checkpointer=saver)   # resumable

Conditional edges are where you keep control

The router function inspects the state and decides the next node — continue to tools, or finish. This is exactly where you cap the loop (a step counter in state), branch on results, and stop a runaway agent. The control flow is explicit and testable, not buried in a free-form 'think then act' prompt.

Checkpoints make sessions resumable

A checkpointer persists the state after every node against a thread id. That single feature buys you a lot: a conversation can pause for human approval and resume, a crashed run can continue instead of restarting, and you can inspect the exact state at any step when debugging.

State is checkpointed per thread
# each conversation is a thread; state is saved at every step
cfg = {'configurable': {'thread_id': user_id}}
app.invoke({'messages': [msg]}, cfg)
# crashed or paused? invoke again with the same thread_id — it resumes

Why this beats a free-form loop

  • Explicit — you can see and reason about every path the agent can take.
  • Testable — nodes and routing are ordinary functions you can unit-test.
  • Observable — pair it with tracing (see the LangSmith post) and nothing is a black box.
  • Resumable — checkpointing turns a fragile long-running loop into a durable one.
A production agent isn't a clever loop — it's an explicit graph with saved state, so you always know where it is and can pick up exactly where it left off.
Building something with LLMs?
I help teams ship GenAI that’s reliable and cost-efficient.
Let’s talk