This crewai custom tool tutorial first tool walks you through building a functional extension for a CrewAI agent from scratch. You will define an input schema, implement the logic, test it standalone, then wire it into a multi-agent crew that calls a live API for cryptocurrency prices.
Prerequisites
Before writing any code, set up an isolated environment and install the dependencies. You need Python 3.10 or newer because CrewAI and Pydantic v2 rely on modern type features.
python -m venv .venv
source .venv/bin/activate
pip install crewai requests python-dotenv
You also need an LLM endpoint. The default path is an OpenAI API key, but you can point CrewAI at the n4n.ai OpenAI-compatible endpoint to get automatic fallback across 240+ models when a provider is rate-limited. Either works; the tool code is identical.
Create a .env file:
OPENAI_API_KEY=sk-...
# or N4N_API_KEY=...
Finally, confirm you can reach the public CoinGecko price endpoint from your network—no API key is required for the simple price route we use.
How CrewAI invokes tools
CrewAI serializes your tool’s name, description, and args_schema into a JSON schema and sends it to the LLM as a function definition. When the model decides the tool is relevant, it returns a structured call with arguments matching your Pydantic model. The framework validates those arguments, executes _run, and feeds the returned string back as an observation. That loop repeats until the agent produces a final answer. Keeping descriptions precise is the single highest-leverage thing you can do for tool reliability.
Step 1: Define the input contract
Start by declaring exactly what the agent must pass. Use Pydantic’s Field to attach descriptions that the LLM will read.
from pydantic import BaseModel, Field
class CryptoPriceInput(BaseModel):
coin_id: str = Field(
..., description="CoinGecko coin id, e.g. 'bitcoin' or 'ethereum'"
)
currency: str = Field(
default="usd", description="Ticker to price against, e.g. 'usd', 'eur', 'btc'"
)
The ... marks coin_id as required. If the model omits it, validation fails before your code runs.
Step 2: Implement the tool
Subclass BaseTool from crewai_tools. Set the class attributes and implement _run. Always return a string—agents parse text, not objects.
from crewai_tools import BaseTool
import requests
class CryptoPriceTool(BaseTool):
name: str = "Crypto Price Fetcher"
"Returns the current market price of a cryptocurrency "
"from CoinGecko. Use when asked about crypto valuations."
)
args_schema: type[BaseModel] = CryptoPriceInput
def _run(self, coin_id: str, currency: str = "usd") -> str:
url = "https://api.coingecko.com/api/v3/simple/price"
params = {"ids": coin_id, "vs_currencies": currency}
try:
resp = requests.get(url, params=params, timeout=10)
resp.raise_for_status()
data = resp.json()
except requests.RequestException as e:
return f"Request failed: {e}"
if coin_id not in data:
return f"No price found for {coin_id}"
price = data[coin_id].get(currency)
if price is None:
return f"Currency {currency} not available for {coin_id}"
return f"1 {coin_id} = {price} {currency}"
Note the defensive try/except. An unhandled network error will bubble up and stall the entire crew; returning a descriptive string lets the agent recover or report gracefully.
Step 3: Test the tool in isolation
Never wire an untested tool into an agent. Run it directly:
if __name__ == "__main__":
tool = CryptoPriceTool()
print(tool.run(coin_id="ethereum"))
print(tool.run(coin_id="bitcoin", currency="eur"))
Expected output (values fluctuate):
1 ethereum = 3120.45 usd
1 bitcoin = 58200.10 eur
If you get a Request failed message, check outbound HTTPS or CoinGecko’s public rate limit (roughly 10–30 calls/minute per IP).
Step 4: Configure the LLM and agent
Instantiate the LLM. CrewAI’s LLM class accepts base_url, so swapping providers is a one-line change.
from crewai import Agent, Crew, Task, Process, LLM
from dotenv import load_dotenv
import os
load_dotenv()
llm = LLM(
model="gpt-4o-mini",
temperature=0,
api_key=os.getenv("OPENAI_API_KEY"),
# To use n4n.ai instead, uncomment:
# base_url="https://api.n4n.ai/v1",
# api_key=os.getenv("N4N_API_KEY"),
)
Now create an agent that owns the tool:
analyst = Agent(
role="Cryptocurrency Analyst",
goal="Answer user questions about crypto prices accurately",
backstory="You track token markets and verify numbers with tools.",
tools=[CryptoPriceTool()],
llm=llm,
verbose=True,
)
verbose=True prints the thought/action/observation cycle, which is essential for debugging.
Step 5: Run a task through the crew
Define a task and execute the crew sequentially:
task = Task(
description="What is the current price of solana in usd?",
expected_output="The current price of solana in USD, stating the tool used.",
agent=analyst,
)
crew = Crew(
agents=[analyst],
tasks=[task],
process=Process.sequential,
)
result = crew.kickoff()
print("FINAL:", result)
Sample verbose log excerpt:
Agent: Cryptocurrency Analyst
Thought: I need the live price of solana.
Action: Crypto Price Fetcher
Action Input: {"coin_id": "solana", "currency": "usd"}
Observation: 1 solana = 145.22 usd
Final Answer: The current price of solana is 145.22 usd, fetched via Crypto Price Fetcher.
FINAL: The current price of solana is 145.22 usd, fetched via Crypto Price Fetcher.
That output confirms this crewai custom tool tutorial first tool is correctly discovered and invoked by the model.
Alternative: the @tool decorator
For stateless helpers, the decorator approach cuts boilerplate. It still satisfies the crewai custom tool tutorial first tool requirement of a minimal, runnable integration.
from crewai_tools import tool
@tool("Add Two Numbers")
def add_numbers(a: int, b: int) -> str:
"""Adds two integers and returns the sum as a string."""
return str(a + b)
CrewAI infers the schema from type hints and the docstring. Use the class form when you need state (e.g., a cached session) or fine-grained validation; use the decorator for pure functions.
Common pitfalls when building your first tool
- Vague descriptions: “Fetches data” leads to misfires. Say what data, from where, and when to use it.
- Non-string returns: Returning a dict or None breaks the observation parser. Convert to text.
- Missing timeouts:
requests.getwithouttimeoutcan hang the agent loop indefinitely. - Schema mismatches: If the LLM sends
coin_idasid, Pydantic rejects it. Field names must match what the model sees in the schema. - Secret leakage: Never hardcode API keys; load them from environment as shown.
Production hardening
If you deploy this, add response caching to avoid hitting CoinGecko limits:
from functools import lru_cache
class CachedCryptoPriceTool(CryptoPriceTool):
@lru_cache(maxsize=128)
def _cached_price(self, coin_id: str, currency: str) -> str:
return self._run(coin_id, currency)
And if you route LLM traffic through n4n.ai, its gateway honors provider cache-control hints and automatically fails over when a backend is degraded, which keeps the agent loop running during provider incidents.
Wrapping up
You have built, tested, and operated a custom CrewAI tool end to end. The pattern—Pydantic schema, _run implementation, isolated test, agent attachment—is the same for database queries, internal microservices, or file ingestion. Extend CryptoPriceTool with caching or swap in the @tool decorator, and you’ve completed the essential crewai custom tool tutorial first tool workflow with confidence.