LangGraphLangGraph 1.2 · Python 3.10+
Dashboard
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: 29 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.

Installing langchain-tavily and getting a Tavily key · from the Complete Agentic AI Course In 10 Hours · 212:26 to 215:27

Web search with TavilySearch

The LangGraph section's first tool is a web search. It adds langchain_tavily to requirements.txt, installs it with uv add -r requirements.txt, signs in at tavily.com and pastes the key into the .env file, then restarts the notebook kernel so the key is loaded. It creates the tool with TavilySearch(max_results=2) and invokes it with "What is langgraph": the result holds a title, a URL and an extract from each of two pages.

Installing langchain-tavily

Add the package to the ones from Installation and setup. It is LangChain's Tavily integration and calls Tavily's API itself, so no other Tavily package is needed:

pip install "langchain-tavily==0.2.18"

Getting a free Tavily keyOptional

Sign up at app.tavily.com. The API key is on the dashboard and starts with tvly-. The free Researcher plan gives 1,000 API credits a month and needs no card; a basic search costs one credit and an advanced search two. Set the key the same way as the Groq key:

export TAVILY_API_KEY=tvly-...

Or add it to the .env file under the Groq key, as the video does; load_dotenv() loads both:

bash
GROQ_API_KEY=gsk_...
# optional, only the web search examples use it
TAVILY_API_KEY=tvly-...

The key is optional. The course's own examples, the shop's order lookup included, run with the Groq key alone. TavilySearch is the video's real-web tool, and its examples in this lesson, Agent loop, Checkpointer and Interrupts need the key. Without it, TavilySearch(max_results=2) stops with a ValidationError: "Did not find tavily_api_key, please add an environment variable TAVILY_API_KEY".

Creating the TavilySearch tool

python
from langchain_tavily import TavilySearch

tool=TavilySearch(max_results=2)
tool.invoke("What is langgraph")

In a script, wrap the call in print() to see it. In the video's notebook the call returns a dictionary: the query, then a results list with a title, URL, content extract and score for each of the two pages. The graph below shows the same kind of result as a tool message.

ToolNode and the two nodes of the tool graph · from the Complete Agentic AI Course In 10 Hours · 219:59 to 223:02

The crash course's tool graph

The LangGraph section of the video gives its chatbot two tools: TavilySearch(max_results=2), a web search, and multiply, a plain function with a docstring. llm.bind_tools(tools) lets the model ask for either. The graph has two nodes: tool_calling_llm, which invokes the model with the tools bound, and "tools", a ToolNode built from the same list.

tools_condition and the edges of the tool graph · from the Complete Agentic AI Course In 10 Hours · 223:02 to 227:00

add_conditional_edges("tool_calling_llm", tools_condition) sends the run to "tools" when the model's last message is a tool call and to END when it is not, and a plain edge takes "tools" to END.

tool_calling_llm goes to tools on a tool call or to __end__ without one, and tools goes to __end__.

The video's first question to this graph is "What is the recent ai news". It runs below on Groq with both tools, tool and multiply, as in the video; only the model string changes. Save it as its own file, such as tool_graph.py, with the load_dotenv() lines at the top if your keys are in .env; it needs GROQ_API_KEY, TAVILY_API_KEY and the langchain-tavily package from above:

ExampleAPI keyFrom the video, run on Groq
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph,START,END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition
from langchain.chat_models import init_chat_model
from langchain_tavily import TavilySearch

class State(TypedDict):
    messages:Annotated[list,add_messages]

llm=init_chat_model("groq:openai/gpt-oss-120b")

tool=TavilySearch(max_results=2)

## Custom function
def multiply(a:int,b:int)->int:
    """Multiply a and b

    Args:
        a (int): first int
        b (int): second int

    Returns:
        int: output int
    """
    return a*b

tools=[tool,multiply]
llm_with_tool=llm.bind_tools(tools)

## Node definition
def tool_calling_llm(state:State):
    return {"messages":[llm_with_tool.invoke(state["messages"])]}

## Grpah
builder=StateGraph(State)
builder.add_node("tool_calling_llm",tool_calling_llm)
builder.add_node("tools",ToolNode(tools))

## Add Edges
builder.add_edge(START, "tool_calling_llm")
builder.add_conditional_edges("tool_calling_llm",tools_condition)
builder.add_edge("tools",END)

## compile the graph
graph=builder.compile()

response=graph.invoke({"messages":"What is the recent ai news"})
for m in response['messages']:
    m.pretty_print()

The model called tavily_search with a query it wrote itself, "latest AI news September 2026", and chose an advanced search over the past month. ToolNode ran the search, and the tool message holds Tavily's JSON: two results, the number max_results allows, each with a URL, a title, an extract of the page and a score. The edge from "tools" goes to END, so the run stops there, and the model never turns the results into an answer. Search results change from day to day, so your run will find other pages.

The video then asks "What is 5 multiplied by 2". Asked it as it stands, gpt-oss-120b worked out 10 by itself and called no tool, so the question here ends with "Use the multiply tool." Run it on the same graph:

ExampleAPI keyFrom the video, run on Groq
response=graph.invoke({"messages":"What is 5 multiplied by 2? Use the multiply tool."})
for m in response['messages']:
    m.pretty_print()

The run stops at the tool's answer, 10. The model asked for multiply, ToolNode ran it and added a tool message, and the edge from "tools" goes to END, so the model never reads the result or writes a sentence. The video hits the same wall with "Give me the recent AI news and then multiply 5 by 10": the search runs and the multiplication never happens. Agent loop fixes it with one edge and runs that question.

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

The shop's ToolNode runs without a model: put it in a one-node graph and hand it a message that already contains a tool call, written by hand here, and it runs the tool. The graph above had a real model write it; writing it by hand lets you test ToolNode without a model or a key. Build it 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.
  • The tool's return value became the ToolMessage's content, which is the last message printed.

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?
  • Put a tool name in the call that no tool has, and read the ToolMessage that comes back.

Slow is fine. Stopping is the only problem.