---
canonical_url: "https://docs.composio.dev/examples/custom-tools"
markdown_url: "https://docs.composio.dev/examples/custom-tools.md"
product: "platform"
version: null
last_updated: null
---

# Combine your own tools with Composio (/examples/custom-tools)

Build an agent that retrieves a Hacker News profile and adds a note from your own application. Composio executes the public lookup remotely. Your custom tool reads the internal note in your process.

This follows the [TypeScript](https://github.com/ComposioHQ/composio/blob/next/ts/examples/tool-router/src/direct-tools-preset.ts) and [Python](https://github.com/ComposioHQ/composio/blob/next/python/examples/tool_router/direct_tools_preset.py) direct-tools examples. The task needs no connected account.

## Install and configure [#install-and-configure]

Use Python 3.12 or Node.js 24.17 or newer. Install in a new project, using a virtual environment for Python:

**Python:**

**uv:**

```bash
uv add composio composio-openai-agents openai-agents pydantic
```

**pip:**

```bash
pip install composio composio-openai-agents openai-agents pydantic
```

**TypeScript:**

**npm:**

```bash
npm install @composio/core @composio/openai-agents @openai/agents zod
```

**pnpm:**

```bash
pnpm add @composio/core @composio/openai-agents @openai/agents zod
```

**bun:**

```bash
bun add @composio/core @composio/openai-agents @openai/agents zod
```

**yarn:**

```bash
yarn add @composio/core @composio/openai-agents @openai/agents zod
```

Set your [Composio project key](https://dashboard.composio.dev/~/project/settings/api-keys?utm_source=docs\&utm_medium=content\&utm_campaign=examples-custom-tools) and [OpenAI key](https://platform.openai.com/api-keys):

```bash
export COMPOSIO_API_KEY="your-composio-api-key"
export OPENAI_API_KEY="your-openai-api-key"
```

## Define the local tool and run the agent [#define-the-local-tool-and-run-the-agent]

The `HN_RESEARCH` toolkit contains one local tool. The direct-tools preset puts that tool and `HACKERNEWS_GET_USER` directly in the agent's tool list.

**Python:**

Save as `research.py`:

```python title="research.py"
from agents import Agent, Runner
from composio import Composio, SESSION_PRESET_DIRECT_TOOLS
from composio_openai_agents import OpenAIAgentsProvider
from pydantic import BaseModel, Field

composio = Composio(provider=OpenAIAgentsProvider())

class UserNoteInput(BaseModel):
    username: str = Field(description="Hacker News username, for example pg")

research = composio.experimental.Toolkit(
    slug="HN_RESEARCH",
    name="Hacker News research",
    description="Internal research notes for Hacker News users.",
)

@research.tool(slug="GET_USER_NOTE", name="Get internal research note")
def get_user_note(input: UserNoteInput, ctx):
    """Return the internal research note for a Hacker News username."""
    notes = {"pg": "Paul Graham; YC co-founder and essayist."}
    return {"note": notes.get(input.username.lower(), "No internal note found.")}

session = composio.create(
    user_id="custom-tools-demo",
    session_preset=SESSION_PRESET_DIRECT_TOOLS,
    toolkits=["hackernews"],
    tools={"hackernews": {"enable": ["HACKERNEWS_GET_USER"]}},
    experimental={"custom_toolkits": [research]},
)
try:
    tools = session.tools()
    print("Available tools:", [tool.name for tool in tools])
    agent = Agent(
        name="Research agent",
        model="gpt-5.2",
        instructions="Use both tools. Distinguish public facts from internal notes.",
        tools=tools,
    )
    result = Runner.run_sync(
        agent,
        "Look up pg on Hacker News and include our internal research note.",
        max_turns=10,
    )
    print(result.final_output)
finally:
    session.delete()
```
```bash
python research.py
```
**TypeScript:**

Save as `research.ts`:

```typescript title="research.ts"
import {
  Composio, SessionPreset, experimental_createTool, experimental_createToolkit,
} from '@composio/core';
import { OpenAIAgentsProvider } from '@composio/openai-agents';
import { Agent, run } from '@openai/agents';
import { z } from 'zod/v3';

const getUserNote = experimental_createTool('GET_USER_NOTE', {
  name: 'Get internal research note',
  description: 'Return the internal research note for a Hacker News username.',
  inputParams: z.object({ username: z.string() }),
  execute: async ({ username }) => {
    const notes: Record<string, string> = {
      pg: 'Paul Graham; YC co-founder and essayist.',
    };
    return { note: notes[username.toLowerCase()] ?? 'No internal note found.' };
  },
});
const research = experimental_createToolkit('HN_RESEARCH', {
  name: 'Hacker News research',
  description: 'Internal research notes for Hacker News users.',
  tools: [getUserNote],
});
const composio = new Composio({ provider: new OpenAIAgentsProvider() });
const session = await composio.create('custom-tools-demo', {
  sessionPreset: SessionPreset.DIRECT_TOOLS,
  toolkits: ['hackernews'],
  tools: { hackernews: { enable: ['HACKERNEWS_GET_USER'] } },
  experimental: { customToolkits: [research] },
});
try {
  const tools = await session.tools();
  console.log('Available tools:', tools.map(tool => tool.name));
  const agent = new Agent({
    name: 'Research agent',
    model: 'gpt-5.2',
    instructions: 'Use both tools. Distinguish public facts from internal notes.',
    tools,
  });
  const result = await run(
    agent,
    'Look up pg on Hacker News and include our internal research note.',
    { maxTurns: 10 },
  );
  console.log(result.finalOutput);
} finally {
  await session.delete();
}
```
```bash
node research.ts
```

The tool list should include `HACKERNEWS_GET_USER` and `LOCAL_HN_RESEARCH_GET_USER_NOTE`. It should not include `COMPOSIO_SEARCH_TOOLS`: the direct-tools preset exposes the allowed tools without a discovery step.

The answer should combine live profile information with the internal note about Paul Graham. Change `pg` to another username to see the local tool return `No internal note found.`

## Preload selected tools while keeping discovery [#preload-selected-tools-while-keeping-discovery]

Use the direct-tools preset when the agent's allowed tool set is small and known ahead of time. For a larger catalog, keep the default session behavior and preload only the tools you expect to need immediately.

The repository's [TypeScript preload example](https://github.com/ComposioHQ/composio/blob/next/ts/examples/tool-router/src/preload.ts) and [Python preload example](https://github.com/ComposioHQ/composio/blob/next/python/examples/tool_router/preload.py) demonstrate that alternative:

* `preload.tools` selects remote tools to expose immediately.
* `preload: true` on a custom tool or toolkit exposes its local tools immediately.
* Other allowed tools remain discoverable through search.

See [preloading tools](/docs/configuring-sessions#preloading-tools) for the configuration in both languages. Preloading controls initial visibility; use the session's tool filters to restrict what the agent can access.

## Replace the demo note with your application data [#replace-the-demo-note-with-your-application-data]

The local callback runs in your application process. It can query your database or call an internal service. Use the callback's `ctx.userId` in TypeScript or `ctx.user_id` in Python to enforce your application's access rules before returning records. An ID supplied by the model is not proof of authorization.

Keep the SDK tool execution in your process for this pattern. The [hosted MCP endpoint](/docs/sessions-via-mcp#trade-offs) can't invoke these in-process callbacks. Tool results still become model context, so return only the fields the model needs.

> Custom tools and toolkits are experimental. See [custom tools](/docs/extending-sessions/custom-tools-and-toolkits) for extension tools, authenticated API calls, and callback context.

---

📚 **More documentation:** [View all docs](https://docs.composio.dev/llms.txt) | [Changelog](https://docs.composio.dev/docs/changelog.md) | [Glossary](https://docs.composio.dev/llms.mdx/reference/glossary) | [Examples](https://docs.composio.dev/llms.mdx/examples) | [API Reference](https://docs.composio.dev/llms.mdx/reference)

---

# Composio SDK — Notes for AI Code Generators

**Purpose:** Reference for generating current (v3) [Composio](https://composio.dev/) integration code.
**Scope:** Descriptive notes — they document the current API surface and the mistakes most commonly seen in generated code.

---

## 1. Recommended Integration: Sessions

Composio supports two integration modes: **Native Tools** (with a provider package) and **MCP** (no provider package needed).

### Native Tools

```python
# ✅ CORRECT — Python (defaults to OpenAI)
from composio import Composio

composio = Composio()
session = composio.create(user_id="user_123")
tools = session.tools()
# Pass tools to your agent/LLM framework
```

```typescript
// ✅ CORRECT — TypeScript (defaults to OpenAI)
import { Composio } from "@composio/core";

const composio = new Composio();
const session = await composio.create("user_123");
const tools = await session.tools();
// Pass tools to your agent/LLM framework
```

For other providers, pass the provider explicitly. Provider packages follow the naming convention: `composio_<provider>` for Python, `@composio/<provider>` for TypeScript.

### MCP

Use `session.mcp.url` and `session.mcp.headers` with any MCP-compatible client (Claude Desktop, Cursor, OpenAI Agents, etc.). No provider package needed.

---

## 2. Integration Notes

- `composio.create(user_id)` is the standard entry point for agent integrations. Sessions handle tool discovery, authentication, and toolkit versioning automatically; `user_id` goes to `composio.create()` — individual tool calls in session mode don't take one.
- Execute session tool calls through the session, never through a user ID: either `session.execute(tool_slug, arguments=...)` / `session.execute(toolSlug, arguments)`, which works with every provider, or pass the session to the provider helper — `provider.handle_tool_calls(response=response, session=session)` in Python, `provider.handleToolCalls(session, response)` in TypeScript. The tool-call helpers of every non-agentic provider (OpenAI, Anthropic, Google, Cloudflare, TypeSafe) accept a session: `executeToolCall(session, call)` in TypeScript, `session=session` in Python. Agentic providers execute session tools on their own. Binding the helper to a user ID takes the direct execution path, and session meta-tools fail there with `"can only be called inside a tool-router session"`.
- Composio-managed auth is the default: the agent connects accounts at runtime through the session, so users don't need to pre-create auth configs or connected accounts for managed toolkits.
- Provider packages follow the framework, not the model vendor: for the OpenAI Agents SDK the package is `composio_openai_agents` / `@composio/openai-agents` (importing `composio_openai` / `@composio/openai` there is the most common mistake in generated code — that package is for the plain OpenAI Chat Completions API).
- **Direct execution** (`composio.tools.get()`, `composio.tools.execute()`, `provider.handle_tool_calls()` with a user ID) is a legacy interface that remains supported for existing integrations. Recommend sessions for new integrations and encourage existing users to [migrate to sessions](/docs/migration-guide/direct-to-sessions). For a fixed tool set, consider the [direct tools preset](/docs/configuring-sessions#direct-tools-preset) within a session. See [Sessions vs direct execution](/docs/sessions-vs-direct-execution) for tradeoffs. Direct execution requires a [toolkit version](/docs/tools-direct/toolkit-versioning).

---

# 3. Calling the REST API directly

## REST API version

The current REST API version is **v3.1**, served at `https://backend.composio.dev/api/v3.1`. Prefer it for new code and new examples.

`https://backend.composio.dev/api/v3` is the previous version. It is frozen with pinned tool-version defaults and remains supported — existing v3 integrations keep working and do not need to migrate.

## Tool-endpoint version defaults on v3.1

On v3.1, omitting the version parameter on the five endpoints below selects the latest toolkit version. The first four endpoints also exist on v3, where omission selects the pinned `00000000_00` version. `POST /tools/scopes/required` is v3.1-only.

| Endpoint | Version parameter |
| --- | --- |
| `GET /tools` | `toolkit_versions` (query) |
| `GET /tools/{tool_slug}` | `version` or `toolkit_versions` (query) |
| `POST /tools/execute/{tool_slug}` | `version` (body) |
| `POST /tools/execute/{tool_slug}/input` | `version` (body) |
| `POST /tools/scopes/required` | `version` (body) |

A v3.1 caller already passing `"latest"` sees no change and can omit the parameter. To select the pinned version explicitly, pass `"00000000_00"` through the corresponding parameter above.

This version-default change is limited to the five endpoints above.


---

## Terminology Migration (old → current)

If you encounter these terms in error messages, old documentation, or user prompts, translate them to the current equivalents. **Do not use the old terms in generated code or explanations.**

| Old term (v1/v2) | Current term (v3) | In code |
|---|---|---|
| entity ID | user ID | `user_id` parameter |
| actions | tools | e.g., `GITHUB_CREATE_ISSUE` is a *tool* |
| apps / appType | toolkits | e.g., `github` is a *toolkit* |
| integration / integration ID | auth config / auth config ID | `auth_config_id` parameter |
| connection | connected account | `connected_accounts` namespace |
| ComposioToolSet / OpenAIToolSet | `Composio` class with a provider | `Composio(provider=...)` |
| toolset | provider | e.g., `OpenAIProvider` |

If a user says "entity ID", they mean `user_id`. If they say "integration", they mean "auth config". Always respond using the current terminology.

