Room 42, home

LangGraph basics: agents as graphs

Model an agent's control flow as a graph of state, nodes and edges, and learn the loops that let an agent act, check its work and pause.

Agentsadvanced3 min readUpdated 12 Jun 2026

In short

  1. 01LangGraph models agent control flow as a graph of nodes and edges with shared state, which is what LangChain's own agents run on.
  2. 02A graph adds the three things a flat chain cannot: cycles, conditional edges that route by state, and one shared state object.
  3. 03A model node, a tool node, and a conditional edge that loops back form a complete agent that acts until the work is done.
  4. 04Use the create_agent helper for the standard loop, and add a checkpointer and interrupts for persistence and human-in-the-loop.
Contents · 5 sections
  1. 1Why use a graph instead of a chain?
  2. 2What are the building blocks of a LangGraph?
  3. 3How do I build an agent loop in LangGraph?
  4. 4Do I need to build the graph by hand?
  5. 5How do I add persistence and human-in-the-loop?

A LangChain chain runs one way, start to finish. Real agents need to loop: call a model, run a tool, feed the result back, and go again until the work is done. LangGraph models that as a graph of nodes and edges with shared state, and it is what LangChain's own agents run on under the hood.

Why use a graph instead of a chain?

A chain is a one-way pipe. A graph adds the three things an agent needs:

  • Cycles · the model and its tools can pass control back and forth until a stopping condition is met.
  • Control flow · conditional edgesAn edge whose destination is chosen at runtime by a function that inspects the current state, rather than always going to the same next node. route to different nodes based on what just happened.
  • Shared state · every node reads and updates one state object as it runs.

What are the building blocks of a LangGraph?

LangGraph has a small vocabulary:

  • State · a typed object that flows through the graph. For chat agents the key piece is a messages list with a reducerA function that decides how a node update is merged into existing state, for example appending to a list instead of overwriting it. that appends new messages.Common gotchaThe easy mistake is declaring messages as a plain list without the add_messages reducer.Without it every node return overwrites the whole history instead of appending, so the model loses the prior turns and the tool loop forgets what it just did. Annotate the field as Annotated[list, add_messages] so each update merges in.
  • Nodes · functions that take the state and return an update to it.
  • Edges · connect nodes. A plain edge always goes to the next node; a conditional edge picks the next node based on the state.
  • START and END · the entry and exit sentinelsSpecial marker values that stand for the graph entry and exit points rather than being real nodes you write..
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from typing_extensions import TypedDict, Annotated
 
class State(TypedDict):
    messages: Annotated[list, add_messages]

How do I build an agent loop in LangGraph?

Here is a complete agent: a model node, a tool node, and a conditional edge that loops back whenever the model wants to call a tool.

from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode
from langchain_anthropic import ChatAnthropic
from langchain.tools import tool
from typing_extensions import TypedDict, Annotated
 
@tool
def get_weather(city: str) -> str:
    """Return the weather for a city."""
    return f"Sunny in {city}"
 
tools = [get_weather]
model = ChatAnthropic(model="claude-sonnet-4-6").bind_tools(tools)
 
class State(TypedDict):
    messages: Annotated[list, add_messages]
 
def call_model(state: State):
    return {"messages": [model.invoke(state["messages"])]}
 
def should_continue(state: State):
    last = state["messages"][-1]
    return "tools" if last.tool_calls else END
 
builder = StateGraph(State)
builder.add_node("model", call_model)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "model")
builder.add_conditional_edges("model", should_continue, ["tools", END])
builder.add_edge("tools", "model")  # loop back
graph = builder.compile()
 
graph.invoke({"messages": [{"role": "user", "content": "Weather in SF?"}]})

The shape is the whole idea: the model runs, and if it asked for a tool, control goes to the tool node and then back to the model. That cycle is what a flat chain cannot express.

Do I need to build the graph by hand?

For a standard agent you rarely need to wire the graph yourself. LangChain ships a create_agent helper that builds this exact loop for you:

from langchain.agents import create_agent
 
agent = create_agent(
    model="anthropic:claude-sonnet-4-6",
    tools=[get_weather],
    system_prompt="You are a helpful assistant.",
)
agent.invoke({"messages": [{"role": "user", "content": "Weather in SF?"}]})

Drop down to a hand-built StateGraph when you need control the prebuilt agent does not give you: parallel nodes, custom branching, retries, or a supervisorA coordinating agent that delegates subtasks to several specialised worker agents and combines their results. coordinating several workers.

How do I add persistence and human-in-the-loop?

Two features turn a toy into something usable.

A checkpointer saves state between calls, keyed by a thread idA string id that groups all the saved snapshots belonging to one conversation so they can be resumed later., so a conversation survives across invocations:Common gotchaAttaching a checkpointer but leaving the thread_id out of the config silently breaks persistence: with no key to load, every call starts a fresh conversation.Always pass the same thread_id to resume, and a new one to start a separate conversation.

from langgraph.checkpoint.memory import MemorySaver
 
graph = builder.compile(checkpointer=MemorySaver())
config = {"configurable": {"thread_id": "conversation-1"}}
graph.invoke(inputs, config)

An interrupt pauses the graph to ask a human, then resumes with their answer:

from langgraph.types import interrupt, Command
 
def review(state: State):
    decision = interrupt({"question": "Approve this action?"})
    return {"messages": [{"role": "user", "content": decision}]}
 
# First call pauses at the interrupt; resume with the human's value:
graph.invoke(Command(resume="yes, proceed"), config)

Interrupts need a checkpointer and a thread id, because pausing means saving the whole state and picking it back up later.

With state, loops, and pauses, you can build capable agents, and keep your agent on track shows how to stop them looping forever. The next two guides move to Anthropic's own Claude Agent SDK for doing this with less wiring.