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.

  1. Standalone tools - Simple tools that don’t require any authentication
  2. 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:

1const tool = await composio.tools.createCustomTool({
2 slug: 'CALCULATE_SQUARE',
3 name: 'Calculate Square',
4 description: 'Calculates the square of a number',
5 inputParams: z.object({
6 number: z.number().describe('The number to calculate the square of'),
7 }),
8 execute: async input => {
9 const { number } = input;
10 return {
11 data: { result: number * number },
12 error: null,
13 successful: true,
14 };
15 },
16});

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:
1const tool = await composio.tools.createCustomTool({
2 slug: 'GITHUB_STAR_COMPOSIOHQ_REPOSITORY',
3 name: 'Github star composio repositories',
4 toolkitSlug: 'github',
5 description: 'Star any specificied repo of `composiohq` user',
6 inputParams: z.object({
7 repository: z.string().describe('The repository to star'),
8 page: z.number().optional().describe('Pagination page number'),
9 customHeader: z.string().optional().describe('Custom header'),
10 }),
11 execute: async (input, connectionConfig, executeToolRequest) => {
12 // This method makes authenticated requests to relevant API
13 // You can use relative paths!
14 // Composio will automatically inject the baseURL
15 const result = await executeToolRequest({
16 endpoint: `/user/starred/composiohq/${input.repository}`,
17 method: 'PUT',
18 body: {},
19 // Add custom headers or query parameters
20 parameters: [
21 // Add query parameters
22 {
23 name: 'page',
24 value: input.page?.toString() || '1',
25 in: 'query',
26 },
27 // Add custom headers
28 {
29 name: 'x-custom-header',
30 value: input.customHeader || 'default-value',
31 in: 'header',
32 },
33 ],
34 });
35 return result;
36 },
37});
  1. Using connectionConfig - For making direct API calls when needed:
1const tool = await composio.tools.createCustomTool({
2 slug: 'GITHUB_DIRECT_API',
3 name: 'Direct GitHub API Call',
4 description: 'Makes direct calls to GitHub API',
5 toolkitSlug: 'github',
6 inputParams: z.object({
7 repo: z.string().describe('Repository name'),
8 }),
9 execute: async (input, connectionConfig, executeToolRequest) => {
10 // Use connectionConfig for direct API calls
11 const result = await fetch(`https://api.github.com/repos/${input.repo}`, {
12 headers: {
13 Authorization: `Bearer ${connectionConfig.access_token}`,
14 },
15 });
16
17 return {
18 data: await result.json(),
19 error: null,
20 successful: true,
21 };
22
23},
24});

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:

1const tool = await composio.tools.createCustomTool({
2 slug: 'GITHUB_SEARCH_REPOSITORIES',
3 name: 'Search GitHub Repositories',
4 description: 'Search for repositories with custom parameters',
5 toolkitSlug: 'github',
6 inputParams: z.object({
7 query: z.string().describe('Search query'),
8 perPage: z.number().optional().describe('Results per page'),
9 acceptType: z.string().optional().describe('Custom accept header'),
10 }),
11 execute: async (input, connectionConfig, executeToolRequest) => {
12 const result = await executeToolRequest({
13 endpoint: '/search/repositories',
14 method: 'GET',
15 parameters: [
16 // Add query parameters for pagination
17 {
18 name: 'q',
19 value: input.query,
20 in: 'query',
21 },
22 {
23 name: 'per_page',
24 value: (input.perPage || 30).toString(),
25 in: 'query',
26 },
27 // Add custom headers
28 {
29 name: 'Accept',
30 value: input.acceptType || 'application/vnd.github.v3+json',
31 in: 'header',
32 },
33 {
34 name: 'X-Custom-Header',
35 value: 'custom-value',
36 in: 'header',
37 },
38 ],
39 });
40 return result;
41 },
42});

Executing Custom Tools

You can execute custom tools just like any other tool:

1const result = await composio.tools.execute('TOOL_SLUG', {
2 arguments: {
3 // Tool input parameters
4 },
5 userId: 'user-id',
6 connectedAccountId: 'optional-account-id', // Required for toolkit-based tools
7});

Best Practices

  1. Use descriptive names and slugs for your tools
  2. Always provide descriptions for input parameters using describe()
  3. Handle errors gracefully in your execute function
  4. For toolkit-based tools:
    • Prefer executeToolRequest over direct API calls when possible
    • Use relative paths with executeToolRequest - Composio will automatically inject the correct baseURL
    • Use the parameters option to add custom headers or query parameters:
      1parameters: [
      2 { name: 'page', value: '1', in: 'query' }, // Adds ?page=1 to URL
      3 { name: 'x-custom', value: 'value', in: 'header' }, // Adds header
      4];
    • Remember that executeToolRequest can only call tools from the same toolkit
    • Use executeToolRequest to leverage Composio’s automatic credential handling
    • Only use connectionConfig when you need to make direct API calls or interact with different services
  5. Chain multiple toolkit operations using executeToolRequest for better maintainability

Limitations

  1. Custom tools are stored in memory and are not persisted
  2. They need to be recreated when the application restarts
  3. Toolkit-based tools require a valid connected account with the specified toolkit
  4. executeToolRequest can only execute tools from the same toolkit that the custom tool belongs to
  5. Each toolkit-based tool can only use one connected account at a time