Defining Tools: Pydantic Schemas and the BaseTool Pattern

~15 min read

The book's CrewAI CurrencyConverterTool example shows the standard shape every custom tool follows: a Pydantic input schema defining exactly what arguments the tool needs, plus a BaseTool subclass that wires that schema to the actual logic.

Building a custom tool starts with a specific question: what exactly does this tool need as input, and what does it produce as output? This course's CrewAI CurrencyConverterTool example (built to fetch live exchange rates rather than making the LLM guess them) shows the standard two-part pattern for answering this cleanly: a Pydantic input schema, then a BaseTool subclass.

The Pydantic schema defines the input fields the tool expects — for a currency converter, that's an amount (a number), a source currency (a string, likely constrained to valid currency codes), and a target currency (the same). This isn't just documentation — Pydantic validates incoming arguments against these exact types before the tool's actual logic ever runs, catching malformed input (a non-numeric amount, an invalid currency code) before it can cause a confusing failure deeper in the tool's execution.

The CurrencyConverterTool itself is then defined by inheriting from BaseTool, CrewAI's base class for custom tools. This gives the tool a name and description (critical, since — as the next subtopic covers — the LLM decides whether and when to call a tool largely based on how well its description matches what the LLM is trying to accomplish), and ties it to the Pydantic input schema so the framework knows exactly what arguments to expect and validate.

This two-part pattern (typed input schema + a class wiring that schema to actual logic) isn't specific to CrewAI — the same shape shows up across virtually every agent framework and even MCP's own tool definitions (which similarly pair a JSON schema for arguments with the function that actually executes). Getting the schema right — specific types, clear required-vs-optional fields, and (as the description matters for tool selection) a genuinely descriptive tool description — is foundational to everything the other subtopics in this topic build on.

💻 Code example

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

class CurrencyConversionInput(BaseModel):
    """Pydantic schema — defines exactly what arguments this tool
    expects, and validates them before the tool's logic ever runs."""
    amount: float = Field(..., description="The amount to convert")
    source_currency: str = Field(..., description="3-letter source currency code, e.g. USD")
    target_currency: str = Field(..., description="3-letter target currency code, e.g. EUR")

class CurrencyConverterTool(BaseTool):
    name: str = "currency_converter"
    # The description is what the LLM actually reads to decide WHEN
    # to call this tool — vague descriptions lead to missed or
    # incorrect tool calls (covered in the next subtopic)
    description: str = (
        "Converts an amount from one currency to another using live "
        "exchange rates. Use this whenever the user asks about currency "
        "conversion or exchange rates — never guess exchange rates yourself."
    )
    args_schema: type[BaseModel] = CurrencyConversionInput

    def _run(self, amount: float, source_currency: str, target_currency: str) -> str:
        # Actual tool logic goes here — covered in the Tool Execution subtopic
        ...

💬 Deep Dive with AI

Key points

  • Defining a tool starts with a Pydantic schema specifying exactly what typed arguments it expects
  • Pydantic validates incoming arguments against these types before the tool's logic runs, catching malformed input early
  • A BaseTool subclass wires that schema to the actual logic, and carries the tool's name and description
  • The description matters enormously — it's what the LLM reads to decide when a tool is relevant, covered in the next subtopic
  • This typed-schema-plus-execution-class pattern is common across agent frameworks and even MCP's own tool definitions