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
Install dependencies
pip install composio pydantic requests
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
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" )
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
Implement Tool Logic
Write the tool’s logic. For toolkit-integrated tools, you can access execute_request and auth_credentials.
Execute the Tool
Call composio.tools.execute() with the tool’s slug and arguments to run it.
Standalone Tool
Toolkit-Integrated Tool
@composio.tools.custom_tool
def simple_tool ( request : InputModel) -> ReturnType:
"""Tool description."""
# Implementation
return result
Function Parameters
The validated input parameters matching your Pydantic model
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" ,
}],
)
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)}
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