skip to content

Tools

You will learn how CrewAI agents reach the outside world: the crewai-tools catalogue, the @tool decorator, and BaseTool subclasses with typed argument schemas. Interviewers care about result caching, failure handling, and which agent is allowed to hold which tool.

part ofAI agent & RAG frameworksoverview, primer and where to startread it →
on this pageshow

questions

6

In CrewAI, when should a tool subclass BaseTool with args_schema instead of using @tool?

level: middleimportance: must knowfreq 68%

answer

  1. two authoring paths, different ceilings
  2. docstring versus explicit Pydantic model
  3. the underscore-prefixed execution method
  4. constructor state, cache_function, result_as_answer

basics

~20 s

CrewAI's @tool decorator wraps a plain function and derives the argument schema from its signature and the description from its docstring. Subclass BaseTool when you need an explicit Pydantic args_schema, constructor-held state such as clients, or per-tool fields like cache_function.

solid answer

~50 s

CrewAI gives you two authoring paths for a custom tool. The `@tool("Name")` decorator from `crewai.tools` turns a function into a tool: the type hints become the argument schema and the docstring becomes the description the LLM reads, so it is the right choice for a small stateless helper. Subclassing `BaseTool` costs more lines but buys control: you declare `name` and `description` as fields, point `args_schema` at a Pydantic model whose `Field(description=...)` text is what the model actually sees per argument, and implement `_run(self, **kwargs)`. Because it is a class, it can hold an HTTP client, credentials or a connection built in the constructor instead of being rebuilt per call, and it can set fields like `cache_function` or `result_as_answer`. In both cases CrewAI validates the model's arguments against the schema before `_run` executes, so a good schema is a validation layer, not just documentation.

code

python · 21 lines
python
from typing import Type

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


class TicketArgs(BaseModel):
    ticket_id: str = Field(..., description="Jira key, e.g. OPS-1421")
    include_comments: bool = Field(False, description="Attach the comment thread")


class TicketReaderTool(BaseTool):
    name: str = "Read Jira ticket"
    description: str = "Fetch one Jira ticket's summary, status and optional comments."
    args_schema: Type[BaseModel] = TicketArgs

    def _run(self, ticket_id: str, include_comments: bool = False) -> str:
        ticket = {"id": ticket_id, "status": "In Progress"}
        if include_comments:
            ticket["comments"] = []
        return str(ticket)

go deeper

for a junior

Be able to write a working tool with the @tool decorator: a typed function plus a clear docstring, passed into an agent's tools list. Know that the docstring is what the model reads.

for a middle

Explain both paths and the split: inferred schema and docstring versus explicit args_schema, name, description and _run. Mention that arguments are validated against the schema before your code runs.

for a senior

Show judgment about the contract — per-argument Field descriptions as the fix for malformed tool calls, constructor-held clients instead of per-call setup, and when result_as_answer protects an exact deliverable from LLM paraphrasing.

for a principal

Own the tool layer as an internal platform: a shared base class carrying auth, timeouts, error shaping and telemetry, schemas versioned and tested independently of prompts, and a review bar for descriptions because they are prompt surface that every agent pays context for.

## Two ways to author a CrewAI tool A CrewAI tool is an object with a name, a natural-language description, an argument schema, and an implementation. Everything an agent's LLM knows about the tool comes from the first three; the fourth is your code. CrewAI ships two ways to produce that object. ### The @tool decorator ``` from crewai.tools import tool ``` Decorating a function with `@tool("Read Jira ticket")` produces a tool whose name is the string you pass, whose description is the function's docstring, and whose argument schema is inferred from the parameter names and type hints. This is the fastest path and it is idiomatic for stateless helpers: a formatter, a lookup against a module-level client, a pure computation. The tradeoffs are that the docstring is doing double duty as human documentation and as LLM-facing prompt text, and that you have no per-argument descriptions unless you spell them out in prose inside the docstring. ### Subclassing BaseTool ``` from crewai.tools import BaseTool ``` A `BaseTool` subclass is a Pydantic model. You declare `name: str` and `description: str` as class fields, set `args_schema: Type[BaseModel]` to a Pydantic model describing the inputs, and implement `_run(self, ...)` — note the leading underscore; `_run` is the method CrewAI invokes, not `run`. Each field in the args schema can carry `Field(..., description="...")`, and that per-argument text is what reaches the model. This matters in practice: most tool-call failures are the model guessing an argument format, and the schema description is where you say "ISO-8601 date" or "Jira key such as OPS-1421". ## What subclassing buys you - **Explicit, reviewable schema.** The tool contract lives in one Pydantic model you can test, reuse across tools, and validate independently of the LLM. - **Constructor state.** Because it is a class you instantiate (`TicketReaderTool(base_url=..., session=...)`), expensive clients are built once and shared across every call the crew makes, rather than reconstructed inside a function body. - **Per-tool behaviour fields.** `cache_function` decides whether a given result is worth caching; `result_as_answer=True` makes the tool's raw output the agent's final answer for that step instead of letting the LLM paraphrase it — useful when the tool already produces the exact deliverable and any rewriting is a fidelity risk. - **Subclassable families.** If you have eight internal API tools sharing auth and error handling, a shared base class removes eight copies of that logic. ## Validation is part of the deal CrewAI validates the arguments the model produced against the schema before your `_run` body executes. A missing required field or a wrong type produces a validation error that is handed back to the agent as the tool's observation, so the model gets a chance to retry with corrected arguments. The practical consequence is that a tight schema converts a class of runtime exceptions in your code into a recoverable, model-visible correction loop. Loose schemas — a single `str` argument that you parse yourself — push that failure into your `_run` body where it is more likely to crash or, worse, silently do the wrong thing. ## Naming and description are prompt engineering Both paths feed the tool's name and description into the agent's prompt alongside every other tool it holds. An agent with twelve tools whose descriptions all begin "This tool is useful for..." will pick badly. Write descriptions that state what the tool returns and when *not* to use it ("returns the latest deploy record for one service; does not search across services"). This is the single highest-leverage edit on a misbehaving CrewAI agent, and it is free. ## The prebuilt catalogue Before writing either, check `crewai_tools`: `SerperDevTool` for web search (it reads `SERPER_API_KEY` from the environment), `ScrapeWebsiteTool` for fetching page text, `FileReadTool` and `DirectoryReadTool` for local files, `RagTool` for semantic question-answering over a configured data source, and `CodeInterpreterTool` for executing generated Python. The `*SearchTool` variants (PDF, CSV, website) are RAG tools specialised to one source type. Every one of these is a `BaseTool` subclass, so reading their source is the fastest way to learn the idioms. ## Rule of thumb Start with `@tool`. Promote to `BaseTool` the moment you need a real argument contract, shared state, or per-tool caching and answer behaviour — those are exactly the things the decorator cannot express.

  • What does setting result_as_answer=True on a CrewAI tool change about the agent's step?
    The tool's raw output becomes the agent's final answer for that task step instead of being passed back to the LLM for a summarising turn. You use it when the tool already produces the exact deliverable — a rendered report, a signed URL, a formatted table — and any LLM rewriting would risk distorting it. It also saves one model round trip.
  • Before writing a custom tool, what does the crewai_tools catalogue already give you?
    Search and scraping (`SerperDevTool`, which reads `SERPER_API_KEY`, and `ScrapeWebsiteTool`), file access (`FileReadTool`, `DirectoryReadTool`), semantic Q&A over a configured source (`RagTool`, plus source-specific `*SearchTool` variants), and sandboxed execution (`CodeInterpreterTool`). All are `BaseTool` subclasses, so you can subclass or wrap them rather than reimplementing auth, retries and output shaping.
  • Where do per-argument descriptions come from in each authoring style?
    With `BaseTool`, from `Field(..., description="...")` on each field of the `args_schema` model — that text reaches the LLM per argument. With `@tool`, the schema is inferred from type hints only, so any per-argument guidance has to be written into the function's docstring prose. That is the main reason a tool with fiddly inputs is better as a class.

saying these in an interview costs you the question

  • Says BaseTool subclasses must implement run() rather than _run()
  • Thinks the docstring is ignored and only the name matters
  • Believes args_schema is documentation only and is never validated
  • Claims @tool cannot produce a typed argument schema at all
  • Rebuilds an HTTP client inside every tool invocation

context

open as a page

In CrewAI, what happens when a Task defines tools and its Agent already has tools?

level: juniorimportance: should knowfreq 52%

basics

~20 s

Tools listed on a CrewAI Task take precedence for that task: the assigned agent works with the task's tool list rather than its own. Leave Task.tools unset to use the agent's tools; set it to narrow or replace the toolset for one step.

open as a page

How does CrewAI cache tool results, and what does cache_function control?

level: middleimportance: should knowfreq 44%

basics

~20 s

CrewAI caches tool results during a crew run, keyed by tool and arguments, so an identical repeat call returns the stored value instead of re-executing. Setting cache_function on a tool lets you decide per result whether it is stored; Crew(cache=False) disables caching entirely.

open as a page

In CrewAI, what happens to a crew run when a tool's _run raises an exception?

level: seniorimportance: should knowfreq 46%

basics

~20 s

CrewAI's tool-usage layer catches the exception and hands the error back to the agent as that tool call's observation, so the kickoff continues and the LLM can retry or change course. The cost is extra model turns, so bound it and return short, actionable error text yourself.

open as a page

How do you give a CrewAI agent tools from an MCP server, and what lifecycle must you manage?

level: seniorimportance: nice to knowfreq 33%

basics

~20 s

Use MCPServerAdapter from crewai_tools: construct it with the server's connection parameters, and it exposes the server's tools as CrewAI tools you pass into Agent(tools=...). It holds a live connection, so use it as a context manager or call stop() in a finally block.

open as a page

Which CrewAI agents should hold CodeInterpreterTool, and how do you contain what it runs?

level: principalimportance: nice to knowfreq 28%

basics

~20 s

Give CodeInterpreterTool to one narrowly scoped agent, never to agents that also ingest untrusted text. It executes generated Python inside a Docker container by default; unsafe_mode=True runs it in the host process, which is a development-only setting.

open as a page