Creating Custom Tools
Learn how to extend Composio's toolkits with your own tools
Custom tools allow you to create your own tools that can be used with Composio.
- Standalone tools - Simple tools that don't require any authentication
- Toolkit-based tools - Tools that require authentication and can use toolkit credentials
Creating a Custom Tool
Standalone Tool
A standalone tool is the simplest form of custom tool. It only requires input parameters and an execute function:
from pydantic import BaseModel, Field
from composio import Composio
from composio.types import ExecuteRequestFn
composio = Composio()
class AddTwoNumbersInput(BaseModel):
a: int = Field(
...,
description="The first number to add",
)
b: int = Field(
...,
description="The second number to add",
)
# function name will be used as slug
@composio.tools.custom_tool
def add_two_numbers(request: AddTwoNumbersInput) -> int:
"""Add two numbers."""
return request.a + request.bconst tool = await composio.tools.createCustomTool({
slug: 'CALCULATE_SQUARE',
name: 'Calculate Square',
description: 'Calculates the square of a number',
inputParams: z.object({
number: z.number().describe('The number to calculate the square of'),
}),
execute: async input => {
const { number } = input;
return {
data: { result: number * number },
error: null,
successful: true,
};
},
});Toolkit-based Tool
A toolkit-based tool has access to two ways of making authenticated requests:
1. Using executeToolRequest - The recommended way to make authenticated requests to the toolkit's API endpoints. Composio automatically handles credential injection and baseURL resolution:
class GetIssueInfoInput(BaseModel):
issue_number: int = Field(
...,
description="The number of the issue to get information about",
)
# function name will be used as slug
@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}const tool = await composio.tools.createCustomTool({
slug: 'GITHUB_STAR_COMPOSIOHQ_REPOSITORY',
name: 'Github star composio repositories',
toolkitSlug: 'github',
description: 'Star any specified repo of `composiohq` user',
inputParams: z.object({
repository: z.string().describe('The repository to star'),
page: z.number().optional().describe('Pagination page number'),
customHeader: z.string().optional().describe('Custom header'),
}),
execute: async (input, connectionConfig, executeToolRequest) => {
// This method makes authenticated requests to the relevant API
// You can use relative paths!
// Composio will automatically inject the baseURL
const result = await executeToolRequest({
endpoint: `/user/starred/composiohq/${input.repository}`,
method: 'PUT',
body: {},
// Add custom headers or query parameters
parameters: [
// Add query parameters
{
name: 'page',
value: input.page?.toString() || '1',
in: 'query',
},
// Add custom headers
{
name: 'x-custom-header',
value: input.customHeader || 'default-value',
in: 'header',
},
],
});
return result;
},
});2. Using connectionConfig - For making direct API calls when needed:
import 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()}const tool = await composio.tools.createCustomTool({
slug: 'GITHUB_DIRECT_API',
name: 'Direct GitHub API Call',
description: 'Makes direct calls to GitHub API',
toolkitSlug: 'github',
inputParams: z.object({
repo: z.string().describe('Repository name'),
}),
execute: async (input, connectionConfig, executeToolRequest) => {
// Use connectionConfig for direct API calls
const result = await fetch(`https://api.github.com/repos/${input.repo}`, {
headers: {
Authorization: `Bearer ${connectionConfig.access_token}`,
},
});
return {
data: await result.json(),
error: null,
successful: true,
};
},
});Using Custom Headers and Query Parameters
You can add custom headers and query parameters to your toolkit-based tools using the parameters option in executeToolRequest:
@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",
},
{
"name": 'X-Custom-Header',
"value": 'custom-value',
"type": 'header',
},
],
)
return {"data": response.data}const tool = await composio.tools.createCustomTool({
slug: 'GITHUB_SEARCH_REPOSITORIES',
name: 'Search GitHub Repositories',
description: 'Search for repositories with custom parameters',
toolkitSlug: 'github',
inputParams: z.object({
query: z.string().describe('Search query'),
perPage: z.number().optional().describe('Results per page'),
acceptType: z.string().optional().describe('Custom accept header'),
}),
execute: async (input, connectionConfig, executeToolRequest) => {
const result = await executeToolRequest({
endpoint: '/search/repositories',
method: 'GET',
parameters: [
// Add query parameters for pagination
{
name: 'q',
value: input.query,
in: 'query',
},
{
name: 'per_page',
value: (input.perPage || 30).toString(),
in: 'query',
},
// Add custom headers
{
name: 'Accept',
value: input.acceptType || 'application/vnd.github.v3+json',
in: 'header',
},
{
name: 'X-Custom-Header',
value: 'custom-value',
in: 'header',
},
],
});
return result;
},
});Executing Custom Tools
You can execute custom tools just like any other tool:
response = composio.tools.execute(
user_id="default",
slug="TOOL_SLUG", # For the tool above you can use `get_issue_info.slug`
arguments={"issue_number": 1},
)const result = await composio.tools.execute('TOOL_SLUG', {
arguments: {
// Tool input parameters
},
userId: 'user-id',
connectedAccountId: 'optional-account-id', // Required for toolkit-based tools
});Best Practices
- Use descriptive names and slugs for your tools
- Always provide descriptions for input parameters using
describe() - Handle errors gracefully in your execute function
- For toolkit-based tools:
- Prefer
executeToolRequestover direct API calls when possible - Use relative paths with
executeToolRequest- Composio will automatically inject the correct baseURL - Use the
parametersoption to add custom headers or query parameters:parameters: [ { name: 'page', value: '1', in: 'query' }, // Adds ?page=1 to URL { name: 'x-custom', value: 'value', in: 'header' }, // Adds header ]; - Remember that
executeToolRequestcan only call tools from the same toolkit - Use
executeToolRequestto leverage Composio's automatic credential handling - Only use
connectionConfigwhen you need to make direct API calls or interact with different services
- Prefer
- Chain multiple toolkit operations using
executeToolRequestfor better maintainability
Limitations
- Custom tools are stored in memory and are not persisted
- They need to be recreated when the application restarts
- Toolkit-based tools require a valid connected account with the specified toolkit
executeToolRequestcan only execute tools from the same toolkit that the custom tool belongs to- Each toolkit-based tool can only use one connected account at a time