Mar 23, 2026
Latest updates and announcements
Custom Tools and Toolkits (Experimental)
You can now create custom tools and toolkits that run locally alongside remote Composio tools within a session.
SDK Version
| Package | Version |
|---|---|
| @composio/core | v0.6.6 |
| composio | v0.11.4 |
What's New
Custom tools support three patterns — standalone tools for internal logic, extension tools that wrap Composio toolkit APIs with business logic, and custom toolkits that group related tools. TypeScript uses builder functions with Zod schemas; Python uses decorators with Pydantic annotations.
// Standalone tool — internal data, no Composio auth needed
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) => {
// ctx.userId identifies which user's session is running
const profiles: Record<string, { name: string; tier: string }> = {
"user_1": { name: "Alice Johnson", tier: "enterprise" },
};
return profiles[ctx.userId] ?? {};
},
});
// Extension tool — wraps Gmail with preset business logic
// Inherits auth from the session via extendsToolkit
const sendPromoEmail = experimental_createTool("SEND_PROMO_EMAIL", {
name: "Send promo email",
description: "Send the standard promotional email to a recipient",
extendsToolkit: "gmail",
inputParams: z.object({ to: z.string().describe("Recipient email") }),
execute: async (input, ctx) => {
const raw = btoa(`To: ${input.to}\r\nSubject: Try MyApp Pro\r\n\r\nFree for 14 days.`);
const res = await ctx.proxyExecute({
toolkit: "gmail",
endpoint: "https://gmail.googleapis.com/gmail/v1/users/me/messages/send",
method: "POST",
body: { raw },
});
return { status: res.status, to: input.to };
},
});
// Custom toolkit — groups related tools under a namespace
const userManagement = experimental_createToolkit("USER_MANAGEMENT", {
name: "User management",
description: "Manage user roles and permissions",
tools: [
experimental_createTool("ASSIGN_ROLE", {
name: "Assign role",
description: "Assign a role to a user in the internal system",
inputParams: z.object({
user_id: z.string().describe("Target user ID"),
role: z.enum(["admin", "editor", "viewer"]).describe("Role to assign"),
}),
execute: async ({ user_id, role }) => ({ user_id, role, assigned: true }),
}),
],
});
// Bind everything to a session
const composio = new Composio({ apiKey: "your_api_key" });
const session = await composio.create("user_1", {
toolkits: ["gmail"],
experimental: {
customTools: [getUserProfile, sendPromoEmail],
customToolkits: [userManagement],
},
});
const tools = await session.tools(); // Includes both remote and custom toolsimport base64
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")
@composio.experimental.tool()
def get_user_profile(input: UserLookupInput, ctx):
"""Retrieve the current user's profile from the internal directory."""
profiles = {
"user_1": {"name": "Alice Johnson", "tier": "enterprise"},
}
return profiles.get(input.user_id, {})
class PromoEmailInput(BaseModel):
to: str = Field(description="Recipient email")
@composio.experimental.tool(extends_toolkit="gmail")
def send_promo_email(input: PromoEmailInput, ctx):
"""Send the standard promotional email to a recipient."""
raw = base64.urlsafe_b64encode(
(
f"To: {input.to}\r\n"
"Subject: Try MyApp Pro\r\n"
"Content-Type: text/plain; charset=UTF-8\r\n\r\n"
"Free for 14 days."
).encode()
).decode().rstrip("=")
res = ctx.proxy_execute(
toolkit="gmail",
endpoint="https://gmail.googleapis.com/gmail/v1/users/me/messages/send",
method="POST",
body={"raw": raw},
)
return {"status": res.status, "to": input.to}
user_management = composio.experimental.Toolkit(
slug="USER_MANAGEMENT",
name="User management",
description="Manage user roles and permissions",
)
class AssignRoleInput(BaseModel):
user_id: str = Field(description="Target user ID")
role: str = Field(description="Role to assign")
@user_management.tool()
def assign_role(input: AssignRoleInput, ctx):
"""Assign a role to a user in the internal system."""
return {"user_id": input.user_id, "role": input.role, "assigned": True}
session = composio.create(
user_id="user_1",
toolkits=["gmail"],
experimental={
"custom_tools": [get_user_profile, send_promo_email],
"custom_toolkits": [user_management],
},
)
tools = session.tools() # Includes both remote and custom toolsSessionContext
Every custom tool's execute receives (input, ctx):
- TypeScript —
ctx.userId,ctx.proxyExecute(params),ctx.execute(toolSlug, args) - Python —
ctx.user_id,ctx.proxy_execute(...),ctx.execute(tool_slug, arguments)
Limitations
- Native tools only — custom tools work with
session.tools(). MCP support is coming soon. - Experimental API — the custom tool APIs and session
experimentaloption may change. - SDK-specific DX is intentional — TypeScript uses builder helpers and Zod. Python uses decorators, docstrings, and Pydantic annotations.