Back to Crewai

Publish Custom Tools

docs/edge/en/guides/tools/publish-custom-tools.mdx

1.14.8a48.8 KB
Original Source

Overview

CrewAI's tool system is designed to be extended. If you've built a tool that could benefit others, you can package it as a standalone Python library, publish it to PyPI, and make it available to any CrewAI user — no PR to the CrewAI repo required.

This guide walks through the full process: implementing the tools contract, structuring your package, and publishing to PyPI.

<Note type="info" title="Not looking to publish?"> If you just need a custom tool for your own project, see the [Create Custom Tools](/en/learn/create-custom-tools) guide instead. </Note>

The Tools Contract

Every CrewAI tool must satisfy one of two interfaces:

Option 1: Subclass BaseTool

Subclass crewai.tools.BaseTool and implement the _run method. Define name, description, and optionally an args_schema for input validation.

python
from crewai.tools import BaseTool
from pydantic import BaseModel, Field


class GeolocateInput(BaseModel):
    """Input schema for GeolocateTool."""
    address: str = Field(..., description="The street address to geolocate.")


class GeolocateTool(BaseTool):
    name: str = "Geolocate"
    description: str = "Converts a street address into latitude/longitude coordinates."
    args_schema: type[BaseModel] = GeolocateInput

    def _run(self, address: str) -> str:
        # Your implementation here
        return f"40.7128, -74.0060"

Option 2: Use the @tool Decorator

For simpler tools, the @tool decorator turns a function into a CrewAI tool. The function must have a docstring (used as the tool description) and type annotations.

python
from crewai.tools import tool


@tool("Geolocate")
def geolocate(address: str) -> str:
    """Converts a street address into latitude/longitude coordinates."""
    return "40.7128, -74.0060"

Key Requirements

Regardless of which approach you use, your tool must:

  • Have a name — a short, descriptive identifier.
  • Have a description — tells the agent when and how to use the tool. This directly affects how well agents use your tool, so be clear and specific.
  • Implement _run (BaseTool) or provide a function body (@tool) — the synchronous execution logic.
  • Use type annotations on all parameters and return values.
  • Return a string result, or define an optional Pydantic output schema for structured results.

Optional: Async Support

If your tool performs I/O-bound work, implement _arun for async execution:

python
class GeolocateTool(BaseTool):
    name: str = "Geolocate"
    description: str = "Converts a street address into latitude/longitude coordinates."

    def _run(self, address: str) -> str:
        # Sync implementation
        ...

    async def _arun(self, address: str) -> str:
        # Async implementation
        ...

Optional: Input Validation with args_schema

Define a Pydantic model as your args_schema to get automatic input validation and clear error messages. If you don't provide one, CrewAI will infer it from your _run method's signature.

python
from pydantic import BaseModel, Field


class TranslateInput(BaseModel):
    """Input schema for TranslateTool."""
    text: str = Field(..., description="The text to translate.")
    target_language: str = Field(
        default="en",
        description="ISO 639-1 language code for the target language.",
    )

Explicit schemas are recommended for published tools — they produce better agent behavior and clearer documentation for your users.

Optional: Typed Outputs with result_schema

If your tool returns structured data, define a Pydantic output model. This is a good default for published tools because users and agents can rely on named fields.

Direct Python calls still receive the value your tool returns. When an agent uses the tool, CrewAI sends the agent JSON based on the output model.

CrewAI can infer the output schema from a Pydantic return annotation:

python
from crewai.tools import BaseTool
from pydantic import BaseModel, Field


class GeolocateResult(BaseModel):
    latitude: float = Field(..., description="Latitude in decimal degrees.")
    longitude: float = Field(..., description="Longitude in decimal degrees.")


class GeolocateTool(BaseTool):
    name: str = "Geolocate"
    description: str = "Converts a street address into latitude/longitude coordinates."

    def _run(self, address: str) -> GeolocateResult:
        if "1600 Pennsylvania" in address:
            return GeolocateResult(latitude=38.8977, longitude=-77.0365)
        return GeolocateResult(latitude=40.7128, longitude=-74.0060)

Set result_schema explicitly when your tool returns a dictionary:

python
class GeolocateTool(BaseTool):
    name: str = "Geolocate"
    description: str = "Converts a street address into latitude/longitude coordinates."
    result_schema: type[BaseModel] = GeolocateResult

    def _run(self, address: str) -> dict[str, float]:
        if "1600 Pennsylvania" in address:
            return {"latitude": 38.8977, "longitude": -77.0365}
        return {"latitude": 40.7128, "longitude": -74.0060}

If agents should receive a short text summary instead of JSON, override format_output_for_agent on your BaseTool subclass.

python
class GeolocateTool(BaseTool):
    name: str = "Geolocate"
    description: str = "Converts a street address into latitude/longitude coordinates."

    def _run(self, address: str) -> GeolocateResult:
        if "1600 Pennsylvania" in address:
            return GeolocateResult(latitude=38.8977, longitude=-77.0365)
        return GeolocateResult(latitude=40.7128, longitude=-74.0060)

    def format_output_for_agent(self, raw_result: object) -> str:
        result = GeolocateResult.model_validate(raw_result)
        return f"Latitude {result.latitude}, longitude {result.longitude}"

The override only changes what the agent sees. Direct users of your package still receive the normal value from tool.run(...).

Optional: Environment Variables

If your tool requires API keys or other configuration, declare them with env_vars so users know what to set:

python
from crewai.tools import BaseTool, EnvVar


class GeolocateTool(BaseTool):
    name: str = "Geolocate"
    description: str = "Converts a street address into latitude/longitude coordinates."
    env_vars: list[EnvVar] = [
        EnvVar(
            name="GEOCODING_API_KEY",
            description="API key for the geocoding service.",
            required=True,
        ),
    ]

    def _run(self, address: str) -> str:
        ...

Package Structure

Structure your project as a standard Python package. Here's a recommended layout:

crewai-geolocate/
├── pyproject.toml
├── LICENSE
├── README.md
└── src/
    └── crewai_geolocate/
        ├── __init__.py
        └── tools.py

pyproject.toml

toml
[project]
name = "crewai-geolocate"
version = "0.1.0"
description = "A CrewAI tool for geolocating street addresses."
requires-python = ">=3.10"
dependencies = [
    "crewai",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

Declare crewai as a dependency so users get a compatible version automatically.

__init__.py

Re-export your tool classes so users can import them directly:

python
from crewai_geolocate.tools import GeolocateTool

__all__ = ["GeolocateTool"]

Naming Conventions

  • Package name: Use the prefix crewai- (e.g., crewai-geolocate). This makes your tool discoverable when users search PyPI.
  • Module name: Use underscores (e.g., crewai_geolocate).
  • Tool class name: Use PascalCase ending in Tool (e.g., GeolocateTool).

Testing Your Tool

Before publishing, verify your tool works within a crew:

python
from crewai import Agent, Crew, Task
from crewai_geolocate import GeolocateTool

agent = Agent(
    role="Location Analyst",
    goal="Find coordinates for given addresses.",
    backstory="An expert in geospatial data.",
    tools=[GeolocateTool()],
)

task = Task(
    description="Find the coordinates of 1600 Pennsylvania Avenue, Washington, DC.",
    expected_output="The latitude and longitude of the address.",
    agent=agent,
)

crew = Crew(agents=[agent], tasks=[task])
result = crew.kickoff()
print(result)

Publishing to PyPI

Once your tool is tested and ready:

bash
# Build the package
uv build

# Publish to PyPI
uv publish

If this is your first time publishing, you'll need a PyPI account and an API token.

After Publishing

Users can install your tool with:

bash
pip install crewai-geolocate

Or with uv:

bash
uv add crewai-geolocate

Then use it in their crews:

python
from crewai_geolocate import GeolocateTool

agent = Agent(
    role="Location Analyst",
    tools=[GeolocateTool()],
    # ...
)