Library
7 min read

Building agents with LangGraph

Every agent framework hides the same loop: call the model, run the tools it asked for, repeat until it answers. Most frameworks hide it completely. LangGraph, from the LangChain team, does the opposite: it makes you draw the loop as a graph of steps over shared state. That sounds like more work, and for a simple chatbot it is. For anything with branches, approvals, retries, several agents or runs that last hours, having the control flow written down explicitly is exactly what you want.

This chapter builds a small support agent in LangGraph, step by step, so the concepts stick. LangGraph exists for Python and for JavaScript and TypeScript (LangGraph.js) with the same ideas; the code here is Python.

Your answer

An agent framework that hides the loop is quicker to start with. What would you lose by not being able to see and edit the loop? Name one situation where that matters.

Four ideas#

State. A typed dictionary that every step reads and writes, for example the list of messages. Each field can have a reducer that says how updates combine: operator.add on a list means "append what a node returns" instead of "replace".

Nodes. Plain functions that take the state and return the fields they want to update. "Call the model" is a node. "Run the tools" is a node.

Edges. Which node runs next. A normal edge always goes to the same place. A conditional edge calls a function that looks at the state and picks, which is how loops and branches are made.

Compile. Turns the graph into something you can invoke, stream, and, with a checkpointer, pause and resume.

Here is the agent we are about to write, running once.

1/4 The run starts with state = { messages: [the customer question] } and goes to call_model.

The code#

python
import operator
from typing import Literal
 
from typing_extensions import Annotated, TypedDict
from langchain.chat_models import init_chat_model
from langchain.messages import AnyMessage, HumanMessage, SystemMessage, ToolMessage
from langchain.tools import tool
from langgraph.graph import END, START, StateGraph
 
model = init_chat_model("anthropic:claude-opus-5-5")
 
 
@tool
def get_order_status(order_id: str) -> str:
    """Look up the delivery status and ETA of one order by its id."""
    return orders_api.status(order_id)  # your real lookup
 
 
tools_by_name = {get_order_status.name: get_order_status}
model_with_tools = model.bind_tools([get_order_status])
 
 
class State(TypedDict):
    # operator.add: nodes append messages instead of replacing the list
    messages: Annotated[list[AnyMessage], operator.add]
 
 
def call_model(state: State):
    system = SystemMessage("You are a support agent for a food delivery app.")
    return {"messages": [model_with_tools.invoke([system] + state["messages"])]}
 
 
def run_tools(state: State):
    results = []
    for call in state["messages"][-1].tool_calls:
        output = tools_by_name[call["name"]].invoke(call["args"])
        results.append(ToolMessage(content=output, tool_call_id=call["id"]))
    return {"messages": results}
 
 
def route(state: State) -> Literal["run_tools", END]:
    return "run_tools" if state["messages"][-1].tool_calls else END
 
 
builder = StateGraph(State)
builder.add_node("call_model", call_model)
builder.add_node("run_tools", run_tools)
builder.add_edge(START, "call_model")
builder.add_conditional_edges("call_model", route, ["run_tools", END])
builder.add_edge("run_tools", "call_model")
agent = builder.compile()
 
result = agent.invoke({"messages": [HumanMessage("Where is order 44821?")]})
print(result["messages"][-1].content)

Every line of the agent loop is visible: the system prompt, the tool dispatch, the decision to continue. That is the point. To add a rule such as "refunds over Rs 500 go to a human", you add a node and a branch in route. You do not fight a framework that decided the loop for you.

LangChain also ships a prebuilt agent for the standard loop, for when you do not need to change it. Reach for the graph when you do.

Memory and pausing: checkpointers#

Compile the graph with a checkpointer and LangGraph saves the state after every step, keyed by a thread_id. Three things come with that for free: conversation memory (call again with the same thread and the history is there), recovery (a crashed run resumes from its last step), and pausing for a human (covered in the next chapters).

python
from langgraph.checkpoint.memory import InMemorySaver
 
agent = builder.compile(checkpointer=InMemorySaver())
config = {"configurable": {"thread_id": "customer-17"}}
 
agent.invoke({"messages": [HumanMessage("Where is order 44821?")]}, config)
agent.invoke({"messages": [HumanMessage("And can I change the address?")]}, config)

The second call knows which order "the address" refers to, because the thread's state was restored. InMemorySaver is for development. In production use a database backed checkpointer, such as the Postgres one.

ChoiceWhat you gainWhat you payPick it when
Explicit graph (LangGraph)Every branch, loop and approval point is visible and editable. Built in persistence and interrupts.More code and concepts for simple agents, and a framework to learn.Agents with custom control flow, human approval, long runs or several agents.
Prebuilt agent loopA working tool calling agent in a few lines.Changing the loop means dropping down a level anyway.Prototypes, and agents whose loop really is the standard one.
No frameworkFull control, no dependency, nothing hidden.You build persistence, streaming and resumption yourself.Small agents, or teams that want to own every line.
How would you add it in LangGraph?
Refunds above Rs 500 need a supervisor
Classify the question first and use different prompts per type
Remember the conversation across messages
Stop after 10 tool rounds

Checkpoint#

Checkpoint

1. In the graph above, which piece creates the loop?

2. What does Annotated[list, operator.add] do on the messages field?

3. What does compiling with a checkpointer and passing a thread_id give you?

Worth memorising
  • LangGraph: state, nodes, edges, compile. The agent loop is a cycle you can see.
  • Reducers say how updates combine. Append for messages.
  • Conditional edges make branches and loops. Budgets go there too.
  • Checkpointer plus thread_id gives memory, recovery and pausing.
Say this in 60 seconds

LangGraph models an agent as a graph over shared state. State is a typed dictionary with reducers that say how updates combine, nodes are functions that return state updates, edges decide what runs next, and conditional edges create branches and the agent loop itself: call the model, route to the tool node if there are tool calls, and come back until it answers. Because the loop is explicit, adding an approval branch, a router or a step budget is just another node or condition. Compiling with a checkpointer saves state after every step under a thread id, which gives conversation memory, crash recovery and pausing for a human. For a standard loop a prebuilt agent is quicker. I reach for the graph when the control flow is mine to design.