A plain tool calling loop works, but it has no structure. The state is just the message list. There is no clean way to pause in the middle of a run, resume after a crash, or add a human approval step without hacking it in.
LangGraph is a library that structures an agent loop as a graph. Instead of managing a growing message list and writing the loop by hand, you get named nodes, explicit state, and edges that wire them together.
Four pieces#
| Piece | What it is |
|---|---|
| State | A typed object shared across the whole graph |
| Nodes | Functions or model calls that update the state |
| Edges | Connections that say which node runs next |
| Checkpointer | Saves the state after every node runs |
State#
State is a typed object shared across the whole graph. Every node reads from it and writes to it. Instead of passing data through the message list, you have explicit fields you can inspect at any point.
Nodes#
A node is a function or model call that updates the state. Each node does one thing: call the model, run a tool, validate a result, or format the output.
Edges#
Edges connect nodes.
- A fixed edge always goes to the same next node.
- A conditional edge looks at the current state and decides which node runs next. This is how branching and loops happen, for example "if the model asked for a tool, go to the tool node, otherwise finish".
Checkpointer#
The checkpointer saves the state after every node runs. Because the state is saved, you can:
- Pause the graph mid-run and wait for human input.
- Resume after a crash without starting over.
- Replay from any node to debug.
- Interrupt before a risky action to get approval.
A small graph#
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import InMemorySaver
class State(TypedDict):
messages: list
pending_tool: dict | None
def call_model(state: State) -> dict: ... # returns updated fields
def run_tool(state: State) -> dict: ...
def next_step(state: State) -> str:
return "run_tool" if state["pending_tool"] else END
graph = StateGraph(State)
graph.add_node("call_model", call_model)
graph.add_node("run_tool", run_tool)
graph.add_edge(START, "call_model")
graph.add_conditional_edges("call_model", next_step)
graph.add_edge("run_tool", "call_model") # fixed edge: loop back
app = graph.compile(
checkpointer=InMemorySaver(),
interrupt_before=["run_tool"], # pause for approval before any tool runs
)
The conditional edge is the agent loop, and interrupt_before is the human approval step. Both come from the graph structure instead of hand-written loop logic. Check the LangGraph docs for the current API, since names change between versions.
When it pays off#
| Worth it | Overkill |
|---|---|
| Human-in-the-loop approval | A simple agent with two tools |
| Pausing and resuming long runs | A short loop with no approvals |
| Complex branching logic | A fixed workflow of a few steps |
| Debugging across many steps |
Short line#
LangGraph turns a hand-written tool loop into a graph: typed state shared by every node, nodes that each do one job, fixed and conditional edges for routing and loops, and a checkpointer that saves state after every node so runs can pause, resume, replay and wait for approval.
Key takeaways#
- A plain loop keeps state in the message list and cannot pause, resume or add approval cleanly.
- State is a typed object every node reads and writes.
- Nodes do one job each: call the model, run a tool, validate, format.
- Fixed edges always go to the same node. Conditional edges choose from the state, which creates branches and loops.
- The checkpointer saves state after every node, enabling pause, resume, replay and approval before risky actions.
- Worth it for approvals, long runs, complex branching and debugging. Overkill for a simple two-tool agent.