Back to Changelog

Mar 23, 2026

Latest updates and announcements

Markdown

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

PackageVersion
@composio/corev0.6.6
composiov0.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 tools
import 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 tools

SessionContext

Every custom tool's execute receives (input, ctx):

  • TypeScriptctx.userId, ctx.proxyExecute(params), ctx.execute(toolSlug, args)
  • Pythonctx.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 experimental option may change.
  • SDK-specific DX is intentional — TypeScript uses builder helpers and Zod. Python uses decorators, docstrings, and Pydantic annotations.