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
BaseToolwithargs_schemaand_runfor 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.