All articles
January 18, 2026 6 min read

The LangGraph agent loop: tools_condition, ToolNode, and handling tool errors

How does LangGraph actually run the reason–act–observe loop? A conditional edge checks for tool calls, a ToolNode executes them, and a ToolMessage feeds the result back. Here's the wiring — and how errors self-correct.

Written forEngineering
LangGraphTool CallingAgents

The agent-loops post described the reason–act–observe cycle in the abstract. This is how LangGraph implements it concretely, with the two prebuilt pieces that do most of the work: tools_condition and ToolNode.

GoalLLM — reason + decideTool call?yesRun toolresultdoneAnswer
The concrete LangGraph wiring of the loop: the agent node emits tool calls, a conditional edge routes to the tool node, the result comes back, and it loops until there are none.

The three-part cycle

The loop is an agent node, a tools node, and a conditional edge between them:

  • The agent node calls the model, which returns an AIMessage — with tool_calls if it wants to act, without if it's done.
  • tools_condition is a prebuilt conditional edge: it inspects that AIMessage and routes to the tools node if there are tool_calls, or to END if there aren't.
  • ToolNode is a prebuilt node that takes the tool_calls, maps each name to the actual Python function, runs it locally, and appends the result as a ToolMessage — then an edge loops back to the agent node so the model sees the results.
The loop, wired
from langgraph.graph import StateGraph, END
from langgraph.prebuilt import ToolNode, tools_condition

g = StateGraph(State)
g.add_node('agent', call_model)
g.add_node('tools', ToolNode(tools))        # name -> function, run, return ToolMessage
g.set_entry_point('agent')
g.add_conditional_edges('agent', tools_condition)  # tool_calls? -> 'tools', else -> END
g.add_edge('tools', 'agent')                # loop back so the model sees the results
app = g.compile(checkpointer=saver)

The ToolMessage closes the loop

The result of running a tool goes back to the model as a ToolMessage, carrying the tool's output and the tool_call_id from the original request — that id is how the model knows which of its (possibly several) calls this result answers. On the next agent step the model reads those ToolMessages and either calls more tools or produces its final answer. The whole loop is just this: emit calls, run them, feed results back, repeat until there are none.

When a tool throws: self-correction

Tools fail — a bad argument, a network error, a not-found. The naive handling is to let the exception crash the graph, but there's a better move: return the error to the model as the tool's result, and let it self-correct. ToolNode can catch exceptions and hand the error text back as the ToolMessage, so the model sees 'that call failed because X' and tries a corrected call. Combined with validating arguments before execution — Pydantic rejects malformed args up front, and that rejection is itself a message the model can learn from — you get an agent that recovers from its own mistakes instead of falling over.

Errors become feedback, not crashes
# ToolNode can return exceptions to the model as the tool result:
ToolNode(tools, handle_tool_errors=True)
# the model sees the error text in a ToolMessage and issues a corrected call.
# and because @tool args are Pydantic-validated, bad arguments are caught
# *before* the function runs — the validation error also feeds back to the model.
LangGraph's agent loop is three moving parts: a condition that asks 'more tools?', a node that runs them, and a message that carries the result back — and the trick that makes it robust is treating a tool error as feedback, not a crash.
Building something with LLMs?
I help teams ship GenAI that’s reliable and cost-efficient.
Let’s talk