Writing custom tools

CrewAI · Practical · 12 min

Most real agents need access to your own systems, such as databases, internal APIs or business rules.

There are two ways to write a tool.

The `@tool` decorator turns a function into a tool. The text in parentheses is the tool's name, the docstring becomes its description, and the type hints describe the arguments:

from crewai.tools import tool

@tool("Order status lookup")
def order_status(order_id: str) -> str:
    """Return the shipping status for an order ID like 'A-1001'."""

A `BaseTool` subclass gives more control. You set name and description, define args_schema as a Pydantic model (Pydantic is a library where you describe data as a class with typed fields, and it validates values), and put the logic in _run. Use it when the tool needs configuration, such as a database connection passed in the constructor.

Write tools for the model, not for programmers:

  • The description says when to use the tool and what the input looks like.
  • Return compact text. Huge outputs fill the context window and cost money.
  • Return a helpful error message instead of raising, so the agent can correct itself.

Useful options: @tool("...", result_as_answer=True) makes the tool's result the task's final answer without the agent rewriting it, and max_usage_count limits how many times a tool can be called.

In a JSONC project, put the tool in tools/order_status.py and reference it as "custom:order_status".

Example

from crewai.tools import BaseTool, tool
from pydantic import BaseModel, Field

ORDERS = {"A-1001": "shipped", "A-1002": "processing"}

@tool("Order status lookup")
def order_status(order_id: str) -> str:
    """Return the shipping status for an order ID like 'A-1001'."""
    return ORDERS.get(order_id, f"No order {order_id}. IDs look like A-1001.")

class RefundInput(BaseModel):
    order_id: str = Field(description="Order ID, e.g. A-1001")
    amount: float = Field(gt=0, description="Refund amount in USD")

class RefundTool(BaseTool):
    name: str = "Request refund"
    description: str = "Create a refund request for a shipped order."
    args_schema: type[BaseModel] = RefundInput

    def _run(self, order_id: str, amount: float) -> str:
        return f"Refund of ${amount:.2f} requested for {order_id}."

print(order_status.run(order_id="A-1001"))            # shipped
print(RefundTool().run(order_id="A-1001", amount=20))  # Refund of $20.00 requested for A-1001.

Remember

  • @tool("Name") + docstring + type hints = a quick tool
  • Subclass BaseTool with args_schema and _run for configurable tools
  • Return short text and friendly errors the model can act on

Watch out for

  • Raising exceptions for bad input instead of returning a message, so the agent cannot recover and the task fails.

Check yourself

  1. Where does the model learn when it should call a @tool function?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.crewai.com/en/learn/create-custom-tools