LangGraphLangGraph 1.2 · Python 3.10+
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
38 small wins to finish your pathNext lesson →

ToolNode and tools_condition

ToolNode runs the tools a model asked for. tools_condition is a ready-made router that goes to the tools node when the last message has tool calls, and to END otherwise.

Last updated: 27 Sep, 2026 · LangGraph 1.2

When a model asks for a tool, something has to run it and hand the result back. That is ToolNode. You can try it without a model by putting it in a tiny one-node graph and handing it a message that already contains a tool call.

Creating a ToolNode

python
from langgraph.prebuilt import ToolNode, tools_condition

tool_node = ToolNode([lookup_order])
builder.add_conditional_edges("call_model", tools_condition)  # to "tools" or END

You can run ToolNode without a model. Put it in a one-node graph, hand it a message that already contains a tool call, and it runs the tool. Build that up one piece at a time.

The imports

Start with the imports. Each line brings in one piece you need.

python
from langgraph.prebuilt import ToolNode   # the node that runs tools
from langchain.tools import tool          # turns a function into a tool
from langchain.messages import AIMessage  # the message that carries a tool call

The tool

Define one tool. It is a plain function with the @tool decorator, which lets ToolNode find it by name.

python
@tool
def lookup_order(order_id: str) -> str:
    """Look up an order by id."""        # the docstring tells the model what it does
    return f"Order {order_id}: shipped."

Wrapping it in ToolNode

Wrap the tool in a ToolNode. You pass a list, so the node can hold more than one tool and run whichever one is asked for.

python
tool_node = ToolNode([lookup_order])   # a node that can run any tool in this list

A hand-built tool call

Now build the message a model would send when it wants the tool. It has empty text, and its tool_calls list names the tool, the arguments, and an id. Building it by hand lets you test ToolNode without a model.

python
# an AIMessage that asks for the tool (as a model would return)
asked = AIMessage(content="", tool_calls=[
    {"name": "lookup_order", "args": {"order_id": "A17"}, "id": "call_1"}
])

Running ToolNode

Put the node in a one-node graph and run it. ToolNode reads the tool call, runs lookup_order with order id A17, and adds a ToolMessage holding the result. The last message is that result.

python
from langgraph.graph import StateGraph, START, END, MessagesState

b = StateGraph(MessagesState)
b.add_node("tools", tool_node)          # ToolNode as the one node
b.add_edge(START, "tools")
b.add_edge("tools", END)
graph = b.compile()

out = graph.invoke({"messages": [asked]})   # ToolNode runs lookup_order
print(out["messages"][-1].content)          # the tool's result

Running a tool call end to end

The same pieces in one file, ready to run.

Example
from langgraph.graph import StateGraph, START, END, MessagesState
from langgraph.prebuilt import ToolNode
from langchain.tools import tool
from langchain.messages import AIMessage

@tool
def lookup_order(order_id: str) -> str:
    """Look up an order by id."""
    return f"Order {order_id}: shipped."

tool_node = ToolNode([lookup_order])

# an AIMessage that asks for the tool (as a model would return)
asked = AIMessage(content="", tool_calls=[
    {"name": "lookup_order", "args": {"order_id": "A17"}, "id": "call_1"}
])

b = StateGraph(MessagesState)
b.add_node("tools", tool_node)
b.add_edge(START, "tools")
b.add_edge("tools", END)
graph = b.compile()

out = graph.invoke({"messages": [asked]})
print(out["messages"][-1].content)

What ToolNode did with the call

  • The hand-built AIMessage carries a tool_call for lookup_order, exactly what a model returns when it wants a tool.
  • ToolNode read the tool call, ran the tool, and appended a ToolMessage with the result.
  • tools_condition is the prebuilt check that decides whether to send the run to the tools node or to END, based on whether the last message has tool calls.

tools_condition in action

Call tools_condition on a state to see how it routes. When the last message has tool calls it returns the tools node's name, "tools"; when it does not, it returns "__end__".

Example
from langgraph.prebuilt import tools_condition
from langchain.messages import AIMessage

asked = AIMessage(content="", tool_calls=[
    {"name": "lookup_order", "args": {"order_id": "A17"}, "id": "call_1"}])  # has a tool call
plain = AIMessage("Your order shipped.")                                      # no tool call

print(tools_condition({"messages": [asked]}))   # -> route to the tools node
print(tools_condition({"messages": [plain]}))   # -> route to the end

Where ToolNode fits

  • The tools half of every agent: the model asks, ToolNode runs, the result goes back.
  • tools_condition saves you writing the same should-I-run-a-tool router by hand.
Watch out. tools_condition expects the tools node to be named "tools". Name it something else and you must write the router yourself.
Try it yourself
  • Add a second tool and a second tool call in the AIMessage. Are both results returned?
  • Give the tool an id that no tool matches and read the error.

Slow is fine. Stopping is the only problem.