The agent loop
The agent loop is the pattern that makes an agent: call the model, and if it asked for a tool, run the tool and loop back to the model, otherwise stop.
Last updated: 27 Sep, 2026 · LangGraph 1.2
Everything so far comes together here. The one new idea is the edge from the tools node back to the model, so the model can read the tool's result and decide again.
The agent-loop wiring
from langgraph.graph import MessagesState, StateGraph, START, END
from langgraph.prebuilt import ToolNode, tools_condition
def call_model(state: MessagesState):
return {"messages": [model_with_tools.invoke(state["messages"])]}
builder = StateGraph(MessagesState)
builder.add_node("call_model", call_model)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "call_model")
builder.add_conditional_edges("call_model", tools_condition) # -> "tools" or END
builder.add_edge("tools", "call_model") # tools back to the model
agent = builder.compile()To watch the loop run without an API key, a stand-in stands in for the model. It asks for the tool on the first turn, then answers from the tool result on the second. Build the graph one piece at a time.
The imports
Start with the imports: the graph pieces, the tools node and its router, the tool decorator, and the message types.
from langgraph.graph import StateGraph, START, MessagesState
from langgraph.prebuilt import ToolNode, tools_condition # tools node + ready-made router
from langchain.tools import tool
from langchain.messages import HumanMessage, AIMessage, ToolMessageThe tool
Define the tool the agent can call. It takes an order id and returns a short line about the order.
@tool
def lookup_order(order_id: str) -> str:
"""Look up an order by id."""
return f"Order {order_id}: shipped on 3 March."The stand-in model
Now the stand-in model. It reads the last message and decides the next move.
def call_model(state):
last = state["messages"][-1]
if isinstance(last, ToolMessage):
return {"messages": [AIMessage(last.content)]} # answer from the tool result
return {"messages": [AIMessage(content="", tool_calls=[ # first turn: ask for the tool
{"name": "lookup_order", "args": {"order_id": "A17"}, "id": "call_1"}])]}- If the last message is a
ToolMessage, the tool has run, so reply with its content. - Otherwise, return a tool call for
lookup_orderinstead of a final answer.
Wiring the loop
Wire the two nodes into a graph. The edges are what make it loop.
builder = StateGraph(MessagesState)
builder.add_node("call_model", call_model)
builder.add_node("tools", ToolNode([lookup_order]))
builder.add_edge(START, "call_model")
builder.add_conditional_edges("call_model", tools_condition) # tool call? -> tools, else END
builder.add_edge("tools", "call_model") # the loop: back to the model
agent = builder.compile()tools_conditionsends a tool call to thetoolsnode, and anything else to END.- The edge from
toolsback tocall_modelis the loop: run the tool, then let the model read the result.
Running the agent
Run one question through the agent and print the final message.
out = agent.invoke({"messages": [HumanMessage("Where is order A17?")]})
print(out["messages"][-1].content)The agent loop end to end
The same pieces in one file.
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, MessagesState
from langgraph.prebuilt import ToolNode, tools_condition
from langchain.tools import tool
from langchain.messages import HumanMessage, AIMessage, ToolMessage
@tool
def lookup_order(order_id: str) -> str:
"""Look up an order by id."""
return f"Order {order_id}: shipped on 3 March."
def call_model(state):
last = state["messages"][-1]
if isinstance(last, ToolMessage):
return {"messages": [AIMessage(last.content)]} # answer from the tool result
return {"messages": [AIMessage(content="", tool_calls=[ # first turn: ask for the tool
{"name": "lookup_order", "args": {"order_id": "A17"}, "id": "call_1"}])]}
builder = StateGraph(MessagesState)
builder.add_node("call_model", call_model)
builder.add_node("tools", ToolNode([lookup_order]))
builder.add_edge(START, "call_model")
builder.add_conditional_edges("call_model", tools_condition)
builder.add_edge("tools", "call_model")
agent = builder.compile()
out = agent.invoke({"messages": [HumanMessage("Where is order A17?")]})
print(out["messages"][-1].content)How the run reached its answer
The model asked for lookup_order, tools_condition sent the run to the tools node, ToolNode ran the tool and appended its result, the edge sent it back, and this time the model answered with plain text so the run reached END. Swapping the stand-in for model.bind_tools([lookup_order]) is the only change to make it real.
Pick one to watch it run, step by step.
How a run flows
- START → call_model: the model reads the conversation.
- call_model → tools (via
tools_condition) if it asked for a tool; the tool runs and returns a result. - tools → call_model: the model reads the result and answers, or asks for another tool.
- call_model → END when the model replies with no tool call.
The load-bearing edge
The edge tools → call_model is what makes this an agent rather than a one-shot call. Without it the model could ask for a tool but never see the answer. With it, the model can look something up, read the result, and use it in its reply.
Where the agent loop fits
- Any assistant that looks things up before answering.
- The base shape every tool-using agent is built on, by hand here and with
create_agentin the next lesson.
tools → call_model edge and the run ends after one tool call, so the model never uses the result. It is the single most common agent-loop bug.Related
- Previous: Structured output: answers in a fixed shape
- Next: create_agent: the short way
- Reference: Workflows and agents
- Draw the four transitions above on paper before running anything.
- Remove the
tools → call_modeledge and describe what breaks.
Every expert started right here.