Skip to content

Commit 551ebc5

Browse files
authored
docs: add Python custom tools docs (#3020)
## Summary - add Python tabs to the main custom tools and toolkits docs page - update the changelog entry to include Python support and versions - keep inline references limited to docs/reference pages that already exist today ## Notes - the Python docs intentionally describe the decorator + Pydantic annotation DX rather than mirroring the TypeScript builder API - I did not add Python ToolRouterSession / SessionContext reference links because those generated reference pages are not currently surfaced in the Python SDK reference ## Validation - content reviewed locally in a clean worktree - docs build/link validation could not be run in this environment because the docs workspace dependencies were not installed (`next` / `fumadocs-mdx` resolution failed)
2 parents 936d4b2 + d7e7e3a commit 551ebc5

File tree

2 files changed

+221
-11
lines changed

2 files changed

+221
-11
lines changed

docs/content/changelog/03-23-26-custom-tools-and-toolkits.mdx

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,16 @@ You can now create custom tools and toolkits that run locally alongside remote C
1111
| Package | Version |
1212
|---------|---------|
1313
| @composio/core | v0.6.6 |
14+
| composio | v0.11.4 |
1415

1516
---
1617

1718
## What's New
1819

19-
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.
20+
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.
21+
22+
<Tabs groupId="language" items={['TypeScript', 'Python']} persist>
23+
<Tab value="TypeScript">
2024

2125
```typescript
2226
import { Composio, experimental_createTool, experimental_createToolkit } from "@composio/core";
@@ -86,18 +90,101 @@ const session = await composio.create("user_1", {
8690
const tools = await session.tools(); // Includes both remote and custom tools
8791
```
8892

93+
</Tab>
94+
<Tab value="Python">
95+
96+
```python
97+
import base64
98+
99+
from pydantic import BaseModel, Field
100+
101+
from composio import Composio
102+
103+
104+
composio = Composio(api_key="your_api_key")
105+
106+
107+
class UserLookupInput(BaseModel):
108+
user_id: str = Field(description="User ID")
109+
110+
111+
@composio.experimental.tool()
112+
def get_user_profile(input: UserLookupInput, ctx):
113+
"""Retrieve the current user's profile from the internal directory."""
114+
profiles = {
115+
"user_1": {"name": "Alice Johnson", "tier": "enterprise"},
116+
}
117+
return profiles.get(input.user_id, {})
118+
119+
120+
class PromoEmailInput(BaseModel):
121+
to: str = Field(description="Recipient email")
122+
123+
124+
@composio.experimental.tool(extends_toolkit="gmail")
125+
def send_promo_email(input: PromoEmailInput, ctx):
126+
"""Send the standard promotional email to a recipient."""
127+
raw = base64.urlsafe_b64encode(
128+
(
129+
f"To: {input.to}\r\n"
130+
"Subject: Try MyApp Pro\r\n"
131+
"Content-Type: text/plain; charset=UTF-8\r\n\r\n"
132+
"Free for 14 days."
133+
).encode()
134+
).decode().rstrip("=")
135+
res = ctx.proxy_execute(
136+
toolkit="gmail",
137+
endpoint="https://gmail.googleapis.com/gmail/v1/users/me/messages/send",
138+
method="POST",
139+
body={"raw": raw},
140+
)
141+
return {"status": res.status, "to": input.to}
142+
143+
144+
user_management = composio.experimental.Toolkit(
145+
slug="USER_MANAGEMENT",
146+
name="User management",
147+
description="Manage user roles and permissions",
148+
)
149+
150+
151+
class AssignRoleInput(BaseModel):
152+
user_id: str = Field(description="Target user ID")
153+
role: str = Field(description="Role to assign")
154+
155+
156+
@user_management.tool()
157+
def assign_role(input: AssignRoleInput, ctx):
158+
"""Assign a role to a user in the internal system."""
159+
return {"user_id": input.user_id, "role": input.role, "assigned": True}
160+
161+
162+
session = composio.create(
163+
user_id="user_1",
164+
toolkits=["gmail"],
165+
experimental={
166+
"custom_tools": [get_user_profile, send_promo_email],
167+
"custom_toolkits": [user_management],
168+
},
169+
)
170+
171+
tools = session.tools() # Includes both remote and custom tools
172+
```
173+
174+
</Tab>
175+
</Tabs>
176+
89177
### SessionContext
90178

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

93-
- **`ctx.userId`** — the user ID for the current session
94-
- **`ctx.proxyExecute(params)`** — authenticated HTTP request via Composio's auth layer
95-
- **`ctx.execute(toolSlug, args)`** — execute any Composio native tool from within your custom tool
181+
- **TypeScript**`ctx.userId`, `ctx.proxyExecute(params)`, `ctx.execute(toolSlug, args)`
182+
- **Python**`ctx.user_id`, `ctx.proxy_execute(...)`, `ctx.execute(tool_slug, arguments)`
96183

97184
---
98185

99186
## Limitations
100187

101188
- **Native tools only** — custom tools work with `session.tools()`. MCP support is coming soon.
102-
- **TypeScript only**Python support is not yet available.
103-
- **Experimental API**the `experimental_` prefix and session option may change.
189+
- **Experimental API**the custom tool APIs and session `experimental` option may change.
190+
- **SDK-specific DX is intentional**TypeScript uses builder helpers and Zod. Python uses decorators, docstrings, and Pydantic annotations.

docs/content/docs/toolkits/custom-tools-and-toolkits.mdx

Lines changed: 128 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ keywords: [custom tools, custom toolkits, experimental, local tools, proxy execu
55
---
66

77
<Callout type="warn">
8-
Custom tools are experimental. The `experimental_` prefix and `experimental` session option may change in future releases. TypeScript only.
8+
Custom tool APIs are experimental. TypeScript uses `experimental_createTool()` / `experimental_createToolkit()`. Python uses `@composio.experimental.tool()` / `composio.experimental.Toolkit(...)`. The session `experimental` option may change in future releases.
99
</Callout>
1010

1111
<Callout type="info">
@@ -15,11 +15,14 @@ Custom tools work with **native tools** (`session.tools()`). MCP support is comi
1515
Custom tools let you define tools that run in-process alongside remote Composio tools within a session. There are three patterns:
1616

1717
- **Standalone tools** — for internal app logic that doesn't need Composio auth (DB lookups, in-memory data, business rules)
18-
- **Extension tools** — wrap a Composio toolkit's API with custom business logic via `extendsToolkit`, using `ctx.proxyExecute()` for authenticated requests
18+
- **Extension tools** — wrap a Composio toolkit's API with custom business logic via `extendsToolkit` / `extends_toolkit`, using `ctx.proxyExecute()` / `ctx.proxy_execute()` for authenticated requests
1919
- **Custom toolkits** — group related standalone tools under a namespace
2020

2121
The example below defines one of each and binds them to a session:
2222

23+
<Tabs groupId="language" items={['TypeScript', 'Python']} persist>
24+
<Tab value="TypeScript">
25+
2326
```typescript
2427
import { Composio, experimental_createTool, experimental_createToolkit } from "@composio/core";
2528
import { z } from "zod/v3";
@@ -104,6 +107,102 @@ const session = await composio.create("user_1", {
104107
const tools = await session.tools();
105108
```
106109

110+
</Tab>
111+
<Tab value="Python">
112+
113+
```python
114+
import base64
115+
116+
from pydantic import BaseModel, Field
117+
118+
from composio import Composio
119+
120+
121+
composio = Composio(api_key="your_api_key")
122+
123+
124+
class UserLookupInput(BaseModel):
125+
user_id: str = Field(description="User ID")
126+
127+
128+
USERS = {
129+
"user_1": {"name": "Alice Johnson", "email": "alice@myapp.com", "tier": "enterprise"},
130+
"user_2": {"name": "Bob Smith", "email": "bob@myapp.com", "tier": "free"},
131+
}
132+
133+
134+
@composio.experimental.tool()
135+
def get_user_profile(input: UserLookupInput, ctx):
136+
"""Retrieve the current user's profile from the internal directory."""
137+
profile = USERS.get(input.user_id)
138+
if not profile:
139+
raise ValueError(f'No profile found for user "{input.user_id}"')
140+
return profile
141+
142+
143+
class PromoEmailInput(BaseModel):
144+
to: str = Field(description="Recipient email address")
145+
146+
147+
@composio.experimental.tool(extends_toolkit="gmail")
148+
def send_promo_email(input: PromoEmailInput, ctx):
149+
"""Send the standard promotional email to a recipient."""
150+
subject = "You're invited to try MyApp Pro"
151+
body = (
152+
"Hi there,\n\n"
153+
"We'd love for you to try MyApp Pro — free for 14 days.\n\n"
154+
"Best,\nThe MyApp Team"
155+
)
156+
raw_msg = (
157+
f"To: {input.to}\r\n"
158+
f"Subject: {subject}\r\n"
159+
"Content-Type: text/plain; charset=UTF-8\r\n\r\n"
160+
f"{body}"
161+
)
162+
raw = base64.urlsafe_b64encode(raw_msg.encode()).decode().rstrip("=")
163+
164+
res = ctx.proxy_execute(
165+
toolkit="gmail",
166+
endpoint="https://gmail.googleapis.com/gmail/v1/users/me/messages/send",
167+
method="POST",
168+
body={"raw": raw},
169+
)
170+
return {"status": res.status, "to": input.to}
171+
172+
173+
user_management = composio.experimental.Toolkit(
174+
slug="USER_MANAGEMENT",
175+
name="User management",
176+
description="Manage user roles and permissions",
177+
)
178+
179+
180+
class AssignRoleInput(BaseModel):
181+
user_id: str = Field(description="Target user ID")
182+
role: str = Field(description="Role to assign")
183+
184+
185+
@user_management.tool()
186+
def assign_role(input: AssignRoleInput, ctx):
187+
"""Assign a role to a user in the internal system."""
188+
return {"user_id": input.user_id, "role": input.role, "assigned": True}
189+
190+
191+
session = composio.create(
192+
user_id="user_1",
193+
toolkits=["gmail"],
194+
experimental={
195+
"custom_tools": [get_user_profile, send_promo_email],
196+
"custom_toolkits": [user_management],
197+
},
198+
)
199+
200+
tools = session.tools()
201+
```
202+
203+
</Tab>
204+
</Tabs>
205+
107206
## How custom tools work with meta tools
108207

109208
Custom tools integrate seamlessly with Composio's meta tools:
@@ -116,27 +215,51 @@ Custom tools integrate seamlessly with Composio's meta tools:
116215

117216
## Best practices
118217

119-
- **Descriptive names and slugs** — The agent sees your tool's name and description to decide when to use it. Be specific: "Send weekly promo email" is better than "Send email". Slugs should be uppercase with underscores: `SEND_PROMO_EMAIL`.
218+
- **Descriptive names and slugs** — The agent sees your tool's name and description to decide when to use it. Be specific: "Send weekly promo email" is better than "Send email". In TypeScript, define uppercase slugs like `SEND_PROMO_EMAIL`. In Python, inferred slugs come from the function name, so `snake_case` function names produce the cleanest defaults; or pass `slug=` / `name=` explicitly.
120219
- **Detailed descriptions** — Include what the tool does, when to use it, and what it returns. The agent relies on this to pick the right tool.
121-
- **Use `extendsToolkit` for auth** — If your tool needs Gmail/GitHub/etc. auth for `ctx.proxyExecute()` or `ctx.execute()`, set `extendsToolkit` so connection management is handled seamlessly via `COMPOSIO_MANAGE_CONNECTIONS`.
220+
- **Use `extendsToolkit` / `extends_toolkit` for auth** — If your tool needs Gmail/GitHub/etc. auth for `ctx.proxyExecute()` / `ctx.proxy_execute()` or `ctx.execute()`, set the toolkit extension so connection management is handled seamlessly via `COMPOSIO_MANAGE_CONNECTIONS`.
221+
- **In Python, annotations are the schema** — The first parameter must be a Pydantic `BaseModel`, and its field descriptions become the input schema. Docstrings are used as the default tool description unless you pass `description=`.
122222
- **Tool names get prefixed** — Slugs exposed to the agent are automatically prefixed with `LOCAL_` and the toolkit name (if any). `GET_USER_PROFILE` becomes `LOCAL_GET_USER_PROFILE`, `ASSIGN_ROLE` in `USER_MANAGEMENT` becomes `LOCAL_USER_MANAGEMENT_ASSIGN_ROLE`. Your slugs cannot start with `LOCAL_` — this prefix is reserved.
123223

124224
For more best practices, see [How to Build Tools for AI Agents: A Field Guide](https://composio.dev/blog/how-to-build-tools-for-ai-agents-a-field-guide).
125225

126226
## Verifying registration
127227

128-
Use `session.customTools()` to list registered tools (with their final `LOCAL_` prefixed slugs), or filter by toolkit with `session.customTools({ toolkit: "USER_MANAGEMENT" })`. Use `session.customToolkits()` to list registered toolkits.
228+
Use `session.customTools()` / `session.customToolkits()` in TypeScript or `session.custom_tools()` / `session.custom_toolkits()` in Python to list registered tools and toolkits. Registered tool slugs include their final `LOCAL_` prefix, and toolkit-scoped tools also include the toolkit slug.
229+
230+
Reference: [TypeScript ToolRouterSession](/reference/sdk-reference/typescript/tool-router-session) · [Python SDK Reference](/reference/sdk-reference/python)
129231

130232
## Programmatic execution
131233

132234
Use `session.execute()` to run custom tools directly, outside of an agent loop (e.g. `session.execute("GET_USER_PROFILE")`). Custom tools execute in-process; remote tools are sent to the backend automatically.
133235

236+
Reference: [Tool Router API Reference](/reference/api-reference/tool-router) · [TypeScript ToolRouterSession](/reference/sdk-reference/typescript/tool-router-session) · [Python SDK Reference](/reference/sdk-reference/python)
237+
134238
## SessionContext
135239

136240
Every custom tool's `execute` function receives `(input, ctx)`. The `ctx` object provides:
137241

242+
<Tabs groupId="language" items={['TypeScript', 'Python']} persist>
243+
<Tab value="TypeScript">
244+
138245
| Method | Description |
139246
|--------|-------------|
140247
| `ctx.userId` | The user ID for the current session. |
141248
| `ctx.proxyExecute(params)` | Authenticated HTTP request via Composio's auth layer. Params: `toolkit`, `endpoint`, `method`, `body?`, `parameters?` (array of `{ in: "query" \| "header", name, value }`). |
142249
| `ctx.execute(toolSlug, args)` | Execute any Composio native tool from within your custom tool. |
250+
251+
</Tab>
252+
<Tab value="Python">
253+
254+
| Method | Description |
255+
|--------|-------------|
256+
| `ctx.user_id` | The user ID for the current session. |
257+
| `ctx.proxy_execute(...)` | Authenticated HTTP request via Composio's auth layer. Params: `toolkit`, `endpoint`, `method`, `body=None`, `parameters=[{"in": "query" \| "header", "name": ..., "value": ...}]`. |
258+
| `ctx.execute(tool_slug, arguments)` | Execute any Composio native tool from within your custom tool. |
259+
260+
</Tab>
261+
</Tabs>
262+
263+
In Python, `parameters` dictionaries accept both `in` and `type`; this guide uses `in` to match the TypeScript examples.
264+
265+
Reference: [TypeScript SessionContextImpl](/reference/sdk-reference/typescript/session-context-impl) · [Python SDK Reference](/reference/sdk-reference/python)

0 commit comments

Comments
 (0)