n4nAI

CrewAI BaseTool class explained with a working example

Learn the CrewAI BaseTool class with a working example. Understand its structure, why it matters for agents, and avoid common custom tool mistakes.

n4n Team4 min read958 words

Audio narration

Coming soon — every post will get a voice note here.

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:

  • name must be unique within the agent’s tool set. Duplicate names cause silent shadowing.
  • description is not documentation for humans; it is read by the LLM to decide tool usage. Write it as an imperative capability statement.
  • args_schema is 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 – When True, 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}&current=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 _run will bubble up and can halt the entire crew unless you configure fault tolerance.
  • The args_schema forces 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:

  1. Input validation – Pydantic schemas reject malformed LLM outputs before they reach your network code.
  2. Observability – Because every invocation goes through a known class, you can add logging, metrics, or tracing in one place by overriding _run.
  3. Testability – A tool is just a Python class. You can unit test _run without booting an LLM.
  4. Resilience – Centralized error handling keeps a bad API from crashing the agent loop.
  5. 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.

Tagscrewaicustom-toolsbasetooltutorial

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All crewai custom tools & integrations posts →