Skip to main content
This example demonstrates how to create custom tools in Python using Composio’s decorator-based API with Pydantic models for type safety.

Overview

In this example, you’ll learn how to:
  • Create custom tools using the @composio.tools.custom_tool decorator
  • Define input schemas with Pydantic models
  • Create standalone tools and toolkit-integrated tools
  • Access authentication credentials in tools
  • Make authenticated API requests

Prerequisites

1

Install dependencies

pip install composio pydantic requests
2

Set up environment variables

Create a .env file with your API key:
COMPOSIO_API_KEY=your_composio_api_key

Complete Example

import requests
from pydantic import BaseModel, Field
from composio import Composio
from composio.types import ExecuteRequestFn

composio = Composio()

# Example 1: Simple standalone tool
class AddTwoNumbersInput(BaseModel):
    a: int = Field(
        ...,
        description="The first number to add",
    )
    b: int = Field(
        ...,
        description="The second number to add",
    )

@composio.tools.custom_tool
def add_two_numbers(request: AddTwoNumbersInput) -> int:
    """Add two numbers."""
    return request.a + request.b

# Example 2: Toolkit-integrated tool with API access
class GetIssueInfoInput(BaseModel):
    issue_number: int = Field(
        ...,
        description="The number of the issue to get information about",
    )

@composio.tools.custom_tool(toolkit="github")
def get_issue_info(
    request: GetIssueInfoInput,
    execute_request: ExecuteRequestFn,
    auth_credentials: dict,
) -> dict:
    """Get information about a GitHub issue."""
    response = execute_request(
        endpoint=f"/repos/composiohq/composio/issues/{request.issue_number}",
        method="GET",
        parameters=[
            {
                "name": "Accept",
                "value": "application/vnd.github.v3+json",
                "type": "header",
            },
            {
                "name": "Authorization",
                "value": f"Bearer {auth_credentials['access_token']}",
                "type": "header",
            },
        ],
    )
    return {"data": response.data}

# Example 3: Direct HTTP requests
@composio.tools.custom_tool(toolkit="github")
def get_issue_info_direct(
    request: GetIssueInfoInput,
    execute_request: ExecuteRequestFn,
    auth_credentials: dict,
) -> dict:
    """Get information about a GitHub issue."""
    response = requests.get(
        f"https://api.github.com/repos/composiohq/composio/issues/{request.issue_number}",
        headers={
            "Accept": "application/vnd.github.v3+json",
            "Authorization": f"Bearer {auth_credentials['access_token']}",
        },
    )
    return {"data": response.json()}

# Execute the custom tool
response = composio.tools.execute(
    user_id="default",
    slug=get_issue_info.slug,
    arguments={"issue_number": 1},
)

print(response)

How It Works

1

Define Input Schema

Create a Pydantic BaseModel class that defines the input parameters. Use Field to add descriptions and validation.
class AddTwoNumbersInput(BaseModel):
    a: int = Field(..., description="The first number to add")
    b: int = Field(..., description="The second number to add")
2

Decorate Tool Function

Use @composio.tools.custom_tool to register your function as a tool. Optionally specify a toolkit for authentication.
@composio.tools.custom_tool(toolkit="github")
def my_tool(request: MyInput) -> dict:
    # Implementation
    pass
3

Implement Tool Logic

Write the tool’s logic. For toolkit-integrated tools, you can access execute_request and auth_credentials.
4

Execute the Tool

Call composio.tools.execute() with the tool’s slug and arguments to run it.

Tool Function Signatures

@composio.tools.custom_tool
def simple_tool(request: InputModel) -> ReturnType:
    """Tool description."""
    # Implementation
    return result

Function Parameters

request
BaseModel
required
The validated input parameters matching your Pydantic model
execute_request
ExecuteRequestFn
Helper function to make authenticated HTTP requests to the toolkit’s API
execute_request(
    endpoint="/api/path",
    method="GET" | "POST" | "PUT" | "DELETE",
    parameters=[{
        "name": "param_name",
        "value": "param_value",
        "type": "header" | "query" | "body",
    }],
)
auth_credentials
dict
Authentication credentials for the toolkit
{
    "access_token": "...",
    "refresh_token": "...",
    # Other credentials depending on auth type
}

Pydantic Field Options

Use Pydantic’s Field for rich parameter definitions:
class MyToolInput(BaseModel):
    required_field: str = Field(
        ...,  # ... means required
        description="Description for AI",
    )
    
    optional_field: str = Field(
        default="default_value",
        description="Optional parameter",
    )
    
    validated_field: int = Field(
        ...,
        ge=1,  # Greater than or equal to 1
        le=100,  # Less than or equal to 100
        description="Number between 1 and 100",
    )
    
    email_field: str = Field(
        ...,
        pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$',
        description="Valid email address",
    )

Expected Output

{
    "successful": true,
    "data": {
        "number": 1,
        "title": "Example Issue",
        "state": "open",
        "user": {
            "login": "composiohq",
            "id": 12345
        },
        "body": "Issue description..."
    },
    "error": null
}

Advanced Examples

Handle nested data with Pydantic models:
class Address(BaseModel):
    street: str
    city: str
    country: str

class CreateUserInput(BaseModel):
    name: str = Field(..., description="User's full name")
    email: str = Field(..., description="User's email")
    address: Address = Field(..., description="User's address")

@composio.tools.custom_tool
def create_user(request: CreateUserInput) -> dict:
    """Create a new user with address."""
    return {
        "user_id": "12345",
        "name": request.name,
        "address": request.address.dict(),
    }
Accept lists of items:
from typing import List

class BatchProcessInput(BaseModel):
    items: List[str] = Field(
        ...,
        description="List of items to process",
    )
    operation: str = Field(
        ...,
        description="Operation to perform",
    )

@composio.tools.custom_tool
def batch_process(request: BatchProcessInput) -> dict:
    """Process multiple items."""
    results = [f"{request.operation}: {item}" for item in request.items]
    return {"results": results}
Use enums for restricted choices:
from enum import Enum

class Priority(str, Enum):
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"

class CreateTaskInput(BaseModel):
    title: str = Field(..., description="Task title")
    priority: Priority = Field(
        default=Priority.MEDIUM,
        description="Task priority level",
    )

@composio.tools.custom_tool
def create_task(request: CreateTaskInput) -> dict:
    """Create a task with priority."""
    return {
        "task": request.title,
        "priority": request.priority.value,
    }
Handle errors gracefully:
@composio.tools.custom_tool(toolkit="github")
def safe_get_issue(
    request: GetIssueInfoInput,
    execute_request: ExecuteRequestFn,
    auth_credentials: dict,
) -> dict:
    """Get GitHub issue with error handling."""
    try:
        response = execute_request(
            endpoint=f"/repos/composiohq/composio/issues/{request.issue_number}",
            method="GET",
            parameters=[
                {
                    "name": "Authorization",
                    "value": f"Bearer {auth_credentials['access_token']}",
                    "type": "header",
                },
            ],
        )
        return {"success": True, "data": response.data}
    except Exception as e:
        return {"success": False, "error": str(e)}

Tool Attributes

After decoration, your function has additional attributes:
@composio.tools.custom_tool
def my_tool(request: MyInput) -> dict:
    """My custom tool."""
    pass

print(my_tool.slug)  # Auto-generated slug
print(my_tool.name)  # Tool name
print(my_tool.description)  # From docstring

Best Practices

Descriptive Docstrings: Use clear docstrings - they become the tool description for AI models
Field Descriptions: Always add descriptions to Pydantic fields for better AI understanding
Type Safety: Use Pydantic’s validation features to ensure type safety
Error Handling: Return structured error responses instead of raising exceptions
Toolkit Integration: Use toolkit parameter when you need authentication for external APIs

Next Steps

OpenAI Example

Use custom tools with OpenAI Agents

CrewAI Example

Use custom tools in CrewAI crews