The CrewAI BaseTool class is the abstract base for defining custom tools that agents can invoke, encapsulating a name, description, input schema, and execution logic. A crewai basetool class example shows how to subclass it to wrap external APIs or computations with typed validation, giving the agent a reliable interface. It is the backbone of extensibility in CrewAI’s multi-agent orchestration.
What the BaseTool Class Actually Is
BaseTool in CrewAI (exposed via crewai_tools) is a typed contract between an LLM agent and a piece of executable code. It inherits the familiar shape of LangChain’s tool abstraction but trims it for CrewAI’s role-playing agent loop. At minimum, a subclass must declare:
name: a short unique identifier.description: natural language explaining when and how to use the tool.args_schema: a Pydantic model defining the inputs._run(or_arun): the synchronous (or asynchronous) execution method.
The agent never calls your Python code directly via reflection. It emits a tool-call JSON block, CrewAI validates that block against args_schema, instantiates the tool, and invokes _run with the parsed arguments. This validation step is what separates a fragile prompt hack from a production-grade integration. Without it, you are trusting the LLM to produce perfectly shaped arguments on every call—a losing bet at scale.
Anatomy of a Subclass
Here is the minimal skeleton every custom tool starts from:
from crewai_tools import BaseTool
from pydantic import BaseModel, Field
class GreetArgs(BaseModel):
name: str = Field(..., description="Name to greet")
class GreetTool(BaseTool):
name: str = "greet"
args_schema: type[BaseModel] = GreetArgs
def _run(self, name: str) -> str:
return f"Hello, {name}!"
Notes from shipping real agents:
namemust be unique within the agent’s tool set. Duplicate names cause silent shadowing.descriptionis not documentation for humans; it is read by the LLM to decide tool usage. Write it as an imperative capability statement.args_schemais mandatory if you want structured inputs. Without it, CrewAI falls back to inspecting_run’s signature, which breaks on complex types.
Optional Attributes
Two class attributes change runtime behavior:
return_direct: bool– WhenTrue, the tool’s output is returned straight to the user, skipping further agent reasoning. Use it for terminal actions like “send email”.cache_function– A callable that caches results keyed by arguments. Valuable for idempotent API calls where you don’t want to hammer a rate-limited endpoint.
A Working crewai basetool class example
Let’s build a tool that fetches current temperature from the free Open-Meteo API. This crewai basetool class example demonstrates real error handling, typed coordinates, and a string-only return contract.
import requests
from crewai_tools import BaseTool
from pydantic import BaseModel, Field
class WeatherArgs(BaseModel):
latitude: float = Field(..., description="Latitude of the location")
longitude: float = Field(..., description="Longitude of the location")
class WeatherLookupTool(BaseTool):
name: str = "weather_lookup"
args_schema: type[BaseModel] = WeatherArgs
def _run(self, latitude: float, longitude: float) -> str:
url = (
f"https://api.open-meteo.com/v1/forecast"
f"?latitude={latitude}&longitude={longitude}¤t=temperature_2m"
)
try:
resp = requests.get(url, timeout=5)
resp.raise_for_status()
data = resp.json()
temp = data["current"]["temperature_2m"]
return f"Current temperature: {temp}°C"
except requests.RequestException as e:
return f"WEATHER_ERROR: {e}"
Key details:
- The return value is a string. Agents ingest tool output as text; returning a dict forces an extra serialization step and risks mangled JSON in the prompt.
- Errors are caught and returned as prefixed strings. Raising an exception inside
_runwill bubble up and can halt the entire crew unless you configure fault tolerance. - The
args_schemaforces the LLM to supply two floats. If it emits a string, Pydantic rejects it before your code runs.
Wiring the Tool into an Agent
from crewai import Agent, Crew, Task
weather_tool = WeatherLookupTool()
agent = Agent(
role="Field Meteorologist",
goal="Report local weather conditions accurately",
backstory="A pragmatic scientist who trusts data",
tools=[weather_tool],
verbose=True,
)
task = Task(
description="What is the temperature at latitude 48.85, longitude 2.35?",
agent=agent,
)
crew = Crew(agents=[agent], tasks=[task])
result = crew.kickoff()
print(result)
This crewai basetool class example is end-to-end runnable assuming crewai and requests are installed. The agent will emit a tool call, CrewAI validates coordinates, and the tool hits the API.
Why BaseTool Matters for Production Agents
In a prototype, a @tool decorator on a function feels faster. In production, the BaseTool class earns its keep:
- Input validation – Pydantic schemas reject malformed LLM outputs before they reach your network code.
- Observability – Because every invocation goes through a known class, you can add logging, metrics, or tracing in one place by overriding
_run. - Testability – A tool is just a Python class. You can unit test
_runwithout booting an LLM. - Resilience – Centralized error handling keeps a bad API from crashing the agent loop.
- Composability – Tools can be shared across agents and crews without rewriting parsing logic.
If your tool internally calls an LLM—for example to summarize fetched data—targeting an OpenAI-compatible gateway such as n4n.ai that offers automatic fallback across providers keeps the tool resilient when a single provider is degraded. The tool’s _run simply points at the gateway’s /v1/chat/completions endpoint and benefits from provider-agnostic cache-control hints.
Common Misconceptions
“I can just use a plain function”
CrewAI supports the @tool decorator for quick jobs, but a plain function gives you no args_schema, no shared base for cross-cutting concerns, and no clean override point for async. The BaseTool class is the upgrade path.
“The description doesn’t matter”
The agent selects tools by matching the task against description. A vague description like "weather thing" yields missed calls or wrong-tool errors. Write descriptions as precise capability claims: "Fetch current temperature in Celsius for WGS84 coordinates."
“Args schema is optional”
You can omit args_schema and CrewAI will infer types from _run’s signature. That inference breaks on list[str], nested models, or Optional fields. For anything beyond scalars, declare the schema.
“Tools must be synchronous”
BaseTool defines _arun for coroutine execution. If your tool awaits HTTP or DB calls, implement _arun and let the agent run in async mode. Blocking I/O in _run will stall the event loop in async crews.
“Returning complex objects is fine”
Any non-string return is coerced to string anyway. Returning a Pydantic model or DataFrame adds latency and confusing text. Format the exact string the agent should read.
“Tools are stateless”
You can store state on self (e.g., a connection pool), but remember CrewAI may instantiate the tool once per agent. In async crews, multiple tasks may call the same instance concurrently. Use thread-safe structures or instantiate per task.
Testing Your Subclass
Treat tools like any other critical code:
def test_weather_lookup_handles_network_error(monkeypatch):
def fake_get(*args, **kwargs):
raise requests.RequestException("offline")
monkeypatch.setattr(requests, "get", fake_get)
tool = WeatherLookupTool()
out = tool._run(48.85, 2.35)
assert out.startswith("WEATHER_ERROR")
This test confirms the tool degrades gracefully—a property the agent loop depends on. Add a happy-path test with responses or respx to lock in parsing logic.
Putting It Together
A crewai basetool class example is not just sample code; it is the pattern that keeps multi-agent systems from collapsing under unstructured LLM output. Subclass BaseTool, declare a tight Pydantic schema, implement _run with explicit error boundaries, and write a description an agent can act on. Do that, and your custom integrations will survive contact with real prompts.