Why Tools Matter
An LLM alone can only reason over what's in its prompt. Tools let agents take action: search the web, read a file, query a database, hit an API.
Built-in Tools
Install crewai-tools and you get a dozen ready-to-use tools.
Common built-ins
from crewai_tools import (
SerperDevTool, # Google search via Serper API
ScrapeWebsiteTool, # fetch + parse HTML
FileReadTool, # read a local file
DirectoryReadTool, # list a folder
PDFSearchTool, # search a PDF semantically
CSVSearchTool, # search rows in a CSV
)
search = SerperDevTool()
scraper = ScrapeWebsiteTool()
file_reader = FileReadTool()
researcher = Agent(
role="Web Research Analyst",
goal="Find current info on {topic}",
backstory="...",
tools=[search, scraper],
)
API reference for the built-ins above: SerperDevTool, ScrapeWebsiteTool, FirecrawlScrapeWebsiteTool, FileReadTool, FileWriterTool, and PDFSearchTool.
Custom Tools with @tool
Anything you can write as a Python function, you can give to an agent. The docstring becomes the tool's description — write it carefully, the model uses it to decide when to call.
Custom tool
from crewai.tools import tool
import requests
@tool("Get current stock price")
def get_stock_price(ticker: str) -> str:
"""Fetch the latest stock price for a US ticker symbol.
Args:
ticker: Uppercase ticker symbol, e.g. AAPL, MSFT.
Returns:
A short string: "AAPL: $187.32 (+0.4%)"
"""
resp = requests.get(f"https://api.example.com/quote/{ticker}").json()
return f"{ticker}: ${resp[\'price\']} ({resp[\'change_pct\']}%)"
analyst = Agent(
role="Equity Analyst",
goal="Answer questions about US stocks",
backstory="You are precise and use real-time data.",
tools=[get_stock_price],
)
💡 Tip: The function's docstring is what the LLM sees. Spend time on it — it's a prompt.
Class-Based Tools (BaseTool)
For tools that need state (DB connection pool, auth token), subclass BaseTool. Implement _run() for synchronous paths and add _arun() when the tool wraps async HTTP or database drivers used from kickoff_async.
Stateful custom tool
from crewai.tools import BaseTool
from pydantic import BaseModel, Field
class QueryInput(BaseModel):
sql: str = Field(..., description="A read-only SELECT query")
class PostgresQueryTool(BaseTool):
name: str = "Run a SELECT query against the analytics DB"
description: str = "Execute read-only SQL. Only SELECT statements allowed."
args_schema: type[BaseModel] = QueryInput
def __init__(self, conn):
super().__init__()
self._conn = conn
def _run(self, sql: str) -> str:
if not sql.strip().lower().startswith("select"):
return "Error: only SELECT queries are allowed."
with self._conn.cursor() as cur:
cur.execute(sql)
return str(cur.fetchall()[:50])
Handling Tool Errors
Tools throw real exceptions. Wrap risky calls — a raised exception aborts the agent's turn, but a returned error string lets the agent retry or recover.
Defensive tool
@tool("Search internal wiki")
def wiki_search(query: str) -> str:
"""Search the internal Confluence wiki. Returns top 3 page titles."""
try:
results = wiki_client.search(query, limit=3)
if not results:
return "No matches found."
return "\n".join(f"- {r.title}: {r.url}" for r in results)
except TimeoutError:
return "Wiki is slow right now. Try again in 10 seconds."
except Exception as e:
return f"Wiki error: {e}. Try a different query."
Rules of Thumb
- Keep agents to 2–4 tools. Bigger toolsets confuse the model.
- Name and document for the LLM, not the human. The model picks tools by name + description.
- Return strings, not raw objects. The LLM only sees the string.
- Cap output size. A tool that returns 50KB blows the context window. Truncate to ~2KB.
- Async crews need
BaseTool._arun(). Implement the awaitable body when you usekickoff_asyncand httpx-style clients so you do not block the event loop.
CrewAI tools FAQ
What is the difference between CrewAI built-in tools and custom tools?
Built-in tools ship in crewai-tools for common integrations like search and files. Custom tools are functions or BaseTool classes you write when you need private APIs, stateful connections, or domain-specific guardrails.
Why does the @tool docstring matter in CrewAI?
The docstring is the tool description the model reads when deciding whether to call the tool. Clear args, constraints, and return format reduce hallucinated invocations and bad parameters.
When should I subclass BaseTool instead of using @tool?
Subclass BaseTool when you need persistent state such as connection pools, typed args_schema with Pydantic, or shared setup across many calls that would be awkward as a plain function closure.
How should CrewAI tools handle errors?
Return actionable error strings for recoverable failures so the agent can retry. Reserve raised exceptions for truly fatal cases where continuing would corrupt data or violate policy.
How many tools should a CrewAI agent carry at once?
Aim for roughly two to four focused tools per agent. Large tool menus increase mistaken calls, inflate prompts, and slow routing decisions.
Where can I read CrewAI tool API details after this tutorial?
Open the DevShelfHub CrewAI API reference for BaseTool, run_tool patterns, and related classes, then cross-link back to this tutorial while you wire production guardrails.