Giving Agents Tools
How to equip CrewAI agents with built-in tools, custom tools using the @tool decorator, and structured Pydantic input schemas — with examples including database search and web search.
Why Agents Need Tools
Without tools, a CrewAI agent can only reason about what it already knows from its training data. Tools give agents the ability to act on the world: search the web, read files, query databases, call APIs, and more.
A tool is a function that:
- Accepts structured input
- Does something (fetches data, executes code, writes to a file)
- Returns a string result that the agent can read and reason about
Built-In Tools from crewai-tools
The crewai-tools package ships a library of ready-made tools:
pip install crewai-toolsSerperDevTool (Web Search)
from crewai_tools import SerperDevTool
import os
os.environ["SERPER_API_KEY"] = "your-serper-api-key"
search_tool = SerperDevTool()
# Attach to an agent
researcher = Agent(
role="Research Analyst",
goal="Find current, accurate information on any topic",
backstory="You are a methodical researcher who verifies facts from multiple sources.",
tools=[search_tool],
verbose=True,
)Get a free Serper API key at serper.dev. Each search costs a fraction of a cent.
FileReadTool
from crewai_tools import FileReadTool
file_tool = FileReadTool()
# Agent can now read files you specify in the task description
analyst = Agent(
role="Data Analyst",
goal="Extract insights from provided data files",
backstory="You analyze structured data files and produce concise summaries.",
tools=[file_tool],
verbose=True,
)WebsiteSearchTool
from crewai_tools import WebsiteSearchTool
# General web scraping
web_tool = WebsiteSearchTool()
# Or locked to a specific site
fda_tool = WebsiteSearchTool(website="https://www.fda.gov")Other Built-In Tools
from crewai_tools import (
CodeInterpreterTool, # Execute Python code
CSVSearchTool, # Search within CSV files
PDFSearchTool, # Search within PDF files
DirectorySearchTool, # Search across files in a directory
GithubSearchTool, # Search GitHub repositories
YoutubeVideoSearchTool, # Search YouTube video transcripts
)Assigning Tools Per Agent (Not Global)
A common mistake is giving all agents all tools. This is wasteful and confusing — the LLM has to decide from a long list every time it acts.
Assign only the tools each agent actually needs:
from crewai_tools import SerperDevTool, FileReadTool, PDFSearchTool
search_tool = SerperDevTool()
file_tool = FileReadTool()
pdf_tool = PDFSearchTool()
# Researcher needs web search and PDF reading
researcher = Agent(
role="Clinical Research Analyst",
goal="Find evidence in published literature",
backstory="You search PubMed and clinical databases for evidence.",
tools=[search_tool, pdf_tool], # Only what this agent needs
verbose=True,
)
# Writer needs file reading to access templates
writer = Agent(
role="Medical Writer",
goal="Write structured medical documents",
backstory="You write clear, compliant medical communications.",
tools=[file_tool], # Only file reading
verbose=True,
)
# Reviewer needs no tools — it reasons from context only
reviewer = Agent(
role="Regulatory Reviewer",
goal="Review documents for compliance",
backstory="You apply FDA guidelines to review medical communications.",
tools=[], # No tools needed
verbose=True,
)Building Custom Tools with @tool
The @tool decorator is the simplest way to create a custom tool. CrewAI converts the decorated function into a tool the agent can call.
Basic Custom Tool
from crewai.tools import tool
@tool("Drug Database Search")
def search_drug_database(query: str) -> str:
"""
Search the internal drug safety database for information about a specific drug.
Use this when you need data from the company's proprietary safety repository.
Input: a drug name or compound identifier.
"""
# In production this would query a real database
# For this example, we simulate a response
mock_db = {
"metformin": "Adverse events: GI upset (20%), lactic acidosis (rare). Contraindicated in eGFR < 30.",
"semaglutide": "Adverse events: nausea (44%), vomiting (24%), diarrhea (30%). Monitor for thyroid tumors.",
"tirzepatide": "Adverse events: nausea (18-45%), diarrhea (13-30%), injection site reactions (3-7%).",
}
drug_name = query.lower().strip()
result = mock_db.get(drug_name, f"No data found for '{query}' in the drug database.")
return resultThe docstring is critical — CrewAI uses it to tell the LLM when and how to call this tool.
Attach the Custom Tool to an Agent
safety_analyst = Agent(
role="Drug Safety Analyst",
goal="Assess drug safety using internal and external data sources",
backstory=(
"You have access to the company's proprietary safety database "
"and use it alongside public literature to assess safety signals."
),
tools=[search_drug_database, search_tool], # custom + built-in
verbose=True,
)Custom Tool with Pydantic Input Schema
For tools with multiple parameters or complex inputs, use a Pydantic schema for type safety and validation:
from crewai.tools import BaseTool
from pydantic import BaseModel, Field
from typing import Optional
class DrugQueryInput(BaseModel):
drug_name: str = Field(
description="The name of the drug to look up"
)
query_type: str = Field(
description="Type of query: 'adverse_events', 'interactions', 'contraindications', or 'all'"
)
severity_filter: Optional[str] = Field(
default=None,
description="Filter by severity: 'mild', 'moderate', 'severe'. Leave blank for all."
)
class DrugSafetyDatabaseTool(BaseTool):
name: str = "Drug Safety Database"
description: str = (
"Query the internal drug safety database. "
"Use this for adverse event data, drug interactions, and contraindications. "
"Always use this before the web search tool when looking for safety data."
)
args_schema: type[BaseModel] = DrugQueryInput
def _run(self, drug_name: str, query_type: str, severity_filter: Optional[str] = None) -> str:
# Simulate database query
data = self._query_database(drug_name, query_type, severity_filter)
return data
def _query_database(self, drug: str, qtype: str, severity: Optional[str]) -> str:
# In production: actual DB call here
results = {
"metformin": {
"adverse_events": [
{"event": "GI upset", "incidence": "20%", "severity": "mild"},
{"event": "Lactic acidosis", "incidence": "0.01%", "severity": "severe"},
{"event": "B12 deficiency", "incidence": "7%", "severity": "moderate"},
],
"contraindications": [
"eGFR below 30 mL/min/1.73m2",
"Acute metabolic acidosis",
"Radiological contrast procedures",
],
"interactions": [
"Alcohol: increases lactic acidosis risk",
"Iodinated contrast: hold 48h before/after",
],
}
}
drug_data = results.get(drug.lower(), {})
if not drug_data:
return f"No data found for {drug} in the safety database."
if qtype == "adverse_events" or qtype == "all":
events = drug_data.get("adverse_events", [])
if severity:
events = [e for e in events if e["severity"] == severity]
output_lines = [f"Adverse Events for {drug}:"]
for event in events:
output_lines.append(
f" - {event['event']}: {event['incidence']} incidence ({event['severity']})"
)
return "\n".join(output_lines)
return f"Query type '{qtype}' not recognized."
# Instantiate and attach
db_tool = DrugSafetyDatabaseTool()
analyst = Agent(
role="Drug Safety Analyst",
goal="Provide comprehensive drug safety assessments",
backstory=(
"You use the internal safety database as your primary source, "
"supplementing with web searches for external data."
),
tools=[db_tool, search_tool],
verbose=True,
)Full Example: Research Agent with Two Tools
from crewai import Agent, Task, Crew, Process, LLM
from crewai_tools import SerperDevTool
from crewai.tools import tool
# Custom tool
@tool("Internal Drug Registry")
def search_drug_registry(drug_name: str) -> str:
"""
Search the internal drug registry for approved compounds and their status.
Use this first when asked about any specific drug or compound.
Returns: approval status, indication, and key safety notes.
"""
registry = {
"semaglutide": {
"status": "Approved",
"brand": "Ozempic / Wegovy",
"indications": ["Type 2 diabetes (Ozempic)", "Obesity (Wegovy)"],
"safety_notes": "Monitor for thyroid C-cell tumors, pancreatitis",
},
"tirzepatide": {
"status": "Approved",
"brand": "Mounjaro / Zepbound",
"indications": ["Type 2 diabetes (Mounjaro)", "Obesity (Zepbound)"],
"safety_notes": "GI adverse events common; monitor renal function",
},
}
entry = registry.get(drug_name.lower())
if not entry:
return f"Drug '{drug_name}' not found in the internal registry."
return (
f"Drug: {drug_name.title()}\n"
f"Brand: {entry['brand']}\n"
f"Status: {entry['status']}\n"
f"Indications: {', '.join(entry['indications'])}\n"
f"Safety Notes: {entry['safety_notes']}"
)
# Built-in tool
web_search = SerperDevTool()
# Agent with both tools
drug_researcher = Agent(
role="Senior Drug Researcher",
goal=(
"Provide comprehensive, accurate information on drugs by combining "
"internal registry data with the latest published research."
),
backstory=(
"You are a senior research scientist with a PharmD. "
"You always check the internal registry first for authoritative data, "
"then supplement with a web search for the latest evidence. "
"You cite your sources and note any discrepancy between the registry "
"and published literature."
),
tools=[search_drug_registry, web_search],
llm=LLM(model="gpt-4o"),
verbose=True,
)
research_task = Task(
description=(
"Research semaglutide. Check the internal registry first, "
"then search for the latest Phase 4 safety data published in 2025 or 2026."
),
expected_output=(
"A research summary with: "
"1) Registry data (approval status, indications, key safety notes), "
"2) Latest published safety findings (with source citations), "
"3) Any discrepancies between registry data and recent literature."
),
agent=drug_researcher,
)
crew = Crew(
agents=[drug_researcher],
tasks=[research_task],
process=Process.sequential,
verbose=True,
)
result = crew.kickoff()
print(result.raw)Tool Best Practices
| Practice | Why | |----------|-----| | Write clear, specific docstrings | CrewAI uses the docstring to tell the LLM when to use each tool | | Name tools descriptively | "Internal Drug Registry" beats "Tool 1" | | Assign only relevant tools per agent | Fewer tools = less confusion and fewer hallucinated tool calls | | Return strings, not dicts or objects | Agents read tool output as text | | Handle errors gracefully | Return an error message string rather than raising exceptions | | Use Pydantic schemas for multi-parameter tools | Prevents the LLM from misformatting arguments |
Summary
- CrewAI ships a tool library via
crewai-tools— web search, file reading, PDF search, and more - Custom tools use
@toolfor simple functions orBaseToolfor Pydantic-typed inputs - Assign tools per agent, not globally — keep each agent's toolset minimal and focused
- The tool docstring is the agent's instruction for when and how to use the tool
- Return strings from tools — agents parse text, not Python objects
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.