Custom Tools and Toolkits (Experimental)
Custom tool APIs are experimental and may change in future releases.
Custom tools work with native tools (session.tools()). MCP support is coming soon. Custom tools are not available via the MCP server URL yet.
Custom tools let you define tools that run in-process alongside remote Composio tools within a session. There are three patterns:
- Standalone tools - for internal app logic that doesn't need Composio auth (DB lookups, in-memory data, business rules)
- Extension tools - wrap a Composio toolkit's API with custom business logic via
extendsToolkit/extends_toolkit, usingctx.proxyExecute()/ctx.proxy_execute()for authenticated requests - Custom toolkits - group related standalone tools under a namespace
Install
npm install @composio/core zodpip install composioInitialize the client
import { Composio } from "@composio/core";
const composio = new Composio({ apiKey: "your_api_key" });from composio import Composio
composio = Composio(api_key="your_api_key")Create the tool
A standalone tool handles internal app logic that doesn't need Composio auth. ctx.userId identifies which user's session is running.
const profiles: Record<string, { name: string; email: string; tier: string }> = {
"user_1": { name: "Alice Johnson", email: "alice@myapp.com", tier: "enterprise" },
"user_2": { name: "Bob Smith", email: "bob@myapp.com", tier: "free" },
};
const getUserProfile = experimental_createTool("GET_USER_PROFILE", {
name: "Get user profile",
description: "Retrieve the current user's profile from the internal directory",
inputParams: z.object({}),
execute: async (_input, ctx) => {
const profile = profiles[ctx.userId];
if (!profile) throw new Error(`No profile found for user "${ctx.userId}"`);
return profile;
},
});from pydantic import BaseModel, Field
from composio import Composio
composio = Composio(api_key="your_api_key")
class UserLookupInput(BaseModel):
user_id: str = Field(description="User ID")
USERS = {
"user_1": {"name": "Alice Johnson", "email": "alice@myapp.com", "tier": "enterprise"},
"user_2": {"name": "Bob Smith", "email": "bob@myapp.com", "tier": "free"},
}
@composio.experimental.tool()
def get_user_profile(input: UserLookupInput, ctx):
"""Retrieve the current user's profile from the internal directory."""
profile = USERS.get(input.user_id)
if not profile:
raise ValueError(f'No profile found for user "{input.user_id}"')
return profileBind to a session
Pass custom tools via the experimental option. session.tools() returns both remote Composio tools and your custom tools.
const session = await composio.create("user_1", {
experimental: {
customTools: [getUserProfile],
},
});
const tools = await session.tools();from composio import Composio
composio = Composio(api_key="your_api_key")
session = composio.create(
user_id="user_1",
experimental={
"custom_tools": [get_user_profile],
},
)
tools = session.tools()Meta tools integration
Custom tools work automatically with Composio's meta tools:
| Meta tool | Behavior |
|---|---|
COMPOSIO_SEARCH_TOOLS | Includes custom tools in search results, with slight priority for tools that don't require auth |
COMPOSIO_GET_TOOL_SCHEMAS | Returns schemas for custom tools alongside remote tools |
COMPOSIO_MULTI_EXECUTE_TOOL | Runs custom tools in-process while remote tools go to the backend, merging results transparently |
COMPOSIO_MANAGE_CONNECTIONS | Handles auth for extension tools. If a tool extends gmail, the agent can prompt the user to connect Gmail |
Custom tools are not supported in Workbench.
Context object (ctx)
Every custom tool's execute function receives (input, ctx). Use ctx to access the current user, make authenticated API requests, or call other Composio tools.
| Property / Method | Description |
|---|---|
ctx.userId | The user ID for the current session |
ctx.proxyExecute({ toolkit, endpoint, method, body?, parameters? }) | Make an authenticated HTTP request via Composio's auth layer |
ctx.execute(toolSlug, args) | Execute any Composio native tool from within your custom tool |
| Property / Method | Description |
|---|---|
ctx.user_id | The user ID for the current session |
ctx.proxy_execute(toolkit, endpoint, method, body=None, parameters=[]) | Make an authenticated HTTP request via Composio's auth layer |
ctx.execute(tool_slug, arguments) | Execute any Composio native tool from within your custom tool |
See the full API in the SDK reference: TypeScript | Python
Verifying registration
Use these methods to list registered tools and toolkits. Slugs include their final LOCAL_ prefix, and toolkit-scoped tools also include the toolkit slug.
const customTools = session.customTools();
const customToolkits = session.customToolkits();custom_tools = session.custom_tools()
custom_toolkits = session.custom_toolkits()Programmatic execution
Use session.execute() to run custom tools directly, outside of an agent loop. Custom tools execute in-process; remote tools are sent to the backend automatically.
const result = await session.execute("GET_USER_PROFILE");result = session.execute("GET_USER_PROFILE")Best practices
Naming and descriptions
The agent relies on your tool's name and description to decide when to call it. Be specific: "Send weekly promo email" is better than "Send email". Include what the tool does, when to use it, and what it returns.
In TypeScript, use uppercase slugs like SEND_PROMO_EMAIL. In Python, slugs are inferred from the function name, so snake_case produces clean defaults. You can also pass slug and name explicitly.
Accessing authenticated APIs
If your tool needs to call an API that requires user credentials (Gmail, GitHub, etc.), set extendsToolkit / extends_toolkit to the toolkit name. Composio will handle authentication automatically, and the agent can prompt users to connect their account if needed.
Defining inputs in Python
Your tool's first parameter must be a Pydantic BaseModel. The field descriptions become what the agent sees as the input schema, and the function's docstring becomes the tool description. You can override this by passing description explicitly.
Tool names get prefixed
Slugs exposed to the agent are automatically prefixed with LOCAL_ and the toolkit name (if applicable):
GET_USER_PROFILEbecomesLOCAL_GET_USER_PROFILEASSIGN_ROLEinUSER_MANAGEMENTbecomesLOCAL_USER_MANAGEMENT_ASSIGN_ROLE
Your slugs cannot start with LOCAL_. This prefix is reserved.
For more best practices, see How to Build Tools for AI Agents: A Field Guide.