CrewAICrewAI 1.15 · Python 3.10 to 3.13
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
35 small wins to finish your pathNext lesson →

Tool calls: asking for a tool

A tool call is a request the model returns instead of text; the agent runs the tool, adds the result and calls the model again, until it answers in words.

Last updated: 28 Sep, 2026 · CrewAI 1.15

The tools lesson built the tool and gave it to no one. The model from the custom-LLM lesson could only say it had no way to look an order up. Given the tool, it needs to ask for it and read the result back.

Project files used on this pageThis lesson builds on a project from earlier lessons. The code below imports this file. Click a file to see its code, or follow the link to the lesson that wrote it. To run the code yourself, keep it in the same folder.
View the code here
tools.py
from crewai.tools import tool

ORDERS = {"A17": "shipped on 3 March", "C40": "waiting for stock"}


@tool
def lookup_order(order_id: str) -> str:
    """Look up an order's shipping status by its id, such as A17."""
    status = ORDERS.get(order_id)
    return f"{order_id} {status}." if status else f"{order_id} is not an order we have."

The clerk with the tool

python
from crewai import Agent, Crew, Task
from shop_llm import ShopLLM
from tools import lookup_order

clerk = Agent(
    role="Order clerk",
    goal="Find the status of customers' orders",
    backstory="You can look up any order in the shop's system.",
    llm=ShopLLM(model="shop"),
    tools=[lookup_order],
)
task = Task(description="Answer the customer: {question}",
            expected_output="The order's status in one sentence.", agent=clerk)
crew = Crew(agents=[clerk], tasks=[task])
Example
result = crew.kickoff(inputs={"question": "Where is my order A17?"})
print(result.raw)

The clerk has the tool, and still cannot look the order up. CrewAI asks a model whether it can make native tool calls, through a method called supports_function_calling. BaseLLM has no such method, so without it, CrewAI describes the tools in the prompt as text and expects the model to write its request in a fixed text format. The custom-LLM lesson's model does neither.

A model that asks for a tool

python
import json
import os
import re

from crewai import BaseLLM

os.environ["CREWAI_DISABLE_TELEMETRY"] = "true"
os.environ["CREWAI_TRACING_ENABLED"] = "false"
os.environ["CREWAI_DISABLE_VERSION_CHECK"] = "true"

One new import, json: a tool call carries its arguments as a JSON string, which decide builds with json.dumps. The three settings stay as they were.

python
class ShopLLM(BaseLLM):
    script: list = []

    def supports_function_calling(self):
        return True

    def call(self, messages, tools=None, **kwargs):
        if isinstance(messages, str):
            messages = [{"role": "user", "content": messages}]
        if self.script:
            return self.script.pop(0)
        names = [t["function"]["name"] for t in tools or []]
        return self.decide(messages, names)

supports_function_calling returning True makes CrewAI pass the tools to call as a list of schemas. call keeps only their names and hands the choice to a new method, decide. script is a list of fixed replies returned first, in order; from the max_iter lesson on it lets a lesson force a particular reply.

python
    def decide(self, messages, tools):
        last = messages[-1]
        if last["role"] == "tool":
            return last["content"]
        text = last["content"]
        orders = re.findall(r"\b[A-Z]\d+\b", text)
        wanted = "refund_order" if "refund" in text.lower() else "lookup_order"
        matches = [name for name in tools if name.endswith(wanted)]

A message with the role tool is a tool's result coming back, and the model answers with it. Otherwise it picks the tool it wants: refund_order when the text mentions a refund, which arrives in the tool-hooks lesson, and lookup_order otherwise. It matches names by their ending, because tools from an MCP server in the MCP lesson carry a prefix.

python
        if orders and matches:
            args = json.dumps({"order_id": orders[0]})
            return [{"id": f"call_{orders[0]}", "type": "function",
                     "function": {"name": matches[0], "arguments": args}}]
        if orders:
            return f"I have no way to look up {orders[0]} yet."
        return "Hello. Which order is this about?"

A tool call is a dictionary in the format OpenAI's API uses: an id, and the function's name with its arguments as a JSON string. Returning a list of them, instead of text, is how a model asks. Save the file; every lesson from here on imports it.

The tool-calling loop

Example
result = crew.kickoff(inputs={"question": "Where is my order A17?"})

for message in result.tasks_output[0].messages:
    text = message["content"].strip().split("\n")[0]
    print(f"{message['role']:<9}", message.get("tool_calls") or text)

The system and user messages are the agent-prompt lesson's. The first assistant message has no text, only a tool call; CrewAI saw it, ran lookup_order, and added the tool message with the result. The model was called again and the last assistant message is its answer. Text with no tool calls is what ended the loop.

The tool-call loop, one round
tool callrunsno tool callsThe task promptthe customer's questionShopLLM.decidetext or a tool calllookup_orderyour Python functionTool messagethe tool's answerFinal answertext, no tool calls
Hover or tap a piece to see what it is and which lesson built it.
Trace the loop

Pick one to watch it run, step by step.

The same five messages as a picture. Trace the second question through it to see the round the model skips when there is no order id.

Two more questions through the loop

Example
print(crew.kickoff(inputs={"question": "Where is my order B22?"}).raw)
print(crew.kickoff(inputs={"question": "Hello"}).raw)

B22 went through the same loop, and the tool's answer became the reply. "Hello" names no order, so the model answered on the first call and no tool ran. The model decides how many times the loop goes round.

Reading the message trail

  • supports_function_calling must return True, or CrewAI never sends the tool schemas and the tool is never called.
  • An assistant message with a tool call has no text; the tool message that follows carries the result.
  • Text with no tool call ends the loop; the model decides how many rounds it takes.

What call returns

Return valueMeans
a stringa final answer; the loop ends
a list of tool-call dictsrun these tools, then call me again

When the loop runs more than once

  • A lookup then an answer built from its result, as here.
  • Two tools in a row, where the second needs the first tool's output.
  • A retry, where a failed tool result sends the model round again.
Watch out
Without supports_function_calling returning True, CrewAI describes the tools in the prompt as text and waits for a fixed text format. A model that does not write that format never calls the tool, which is the trap the first run showed.
Try it yourself
  • Remove tools=[lookup_order] from the clerk and ask about A17 again.
  • Ask "Where are A17 and C40?" and read which order the model picks.
  • Print result.tasks_output[0].messages[2] and find the tool call's id.

This is what real progress feels like.