Overview
Custom tools allow you to extend Composio’s functionality with your own implementations while keeping a consistent interface with built-in tools. You can create tools with custom logic, integrate with any API, and optionally leverage connected accounts for authentication.
Use Zod to define the input parameters for your tool:
import { Composio } from 'composio-core' ;
import { z } from 'zod' ;
const composio = new Composio ({
apiKey: process . env . COMPOSIO_API_KEY
});
// Define input parameters using Zod
const searchInputSchema = z . object ({
query: z . string (). describe ( 'The search query' ),
limit: z . number (). optional (). describe ( 'Maximum number of results to return' ),
category: z . enum ([ 'web' , 'images' , 'news' ]). optional (). describe ( 'Search category' )
});
from composio import Composio
from pydantic import BaseModel, Field
composio = Composio( api_key = os.environ[ "COMPOSIO_API_KEY" ])
# Define input parameters using Pydantic
class SearchInput ( BaseModel ):
query: str = Field( description = "The search query" )
limit: int = Field( default = 10 , description = "Maximum number of results" )
category: str = Field( default = "web" , description = "Search category" )
Step 2: Implement the execute function
Create the logic for your custom tool:
const customTool = await composio . tools . createCustomTool ({
name: 'Search Database' ,
slug: 'MY_SEARCH_TOOL' ,
description: 'Search through the internal database' ,
inputParams: searchInputSchema ,
execute : async ( input ) => {
// Your custom logic here
try {
const results = await searchDatabase ( input . query , input . limit );
return {
data: {
results ,
count: results . length
},
error: null ,
successful: true
};
} catch ( error ) {
return {
data: null ,
error: { message: error . message },
successful: false
};
}
}
});
Once created, execute it like any other tool:
const result = await composio . tools . execute ( 'MY_SEARCH_TOOL' , {
userId: 'default' ,
arguments: {
query: 'composio' ,
limit: 5 ,
category: 'web'
}
});
if ( result . successful ) {
console . log ( 'Search results:' , result . data . results );
}
Custom tools can leverage connected accounts from existing toolkits for authentication:
import { z } from 'zod' ;
const customGitHubTool = await composio . tools . createCustomTool ({
name: 'Get Repository Stats' ,
slug: 'GITHUB_GET_REPO_STATS' ,
description: 'Get detailed statistics for a GitHub repository' ,
toolkitSlug: 'github' , // Use GitHub connected accounts
inputParams: z . object ({
owner: z . string (). describe ( 'Repository owner' ),
repo: z . string (). describe ( 'Repository name' )
}),
execute : async ( input , connectionConfig , executeToolRequest ) => {
// connectionConfig contains the OAuth tokens from the connected account
console . log ( 'Access token:' , connectionConfig ?. access_token );
try {
// Option 1: Use executeToolRequest to make authenticated API calls
const response = await executeToolRequest ({
endpoint: `/repos/ ${ input . owner } / ${ input . repo } ` ,
method: 'GET' ,
parameters: []
});
// Transform the response
const stats = {
stars: response . data . stargazers_count ,
forks: response . data . forks_count ,
openIssues: response . data . open_issues_count ,
language: response . data . language
};
return {
data: stats ,
error: null ,
successful: true
};
} catch ( error ) {
return {
data: null ,
error: { message: error . message },
successful: false
};
}
}
});
Execute with Connected Account
// Execute with a specific connected account
const result = await composio . tools . execute ( 'GITHUB_GET_REPO_STATS' , {
userId: 'user_123' ,
connectedAccountId: 'conn_abc123' , // Optional: specify which account to use
arguments: {
owner: 'composio' ,
repo: 'composio'
}
});
// Or let Composio automatically use the first connected GitHub account
const result = await composio . tools . execute ( 'GITHUB_GET_REPO_STATS' , {
userId: 'user_123' ,
arguments: {
owner: 'composio' ,
repo: 'composio'
}
});
Making Authenticated API Calls
The executeToolRequest function allows you to make authenticated requests using the connected account:
execute : async ( input , connectionConfig , executeToolRequest ) => {
// Make a GET request
const response = await executeToolRequest ({
endpoint: '/api/v1/data' ,
method: 'GET' ,
parameters: [
{ name: 'page' , in: 'query' , value: '1' },
{ name: 'X-Custom-Header' , in: 'header' , value: 'custom-value' }
]
});
// Make a POST request with body
const createResponse = await executeToolRequest ({
endpoint: '/api/v1/items' ,
method: 'POST' ,
body: {
name: input . name ,
description: input . description
},
parameters: []
});
return {
data: createResponse . data ,
error: null ,
successful: true
};
}
The executeToolRequest function is only available for custom tools that specify a toolkitSlug. It will throw an error for standalone custom tools without a toolkit.
Create tools without requiring authentication:
const weatherTool = await composio . tools . createCustomTool ({
name: 'Get Weather' ,
slug: 'GET_WEATHER' ,
description: 'Get current weather for a location' ,
// No toolkitSlug means no authentication required
inputParams: z . object ({
city: z . string (). describe ( 'City name' ),
units: z . enum ([ 'metric' , 'imperial' ]). optional ()
}),
execute : async ( input ) => {
// Make direct API calls with your own credentials
const response = await fetch (
`https://api.weather.com/v1/weather?city= ${ input . city } &units= ${ input . units || 'metric' } ` ,
{
headers: {
'X-API-Key' : process . env . WEATHER_API_KEY
}
}
);
const data = await response . json ();
return {
data: {
temperature: data . temp ,
conditions: data . conditions ,
humidity: data . humidity
},
error: null ,
successful: true
};
}
});
Define complex nested schemas:
const complexTool = await composio . tools . createCustomTool ({
name: 'Process Order' ,
slug: 'PROCESS_ORDER' ,
description: 'Process a customer order' ,
inputParams: z . object ({
customer: z . object ({
id: z . string (),
email: z . string (). email (),
name: z . string ()
}),
items: z . array ( z . object ({
productId: z . string (),
quantity: z . number (). min ( 1 ),
price: z . number (). positive ()
})),
shipping: z . object ({
address: z . string (),
city: z . string (),
zipCode: z . string (),
country: z . string ()
}),
paymentMethod: z . enum ([ 'credit_card' , 'paypal' , 'bank_transfer' ]),
notes: z . string (). optional ()
}),
execute : async ( input ) => {
// Process the order
const orderId = await processOrder ( input );
return {
data: {
orderId ,
status: 'confirmed' ,
totalAmount: input . items . reduce (( sum , item ) =>
sum + ( item . price * item . quantity ), 0
)
},
error: null ,
successful: true
};
}
});
// Get a specific custom tool by slug
const tool = await composio . customTools . getCustomToolBySlug ( 'MY_SEARCH_TOOL' );
if ( tool ) {
console . log ( 'Tool name:' , tool . name );
console . log ( 'Description:' , tool . description );
}
// Get all custom tools
const allCustomTools = await composio . customTools . getCustomTools ({});
// Get specific custom tools by slugs
const specificTools = await composio . customTools . getCustomTools ({
toolSlugs: [ 'MY_SEARCH_TOOL' , 'GET_WEATHER' ]
});
Custom tools work seamlessly with AI provider integrations:
import { Composio } from 'composio-core' ;
import { OpenAI } from 'openai' ;
const composio = new Composio ({
apiKey: process . env . COMPOSIO_API_KEY
});
// Create custom tools
await composio . tools . createCustomTool ({
name: 'Get User Data' ,
slug: 'GET_USER_DATA' ,
description: 'Retrieve user data from the database' ,
inputParams: z . object ({
userId: z . string ()
}),
execute : async ( input ) => {
const user = await database . users . findById ( input . userId );
return { data: user , error: null , successful: true };
}
});
// Get tools including custom tools
const tools = await composio . tools . get ( 'user_123' , {
tools: [ 'GET_USER_DATA' , 'GITHUB_GET_REPOS' ]
});
// Use with OpenAI
const openai = new OpenAI ();
const response = await openai . chat . completions . create ({
model: 'gpt-4' ,
messages: [{ role: 'user' , content: 'Get data for user 12345' }],
tools: tools
});
Implement robust error handling:
const robustTool = await composio . tools . createCustomTool ({
name: 'Fetch External API' ,
slug: 'FETCH_EXTERNAL_API' ,
description: 'Fetch data from an external API' ,
inputParams: z . object ({
endpoint: z . string (). url ()
}),
execute : async ( input ) => {
try {
// Validate input
if ( ! input . endpoint . startsWith ( 'https://' )) {
return {
data: null ,
error: {
message: 'Only HTTPS endpoints are allowed' ,
code: 'INVALID_ENDPOINT'
},
successful: false
};
}
// Make API call with timeout
const controller = new AbortController ();
const timeout = setTimeout (() => controller . abort (), 5000 );
const response = await fetch ( input . endpoint , {
signal: controller . signal
});
clearTimeout ( timeout );
if ( ! response . ok ) {
return {
data: null ,
error: {
message: `HTTP error: ${ response . status } ${ response . statusText } ` ,
code: 'HTTP_ERROR' ,
statusCode: response . status
},
successful: false
};
}
const data = await response . json ();
return {
data ,
error: null ,
successful: true
};
} catch ( error ) {
if ( error . name === 'AbortError' ) {
return {
data: null ,
error: {
message: 'Request timeout after 5 seconds' ,
code: 'TIMEOUT'
},
successful: false
};
}
return {
data: null ,
error: {
message: error . message || 'Unknown error occurred' ,
code: 'EXECUTION_ERROR'
},
successful: false
};
}
}
});
Best Practices
Use Descriptive Slugs Choose clear, uppercase slugs with underscores (e.g., MY_CUSTOM_TOOL) for consistency.
Document Parameters Use Zod’s .describe() method to add clear descriptions to all input parameters.
Handle Errors Gracefully Always return structured error objects with meaningful messages and error codes.
Leverage Connected Accounts Use toolkitSlug and executeToolRequest for authenticated API calls when possible.
Common Patterns
Database Integration
const dbTool = await composio . tools . createCustomTool ({
name: 'Query Database' ,
slug: 'QUERY_DATABASE' ,
description: 'Execute a database query' ,
inputParams: z . object ({
table: z . string (),
filters: z . record ( z . unknown ()). optional ()
}),
execute : async ( input ) => {
const results = await db ( input . table )
. where ( input . filters || {})
. select ();
return {
data: { results , count: results . length },
error: null ,
successful: true
};
}
});
Webhook Integration
const webhookTool = await composio . tools . createCustomTool ({
name: 'Send Webhook' ,
slug: 'SEND_WEBHOOK' ,
description: 'Send data to a webhook URL' ,
inputParams: z . object ({
url: z . string (). url (),
payload: z . record ( z . unknown ())
}),
execute : async ( input ) => {
const response = await fetch ( input . url , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ( input . payload )
});
return {
data: {
statusCode: response . status ,
success: response . ok
},
error: null ,
successful: response . ok
};
}
});
File Processing
const fileProcessorTool = await composio . tools . createCustomTool ({
name: 'Process CSV File' ,
slug: 'PROCESS_CSV' ,
description: 'Parse and process CSV data' ,
inputParams: z . object ({
fileUrl: z . string (). url (),
delimiter: z . string (). default ( ',' )
}),
execute : async ( input ) => {
const response = await fetch ( input . fileUrl );
const csvText = await response . text ();
const rows = csvText . split ( ' \n ' ). map ( row =>
row . split ( input . delimiter )
);
return {
data: {
headers: rows [ 0 ],
rowCount: rows . length - 1 ,
preview: rows . slice ( 0 , 5 )
},
error: null ,
successful: true
};
}
});
Limitations
Custom tools are stored in-memory and not persisted across SDK instances
The executeToolRequest function only works with custom tools that have a toolkitSlug
Custom tools cannot use Composio’s automatic file upload/download features
Tool schemas are validated at creation time, not at runtime
Next Steps
Tool Execution Learn how to execute custom and built-in tools
Modifiers Apply modifiers to customize tool behavior
Authentication Flows Set up connected accounts for authenticated custom tools
Error Handling Handle errors in custom tool execution