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.
Search across all documentation pages
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.
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.
args_schema with Field descriptions the model will read._run (or use @tool on a function) with timeouts, auth from env, and structured errors.format_output_for_agent if the agent should see Markdown instead of JSON.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_numberThe 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:
@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 |
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.
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).
_run, never from user-controlled tool args.Before writing custom code, check crewai_tools (search, files, web, vector search, cloud, etc.).
Custom tools should cover your domain gaps.
snake_case verbs the model can reselect.run_sql(query: str) invites injection and chaos; expose narrow operations.None causes loops.| 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 |
Often pip install 'crewai[tools]' for the official toolkit. Custom tools only need core crewai plus your client libraries.
Yes. Implement async _run or decorate an async function; the framework handles invocation.
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).
Only if the writer must call it. Prefer ops/research roles for data tools and pass results via task context.
Call tool.run(...) / _run directly with fixtures. Do not require a full crew for pure tool logic tests.
CrewAI documents integration paths for LangChain and LlamaIndex tools via wrappers - verify package versions at build.
Kickoff inputs parameterize the task. Tool args are chosen by the agent at runtime for each call.
Tool call hooks let you log, block, or mutate calls for policy and observability without editing every tool.
Usually a weak description, missing need in the task text, or competing tools. Mention the tool's purpose in the task when appropriate.
Only from trusted projects. Config references can execute local Python for custom tools - treat them like code deploy.
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.
Reviewed by Chris St. John·Last updated Jul 16, 2026