Building a Custom Tool for a CrewAI Agent
A CrewAI tool is a named, described, schema-bound function an agent can call while working a task.
Custom tools wrap your APIs, databases, and internal services so specialists act in the real world instead of only writing prose.
Summary
Expose a clear name and description, define Pydantic input (and optional output) schemas, implement _run (sync or async), attach the tool only to agents that need it, and fail with readable errors the model can recover from.
Recipe
- Pick the smallest capability that unblocks the role (one endpoint, not the whole platform).
- Write a description that states when to use it and when not to.
- Define
args_schemawith Field descriptions the model will read. - Implement
_run(or use@toolon a function) with timeouts, auth from env, and structured errors. - Optionally return a Pydantic model for typed outputs; override
format_output_for_agentif the agent should see Markdown instead of JSON. - Attach the tool to the matching agent only.
- Run a verbose task that must call the tool; confirm arguments and caching behavior.
- Add cache rules only after you know which results are safe to reuse.
Working Example
from typing import Type
from pydantic import BaseModel, Field
from crewai import Agent, Task, Crew, Process
from crewai.tools import BaseTool
class InventoryInput(BaseModel):
sku: str = Field(..., description="Product SKU, for example SKU-123")
class InventoryResult(BaseModel):
sku: str
quantity: int
needs_reorder: bool
class InventoryCheckTool(BaseTool):
name: str = "inventory_check"
description: str = (
"Check on-hand quantity for a product SKU. "
"Use when the task needs stock levels. "
"Do not use for pricing or customer PII."
)
args_schema: Type[BaseModel] = InventoryInput
def _run(self, sku: str) -> InventoryResult:
# Replace with a real API call + timeout
stock = {"SKU-123": 14, "SKU-456": 0}
if sku not in stock:
# Readable errors help the agent recover or ask for a better SKU
return InventoryResult(sku=sku, quantity=0, needs_reorder=True)
qty = stock[sku]
return InventoryResult(sku=sku, quantity=qty, needs_reorder=qty < 5)
inventory_tool = InventoryCheckTool()
ops = Agent(
role="Inventory Ops Analyst",
goal="Report stock status accurately using inventory tools",
backstory="You never invent quantities. You only report tool results.",
tools=[inventory_tool],
verbose=True,
)
task = Task(
description="Check stock for {sku} and say whether to reorder.",
expected_output="One short paragraph with sku, quantity, and reorder recommendation.",
agent=ops,
)
crew = Crew(agents=[ops], tasks=[task], process=Process.sequential, verbose=True)
print(crew.kickoff(inputs={"sku": "SKU-123"}).raw)Decorator form for quick functions:
from crewai.tools import tool
@tool("multiply")
def multiply(first_number: int, second_number: int) -> int:
"""Multiply two integers. Use only for arithmetic checks."""
return first_number * second_numberDeep Dive
What the agent actually sees
The model chooses tools from name + description + argument schema. If the description is vague ("does stuff with data"), the agent will call it randomly or never.
Good descriptions include:
- Primary use case
- Required inputs
- Side effects (writes, emails, charges)
- Hard non-goals
BaseTool vs @tool
| Style | Use when |
|---|---|
Subclass BaseTool | Production tools, typed I/O, shared clients, custom caching |
@tool decorator | Small pure helpers and prototypes |
Async _run | Non-blocking I/O; works with async crew kickoff paths |
Typed outputs
Returning a Pydantic model gives agents stable field names (quantity, needs_reorder).
Direct Python tool.run(...) still receives the real object.
Agents typically receive JSON unless you customize format_output_for_agent.
Caching
Tools support result caching to skip identical re-calls.
Override cache_function when only some results are safe to cache (for example pure math yes, live inventory no).
Security
- Read secrets from the environment inside
_run, never from user-controlled tool args. - Enforce least privilege: the tool's credentials should not be a god-mode admin key.
- Validate and canonicalize inputs (IDs, URLs, SQL) before execution.
- Prefer dedicated sandboxes (E2B, Modal, etc.) for code execution rather than deprecated in-process interpreters when running untrusted code (verify current guidance at build).
Built-in tools first
Before writing custom code, check crewai_tools (search, files, web, vector search, cloud, etc.).
Custom tools should cover your domain gaps.
Gotchas
- Name collisions or cute names. Prefer stable
snake_caseverbs the model can reselect. - Empty descriptions. The agent cannot infer intent from Python alone.
- Over-broad tools.
run_sql(query: str)invites injection and chaos; expose narrow operations. - Swallowing exceptions. Return structured error strings or raise clear errors; silent
Nonecauses loops. - Attaching every tool to every agent. Destroys role specialization and increases prompt bloat.
- Caching mutable data. Stale inventory or permissions bugs are worse than an extra API call.
- Blocking forever. Always set HTTP/client timeouts.
- Forgetting that tool output is untrusted content. Never let retrieved text redefine system policy.
Alternatives
| Approach | When it wins | When it loses |
|---|---|---|
Custom BaseTool | Internal APIs, domain actions | You only needed Serper/PDF search |
| MCP servers as tools | Shared tool mesh across products | Simple single-repo helpers |
| Knowledge sources | Read-only grounding docs | Need side-effecting actions |
Agent kickoff without tools | Pure reasoning tasks | Any external system access |
FAQs
Where do I install tool extras?
Often pip install 'crewai[tools]' for the official toolkit. Custom tools only need core crewai plus your client libraries.
Can tools be async?
Yes. Implement async _run or decorate an async function; the framework handles invocation.
How do I force the task result to be the tool output?
CrewAI supports patterns to force tool output as the result for specific tasks - use when the tool is the source of truth (verify current API at build).
Should the writer agent get the inventory tool?
Only if the writer must call it. Prefer ops/research roles for data tools and pass results via task context.
How do I unit test a tool?
Call tool.run(...) / _run directly with fixtures. Do not require a full crew for pure tool logic tests.
Can I use LangChain tools?
CrewAI documents integration paths for LangChain and LlamaIndex tools via wrappers - verify package versions at build.
What belongs in args_schema vs kickoff inputs?
Kickoff inputs parameterize the task. Tool args are chosen by the agent at runtime for each call.
How do tool hooks help?
Tool call hooks let you log, block, or mutate calls for policy and observability without editing every tool.
Why does the agent ignore my tool?
Usually a weak description, missing need in the task text, or competing tools. Mention the tool's purpose in the task when appropriate.
Are custom tools safe to load from JSON crew configs?
Only from trusted projects. Config references can execute local Python for custom tools - treat them like code deploy.
Related
Related: CrewAI Basics
Related: Defining Agent Roles, Goals, and Backstories
Related: The CrewAI Mental Model: Roles, Tasks, and Crews
Related: Debugging a Crew That Won't Converge on an Answer
Related: CrewAI Best Practices
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.