# Composio Documentation > Composio powers 1000+ toolkits, tool search, context management, authentication, and a sandboxed workbench to help you build AI agents that turn intent into action. --- # Composio SDK — Instructions for AI Code Generators **Purpose:** Enforce only the **current** and **correct** patterns for integrating [Composio](https://composio.dev/) tools into AI agents. **Scope:** All AI-generated code using Composio must follow these guardrails. --- ## 1. Correct Composio Integration 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_` for Python, `@composio/` 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. CRITICAL INSTRUCTIONS FOR AI MODELS ### 2.1 — ALWAYS DO THE FOLLOWING 1. **Use `composio.create(user_id)`** to create a session — this is the entry point for all Composio integrations. 2. **Use `session.tools()`** for native tool integration or **`session.mcp.url`** for MCP integration. 3. **Import** from the correct provider package (`composio_` for Python, `@composio/` for TypeScript). 4. **Pass `user_id` to `composio.create()`**, not to individual tool calls. ### 2.2 — NEVER DO THE FOLLOWING 1. **Do not** instruct users to manually create auth configs or connected accounts before running. 2. **Do not** import from `composio_openai` / `@composio/openai` when using OpenAI Agents SDK — use `composio_openai_agents` / `@composio/openai-agents` instead. ### 2.3 — DISCOURAGED (do not use unless user explicitly asks) Do **not** generate `composio.tools.get()`, `composio.tools.execute()`, or `composio.provider.handle_tool_calls()` unless the user explicitly requests direct tool execution. These are a supported but **not recommended** low-level interface. --- ## 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. # Documentation --- # Welcome (/docs) --- # Quickstart (/docs/quickstart) Build your first AI agent with Composio Tools. You'll create a [session](/docs/how-composio-works) for a user, give your agent access to [tools](/docs/how-composio-works), and let it take action across 1000+ apps. > The TypeScript SDK is ESM-only and requires Node.js 22.22.3 or newer. Use `import` syntax rather than CommonJS `require()`. ## OpenAI Agents #### Install **Python:** **TypeScript:** #### Configure API Keys > Get your `COMPOSIO_API_KEY` from [Settings](https://dashboard.composio.dev/settings) and `OPENAI_API_KEY` from [OpenAI](https://platform.openai.com/api-keys). ```bash title=".env" COMPOSIO_API_KEY=your_composio_api_key OPENAI_API_KEY=your_openai_api_key ``` #### Create session and run agent **Python:** ```python from dotenv import load_dotenv from composio import Composio from agents import Agent, Runner, SQLiteSession from composio_openai_agents import OpenAIAgentsProvider load_dotenv() # Initialize Composio with OpenAI Agents provider composio = Composio(provider=OpenAIAgentsProvider()) # Create a session for your user user_id = "user_123" session = composio.sessions.create(user_id=user_id) tools = session.tools() # For multi-turn, store the session ID in your db and reuse instead of creating another session: # session_id = session.session_id # session = composio.use(session_id) agent = Agent( name="Personal Assistant", instructions="You are a helpful personal assistant. Use Composio tools to take action.", model="gpt-5.2", tools=tools, ) # Memory for multi-turn conversation memory = SQLiteSession("conversation") print(""" What task would you like me to help you with? I can use tools like Gmail, GitHub, Linear, Notion, and more. (Type 'exit' to exit) Example tasks: - 'Summarize my emails from today' - 'List all open issues on the composio github repository' """) while True: user_input = input("You: ").strip() if user_input.lower() == "exit": break print("Assistant: ", end="", flush=True) result = Runner.run_sync(starting_agent=agent, input=user_input, session=memory) print(f"{result.final_output}\n") ``` **TypeScript:** ```typescript import "dotenv/config"; import { Composio } from "@composio/core"; import { Agent, run, MemorySession } from "@openai/agents"; import { OpenAIAgentsProvider } from "@composio/openai-agents"; import { createInterface } from "readline/promises"; // Initialize Composio with OpenAI Agents provider const composio = new Composio({ provider: new OpenAIAgentsProvider() }); // Create a session for your user const userId = "user_123"; const session = await composio.create(userId); const tools = await session.tools(); // For multi-turn, store the session ID in your db and reuse instead of calling create() again: // const sessionId = session.sessionId; // const session = await composio.use(sessionId); const agent = new Agent({ name: "Personal Assistant", instructions: "You are a helpful personal assistant. Use Composio tools to take action.", model: "gpt-5.2", tools, }); const memory = new MemorySession(); const readline = createInterface({ input: process.stdin, output: process.stdout }); console.log(` What task would you like me to help you with? I can use tools like Gmail, GitHub, Linear, Notion, and more. (Type 'exit' to exit) Example tasks: - 'Summarize my emails from today' - 'List all open issues on the composio github repository' `); while (true) { const input = (await readline.question("You: ")).trim(); if (input.toLowerCase() === "exit") break; process.stdout.write("Assistant: "); const result = await run(agent, input, { session: memory }); process.stdout.write(`${result.finalOutput}\n`); } readline.close(); ``` ## Claude Agent SDK #### Install **Python:** **TypeScript:** #### Configure API Keys > Get your `COMPOSIO_API_KEY` from [Settings](https://dashboard.composio.dev/settings) and `ANTHROPIC_API_KEY` from [Anthropic](https://console.anthropic.com/settings/keys). ```bash title=".env" COMPOSIO_API_KEY=your_composio_api_key ANTHROPIC_API_KEY=your_anthropic_api_key ``` #### Create session and run agent **Python:** ```python import asyncio from dotenv import load_dotenv from composio import Composio from composio_claude_agent_sdk import ClaudeAgentSDKProvider from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, create_sdk_mcp_server, AssistantMessage, TextBlock load_dotenv() # Initialize Composio with Claude Agent SDK provider composio = Composio(provider=ClaudeAgentSDKProvider()) # Create a session for your user user_id = "user_123" session = composio.sessions.create(user_id=user_id) tools = session.tools() # For multi-turn, store the session ID in your db and reuse instead of creating another session: # session_id = session.session_id # session = composio.use(session_id) custom_server = create_sdk_mcp_server(name="composio", version="1.0.0", tools=tools) async def main(): options = ClaudeAgentOptions( system_prompt="You are a helpful assistant. Use tools to complete tasks.", permission_mode="bypassPermissions", mcp_servers={"composio": custom_server}, ) async with ClaudeSDKClient(options=options) as client: print(""" What task would you like me to help you with? I can use tools like Gmail, GitHub, Linear, Notion, and more. (Type 'exit' to exit) Example tasks: - 'Summarize my emails from today' - 'List all open issues on the composio github repository' """) while True: user_input = input("You: ").strip() if user_input.lower() == "exit": break await client.query(user_input) print("Claude: ", end="", flush=True) async for message in client.receive_response(): if isinstance(message, AssistantMessage): for block in message.content: if isinstance(block, TextBlock): print(block.text, end="", flush=True) print() asyncio.run(main()) ``` **TypeScript:** ```typescript import "dotenv/config"; import { Composio } from "@composio/core"; import { ClaudeAgentSDKProvider } from "@composio/claude-agent-sdk"; import { createSdkMcpServer, query } from "@anthropic-ai/claude-agent-sdk"; import { createInterface } from "readline/promises"; // Initialize Composio with Claude Agent SDK provider const composio = new Composio({ provider: new ClaudeAgentSDKProvider() }); // Create a session for your user const userId = "user_123"; const session = await composio.create(userId); const tools = await session.tools(); // For multi-turn, store the session ID in your db and reuse instead of calling create() again: // const sessionId = session.sessionId; // const session = await composio.use(sessionId); const customServer = createSdkMcpServer({ name: "composio", version: "1.0.0", tools: tools, }); const readline = createInterface({ input: process.stdin, output: process.stdout }); console.log(` What task would you like me to help you with? I can use tools like Gmail, GitHub, Linear, Notion, and more. (Type 'exit' to exit) Example tasks: - 'Summarize my emails from today' - 'List all open issues on the composio github repository and create a Google Sheet with the issues' `); let isFirstQuery = true; const options = { mcpServers: { composio: customServer }, permissionMode: "bypassPermissions" as const, }; while (true) { const input = (await readline.question("You: ")).trim(); if (input.toLowerCase() === "exit") break; const queryOptions = isFirstQuery ? options : { ...options, continue: true }; isFirstQuery = false; process.stdout.write("Claude: "); for await (const stream of query({ prompt: input, options: queryOptions })) { if (stream.type === "assistant") { for (const block of stream.message.content) { if (block.type === "text") { process.stdout.write(block.text); } } } } console.log(); } readline.close(); ``` ## Vercel AI SDK #### Install #### Configure API Keys > Get your `COMPOSIO_API_KEY` from [Settings](https://dashboard.composio.dev/settings) and `ANTHROPIC_API_KEY` from [Anthropic](https://console.anthropic.com/settings/keys). ```bash title=".env" COMPOSIO_API_KEY=your_composio_api_key ANTHROPIC_API_KEY=your_anthropic_api_key ``` #### Create session and run agent ```typescript import "dotenv/config"; import { anthropic } from "@ai-sdk/anthropic"; import { Composio } from "@composio/core"; import { VercelProvider } from "@composio/vercel"; import { streamText, stepCountIs, type ModelMessage } from "ai"; import { createInterface } from "readline/promises"; // Initialize Composio with Vercel provider const composio = new Composio({ provider: new VercelProvider() }); // Create a session for your user const userId = "user_123"; const session = await composio.create(userId); const tools = await session.tools(); // For multi-turn, store the session ID in your db and reuse instead of calling create() again: // const sessionId = session.sessionId; // const session = await composio.use(sessionId); const readline = createInterface({ input: process.stdin, output: process.stdout }); console.log(` What task would you like me to help you with? I can use tools like Gmail, GitHub, Linear, Notion, and more. (Type 'exit' to exit) Example tasks: - 'Summarize my emails from today' - 'List all open issues on the composio github repository' `); const messages: ModelMessage[] = []; while (true) { const input = (await readline.question("You: ")).trim(); if (input.toLowerCase() === "exit") break; messages.push({ role: "user", content: input }); process.stdout.write("Assistant: "); const result = await streamText({ system: "You are a helpful personal assistant. Use Composio tools to take action.", model: anthropic("claude-sonnet-4-6"), messages, stopWhen: stepCountIs(10), onStepFinish: (step) => { for (const toolCall of step.toolCalls) { process.stdout.write(`\n[Using tool: ${toolCall.toolName}]`); } }, tools, }); for await (const textPart of result.textStream) { process.stdout.write(textPart); } console.log(); messages.push(...(await result.response).messages); } readline.close(); ``` By default, each `composio.sessions.create()` call returns a new session with access to all toolkits. Sessions are highly configurable beyond that: [Reusing a session](/docs/how-composio-works#how-sessions-behave) covers storing a session ID and calling `composio.use()` across multi-turn requests, while [Configuring Sessions](/docs/configuring-sessions) covers restricting toolkits, auth configs, and connected accounts. Prefer to connect over the Model Context Protocol instead? Every session also exposes an MCP endpoint. See [Using sessions via MCP](/docs/sessions-via-mcp). # Next - [Configuring Sessions](/docs/configuring-sessions): Restrict toolkits, set custom auth configs, and select connected accounts --- # Providers (/docs/providers) Composio works with any AI framework. A provider is the adapter that turns Composio tools into the native tool format your framework expects, so you don't write glue code. Pick the provider that matches the SDK or agent framework you already use. Each one fetches tools, handles execution, and hands your agent objects it understands. If your framework isn't listed, you can [build your own provider](/docs/providers/custom-providers). # AI SDKs Use these when you call a model SDK directly. - [Anthropic](/docs/providers/anthropic) ('Python', 'TypeScript') - [OpenAI](/docs/providers/openai) ('Python', 'TypeScript') - [Vercel AI SDK](/docs/providers/vercel) ('TypeScript') - [Google](/docs/providers/google) ('Python', 'TypeScript') # Agent frameworks Use these when an agent framework orchestrates the tool calls for you. - [LangChain](/docs/providers/langchain) ('Python', 'TypeScript') - [CrewAI](/docs/providers/crewai) ('Python') - [LlamaIndex](/docs/providers/llamaindex) ('Python', 'TypeScript') - [Mastra](/docs/providers/mastra) ('TypeScript') - [Pi](/docs/providers/pi) ('TypeScript') - [AutoGen](/docs/providers/autogen) ('Python') # Custom - [Build your own](/docs/providers/custom-providers) ('Python', 'TypeScript') --- # Anthropic (/docs/providers/anthropic) The Anthropic provider formats Composio tools for Claude and executes the tool calls Claude returns. It works two ways: * The [Claude Messages API](https://docs.anthropic.com/en/api/messages), where you run the tool-call loop yourself. * The [Claude Agent SDK](https://docs.anthropic.com/en/docs/agents-and-tools/claude-agent-sdk), where the SDK runs the loop and Composio tools are exposed as an in-process MCP server. Pick the tab that matches your integration. > Choose your integration type · [Use this guide to decide](/docs/native-tools-vs-mcp) ### Messages API The `AnthropicProvider` transforms Composio tools into the format the Claude Messages API expects, then executes the tool calls Claude requests and shapes the results back into Messages API content blocks. **Install** **Python:** **TypeScript:** **Configure API Keys** > Set `COMPOSIO_API_KEY` with your API key from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys) and `ANTHROPIC_API_KEY` with your [Anthropic API key](https://console.anthropic.com/settings/keys). ```txt title=".env" COMPOSIO_API_KEY=xxxxxxxxx ANTHROPIC_API_KEY=xxxxxxxxx ``` **Create session and run** **Python:** ```python import json import anthropic from composio import Composio from composio_anthropic import AnthropicProvider composio = Composio(provider=AnthropicProvider()) client = anthropic.Anthropic() # Create a session for your user session = composio.create(user_id="user_123") tools = session.tools() messages = [ {"role": "user", "content": "Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'"} ] response = client.messages.create( model="claude-opus-4-6", max_tokens=4096, tools=tools, messages=messages, ) # Agentic loop: keep executing tool calls until the model responds with text while response.stop_reason == "tool_use": tool_use_blocks = [block for block in response.content if block.type == "tool_use"] results = composio.provider.handle_tool_calls(user_id="user_123", response=response) messages.append({"role": "assistant", "content": response.content}) messages.append({ "role": "user", "content": [ {"type": "tool_result", "tool_use_id": tool_use_blocks[i].id, "content": json.dumps(result)} for i, result in enumerate(results) ] }) response = client.messages.create( model="claude-opus-4-6", max_tokens=4096, tools=tools, messages=messages, ) # Print final response for block in response.content: if block.type == "text": print(block.text) ``` **TypeScript:** ```typescript import Anthropic from '@anthropic-ai/sdk'; import { Composio } from '@composio/core'; import { AnthropicProvider } from '@composio/anthropic'; const composio = new Composio({ provider: new AnthropicProvider(), }); const client = new Anthropic(); // Create a session for your user const session = await composio.create("user_123"); const tools = await session.tools(); const messages: Anthropic.MessageParam[] = [ { role: "user", content: "Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'" }, ]; let response = await client.messages.create({ model: "claude-opus-4-6", max_tokens: 4096, tools: tools, messages: messages, }); // Agentic loop: keep executing tool calls until the model responds with text while (response.stop_reason === "tool_use") { const toolResults = await composio.provider.handleToolCalls("user_123", response); messages.push({ role: "assistant", content: response.content }); messages.push(...toolResults); response = await client.messages.create({ model: "claude-opus-4-6", max_tokens: 4096, tools: tools, messages: messages, }); } // Print final response for (const block of response.content) { if (block.type === "text") { console.log(block.text); } } ``` > `handleToolCalls` does the loop body for you. It extracts every `tool_use` block from the response, executes the matching Composio tools, and returns a ready-to-append array of Messages API content (a `user` message holding `tool_result` blocks). That is why the TypeScript example spreads the result straight into `messages` with `messages.push(...toolResults)`. In Python it returns the raw result list, which the example wraps into `tool_result` blocks by hand. ### Claude Agent SDK The `ClaudeAgentSDKProvider` exposes your Composio tools to the Claude Agent SDK as an in-process MCP server. You build the server with `create_sdk_mcp_server` (Python) or `createSdkMcpServer` (TypeScript), register it on the agent, and the SDK runs the tool-call loop for you. **Install** **Python:** **TypeScript:** **Configure API Keys** > Set `COMPOSIO_API_KEY` with your API key from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys) and `ANTHROPIC_API_KEY` with your [Anthropic API key](https://console.anthropic.com/settings/keys). ```txt title=".env" COMPOSIO_API_KEY=xxxxxxxxx ANTHROPIC_API_KEY=xxxxxxxxx ``` **Create session and run** **Python:** ```python import asyncio from composio import Composio from composio_claude_agent_sdk import ClaudeAgentSDKProvider from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, create_sdk_mcp_server composio = Composio(provider=ClaudeAgentSDKProvider()) # Create a session for your user session = composio.create(user_id="user_123") tools = session.tools() tool_server = create_sdk_mcp_server(name="composio", version="1.0.0", tools=tools) async def main(): options = ClaudeAgentOptions( system_prompt="You are a helpful assistant", permission_mode="bypassPermissions", mcp_servers={"composio": tool_server}, ) async with ClaudeSDKClient(options=options) as client: await client.query("Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'") async for msg in client.receive_response(): print(msg) asyncio.run(main()) ``` **TypeScript:** ```js import { Composio } from '@composio/core'; import { ClaudeAgentSDKProvider } from '@composio/claude-agent-sdk'; import { createSdkMcpServer, query } from '@anthropic-ai/claude-agent-sdk'; const composio = new Composio({ provider: new ClaudeAgentSDKProvider(), }); // Create a session for your user const session = await composio.create("user_123"); const tools = await session.tools(); const toolServer = createSdkMcpServer({ name: "composio", version: "1.0.0", tools: tools, }); for await (const content of query({ prompt: "Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'", options: { mcpServers: { composio: toolServer }, permissionMode: "bypassPermissions", }, })) { if (content.type === "assistant") { console.log("Claude:", content.message); } } ``` # Provider specifics A few things are specific to the Anthropic provider: * **Tool caching.** Pass `cacheTools: true` to the constructor (`new AnthropicProvider({ cacheTools: true })`) to attach Anthropic's ephemeral `cache_control` to every tool definition and tool-result block. This lets Claude reuse cached tool schemas across requests and can cut prompt cost when you send the same large tool set on every turn. * **`handleToolCalls` returns Messages API content, not raw strings.** On the TypeScript side it returns `Anthropic.Messages.MessageParam[]` (a `user` message of `tool_result` blocks) that you append directly to your message list. This is why the loop above does not build `tool_result` blocks by hand. * **String-encoded tool inputs are handled for you.** Claude occasionally emits a tool's `input` as a JSON string instead of an object. The provider normalizes this before execution, so you do not have to parse it yourself. * **`cacheTools` only.** The constructor takes no other options. There is no agentic execution in this provider; you run the loop (Messages API) or hand the loop to the Claude Agent SDK (Agent SDK tab). # Next - [What is a session?](/docs/how-composio-works): How sessions scope users, tools, and auth, and how to reuse them across requests. --- # OpenAI (/docs/providers/openai) The OpenAI provider formats Composio tools for OpenAI's function-calling and executes the tool calls the model returns. It works three ways: * The [Responses API](https://platform.openai.com/docs/api-reference/responses), the recommended way to build agentic flows, where you run the tool-call loop yourself. * The [Chat Completions API](https://platform.openai.com/docs/api-reference/chat), the classic message-based interface, where you also run the loop. * The [Agents SDK](https://openai.github.io/openai-agents-python/), where the SDK runs the loop and executes Composio tools for you. The OpenAI provider is the default provider for the Composio SDK, so you get it without configuring anything. Pick the tab that matches your integration. > Choose your integration type · [Use this guide to decide](/docs/native-tools-vs-mcp) ### Responses API The `OpenAIResponsesProvider` transforms Composio tools into OpenAI's function-calling format for the Responses API, then executes the tool calls the model returns and shapes the results into `function_call_output` items you feed back in. **Install** **Python:** **TypeScript:** **Configure API Keys** > Set `COMPOSIO_API_KEY` with your API key from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys) and `OPENAI_API_KEY` with your [OpenAI API key](https://platform.openai.com/api-keys). ```txt title=".env" COMPOSIO_API_KEY=xxxxxxxxx OPENAI_API_KEY=xxxxxxxxx ``` **Create session and run** The [Responses API](https://platform.openai.com/docs/api-reference/responses) is the recommended way to build agentic flows with OpenAI. You pass `previous_response_id` on each turn so the model keeps the prior context, and you send back only the new `function_call_output` items. **Python:** ```python import json from openai import OpenAI from composio import Composio from composio_openai import OpenAIResponsesProvider composio = Composio(provider=OpenAIResponsesProvider()) client = OpenAI() # Create a session for your user session = composio.create(user_id="user_123") tools = session.tools() response = client.responses.create( model="gpt-5.2", tools=tools, input=[ { "role": "user", "content": "Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'" } ] ) # Agentic loop: keep executing tool calls until the model responds with text while True: tool_calls = [o for o in response.output if o.type == "function_call"] if not tool_calls: break results = composio.provider.handle_tool_calls(response=response, user_id="user_123") response = client.responses.create( model="gpt-5.2", tools=tools, previous_response_id=response.id, input=[ {"type": "function_call_output", "call_id": tool_calls[i].call_id, "output": json.dumps(result)} for i, result in enumerate(results) ] ) # Print final response for item in response.output: if item.type == "message": print(item.content[0].text) ``` **TypeScript:** ```typescript import OpenAI from 'openai'; import { Composio } from '@composio/core'; import { OpenAIResponsesProvider } from '@composio/openai'; const composio = new Composio({ provider: new OpenAIResponsesProvider(), }); const client = new OpenAI(); // Create a session for your user const session = await composio.create("user_123"); const tools = await session.tools(); let response = await client.responses.create({ model: "gpt-5.2", tools: tools, input: [ { role: "user", content: "Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'" }, ], }); // Agentic loop: keep executing tool calls until the model responds with text while (true) { const toolCalls = response.output.filter((o) => o.type === "function_call"); if (toolCalls.length === 0) break; const results = await composio.provider.handleToolCalls("user_123", response.output); response = await client.responses.create({ model: "gpt-5.2", tools: tools, previous_response_id: response.id, input: results.map((result, i) => ({ type: "function_call_output" as const, call_id: toolCalls[i].call_id, output: JSON.stringify(result), })), }); } // Print final response for (const item of response.output) { if (item.type === "message") { const block = item.content[0]; if (block.type === "output_text") { console.log(block.text); } } } ``` ### Chat Completions The `OpenAIProvider` targets the Chat Completions API and is the default provider used by the Composio SDK when you do not specify one. **Install** **Python:** **TypeScript:** **Configure API Keys** > Set `COMPOSIO_API_KEY` with your API key from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys) and `OPENAI_API_KEY` with your [OpenAI API key](https://platform.openai.com/api-keys). ```txt title=".env" COMPOSIO_API_KEY=xxxxxxxxx OPENAI_API_KEY=xxxxxxxxx ``` **Create session and run** The [Chat Completions API](https://platform.openai.com/docs/api-reference/chat) generates a model response from a list of messages. You keep the full message list yourself and append each assistant message and its `tool` results before the next call. **Python:** ```python import json from openai import OpenAI from composio import Composio from composio_openai import OpenAIProvider composio = Composio(provider=OpenAIProvider()) client = OpenAI() # Create a session for your user session = composio.create(user_id="user_123") tools = session.tools() messages = [ {"role": "user", "content": "Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'"} ] response = client.chat.completions.create( model="gpt-5.2", tools=tools, messages=messages, ) # Agentic loop: keep executing tool calls until the model responds with text while response.choices[0].message.tool_calls: results = composio.provider.handle_tool_calls(response=response, user_id="user_123") messages.append(response.choices[0].message) for i, tc in enumerate(response.choices[0].message.tool_calls): messages.append({ "role": "tool", "tool_call_id": tc.id, "content": json.dumps(results[i]), }) response = client.chat.completions.create( model="gpt-5.2", tools=tools, messages=messages, ) print(response.choices[0].message.content) ``` **TypeScript:** ```typescript import OpenAI from 'openai'; import { Composio } from '@composio/core'; import { OpenAIProvider } from '@composio/openai'; const composio = new Composio({ provider: new OpenAIProvider(), }); const client = new OpenAI(); // Create a session for your user const session = await composio.create("user_123"); const tools = await session.tools(); const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [ { role: "user", content: "Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'" }, ]; let response = await client.chat.completions.create({ model: "gpt-5.2", tools: tools, messages: messages, }); // Agentic loop: keep executing tool calls until the model responds with text while (response.choices[0].message.tool_calls) { const results = await composio.provider.handleToolCalls("user_123", response); messages.push(response.choices[0].message); for (const [i, tc] of response.choices[0].message.tool_calls.entries()) { messages.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify(results[i]), }); } response = await client.chat.completions.create({ model: "gpt-5.2", tools: tools, messages: messages, }); } console.log(response.choices[0].message.content); ``` ### Agents SDK The `OpenAIAgentsProvider` transforms Composio tools into the Agents SDK tool format with execution built in, so the SDK runs the tool-call loop and you only define the agent and call `run`. **Install** **Python:** **TypeScript:** **Configure API Keys** > Set `COMPOSIO_API_KEY` with your API key from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys) and `OPENAI_API_KEY` with your [OpenAI API key](https://platform.openai.com/api-keys). ```txt title=".env" COMPOSIO_API_KEY=xxxxxxxxx OPENAI_API_KEY=xxxxxxxxx ``` **Create session and run** **Python:** ```python import asyncio from composio import Composio from composio_openai_agents import OpenAIAgentsProvider from agents import Agent, Runner composio = Composio(provider=OpenAIAgentsProvider()) # Create a session for your user session = composio.create(user_id="user_123") tools = session.tools() agent = Agent( name="Email Agent", instructions="You are a helpful assistant.", tools=tools, ) async def main(): result = await Runner.run( starting_agent=agent, input="Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'", ) print(result.final_output) asyncio.run(main()) ``` **TypeScript:** ```typescript import { Composio } from "@composio/core"; import { OpenAIAgentsProvider } from "@composio/openai-agents"; import { Agent, run } from "@openai/agents"; const composio = new Composio({ provider: new OpenAIAgentsProvider(), }); // Create a session for your user const session = await composio.create("user_123"); const tools = await session.tools(); const agent = new Agent({ name: "Email Agent", instructions: "You are a helpful assistant.", tools, }); const result = await run( agent, "Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'" ); console.log(result.finalOutput); ``` # Provider specifics The OpenAI integration ships three providers, one per API surface: * **`OpenAIResponsesProvider`** for the Responses API. `handleToolCalls` executes the tool calls and returns `function_call_output` items keyed by `call_id`. You pair it with `previous_response_id` so you only resend new outputs each turn. * **`OpenAIProvider`** for the Chat Completions API. This is the SDK default, so `new Composio()` with no provider uses it. You keep the full message list and append each assistant message plus its `tool` results yourself. * **`OpenAIAgentsProvider`** for the Agents SDK. Tools come with execution wired in, so the SDK runs the loop and you do not call `handleToolCalls` at all. Use the Responses or Agents provider for new agentic flows; reach for Chat Completions when you are extending an existing Chat Completions codebase. # Next - [What is a session?](/docs/how-composio-works): How sessions scope users, tools, and auth, and how to reuse them across requests. --- # Vercel AI SDK (/docs/providers/vercel) The Vercel AI SDK provider transforms Composio tools into Vercel's [tool format](https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling) with built-in execution, so you don't write a manual agentic loop. Each wrapped tool carries its own `execute` function, and the AI SDK calls it for you. **Install** **Configure API Keys** > Set `COMPOSIO_API_KEY` with your API key from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys) and `ANTHROPIC_API_KEY` with your [Anthropic API key](https://console.anthropic.com/settings/keys). ```txt title=".env" COMPOSIO_API_KEY=xxxxxxxxx ANTHROPIC_API_KEY=xxxxxxxxx ``` **Create session and run** The Vercel provider is **agentic**: tools include an `execute` function, so the AI SDK handles tool calls automatically. Set [`stopWhen`](https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling) to cap how many tool-calling steps a run can take. ```typescript import { anthropic } from "@ai-sdk/anthropic"; import { Composio } from "@composio/core"; import { VercelProvider } from "@composio/vercel"; import { generateText, stepCountIs } from "ai"; const composio = new Composio({ provider: new VercelProvider() }); // Create a session for your user const session = await composio.create("user_123"); const tools = await session.tools(); const { text } = await generateText({ model: anthropic("claude-opus-4-6"), tools, prompt: "Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'", stopWhen: stepCountIs(10), }); console.log(text); ``` # Provider specifics **Strict mode.** Some models reject tool schemas that contain optional parameters. Pass `strict: true` to the provider to drop every non-required property from each tool's input schema before it reaches the AI SDK: ```typescript // @noErrors import { Composio } from "@composio/core"; import { VercelProvider } from "@composio/vercel"; const composio = new Composio({ provider: new VercelProvider({ strict: true }) }); ``` > The provider converts each Composio tool's JSON Schema to a Zod schema for the AI SDK and normalizes tool arguments, so it still works when a model emits tool input as a JSON string rather than an object. # Next - [What is a session?](/docs/how-composio-works): How sessions scope users, tools, and auth, and how to reuse them across requests. --- # Pi (/docs/providers/pi) The Pi provider adapts Composio tools for [`@earendil-works/pi-coding-agent`](https://www.npmjs.com/package/@earendil-works/pi-coding-agent). A Pi session can search tools, manage connections, execute tools, and run the remote sandbox. Unlike the other providers, `PiProvider` ships its own hook interface to intercept, allow, deny, or rewrite every helper call before the model sees the result. > The Pi provider ships from `@composio/experimental` for TypeScript projects. # Dynamic session helpers For most Pi apps, expose the dynamic helper tools. They let the model discover exact Composio tool slugs before it executes, request missing connections, and run sandbox commands when you enable them. **Install** **Configure API keys** ```txt title=".env" COMPOSIO_API_KEY=xxxxxxxxx ``` **Create a Composio session and Pi tools** ```typescript // @noErrors import { Composio } from '@composio/core'; import { PiProvider, createPiComposioSystemPrompt } from '@composio/experimental'; import { createAgentSession, DefaultResourceLoader, getAgentDir, SessionManager, } from '@earendil-works/pi-coding-agent'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY, provider: new PiProvider(), }); const composioSession = await composio.sessions.create('user_123', { toolkits: ['github', 'gmail'], manageConnections: { enable: true, callbackUrl: 'https://your-app.example.com/auth/callback', }, sandbox: { enable: true }, }); const composioTools = composio.provider.createSessionTools({ sessionId: composioSession.sessionId, search: composioSession.search.bind(composioSession), execute: composioSession.execute.bind(composioSession), callbackUrl: 'https://your-app.example.com/auth/callback', includeWorkbenchTools: true, connections: { getToolkitStates: toolkits => composioSession.toolkits({ toolkits }), authorizeToolkit: (toolkit, options) => composioSession.authorize(toolkit, options), }, hooks: { execute: (ctx, next) => { if (ctx.request.toolSlug === 'COMPOSIO_MANAGE_CONNECTIONS') { return ctx.deny('Use composio_manage_connections instead.'); } return next(); }, onAuthLink: async ctx => { await sendConnectionLinkToUser(ctx.url); return { message: 'Connection link sent out-of-band.' }; }, }, }); const loader = new DefaultResourceLoader({ cwd: process.cwd(), agentDir: getAgentDir(), systemPromptOverride: () => createPiComposioSystemPrompt(composioSession.sessionId, { includeWorkbenchTools: true, }), }); await loader.reload(); const { session: piSession } = await createAgentSession({ cwd: process.cwd(), resourceLoader: loader, sessionManager: SessionManager.inMemory(process.cwd()), customTools: composioTools, tools: [ 'composio_search_tools', 'composio_manage_connections', 'composio_execute_tool', 'composio_remote_workbench', 'composio_remote_bash', ], }); await piSession.prompt('Find my open GitHub issues and summarize the blockers.'); ``` The provider creates these Pi tools: * `composio_search_tools` searches Composio for exact tool slugs and schemas. * `composio_manage_connections` checks connection state and initiates auth for missing toolkits. * `composio_execute_tool` executes an exact Composio tool slug. * `composio_remote_workbench` runs Python in the Composio sandbox, and requires `includeWorkbenchTools: true`. * `composio_remote_bash` runs short bash commands in the sandbox filesystem, and requires `includeWorkbenchTools: true`. You can rename any of these helpers through the `names` option on `createSessionTools`, and the constants live on `PI_COMPOSIO_SESSION_TOOL_NAMES`. # Hooks Hooks are the Pi provider's distinctive feature: middleware that wraps each helper so you control what runs and what the model sees. Pass a `hooks` object to `createSessionTools`. Each hook is `(ctx, next)`: `await next()` runs the default behavior, returning a value replaces what the model sees, and `ctx.deny(reason)` blocks the call. `ctx.request` is mutable; `ctx.context` is read-only. ```typescript // @noErrors const composioTools = composio.provider.createSessionTools({ sessionId: composioSession.sessionId, search: composioSession.search.bind(composioSession), execute: composioSession.execute.bind(composioSession), hooks: { search: (ctx, next) => { ctx.request.toolkits = ctx.request.toolkits?.map(toolkit => toolkit === 'slack' ? 'slackbot' : toolkit ); return next(); }, execute: async (ctx, next) => { if (ctx.request.toolSlug.startsWith('COMPOSIO_')) { return ctx.deny('Meta tools are blocked.'); } const result = await next(); const file = await saveLargeOutput(result); return file ? { message: `Output saved to ${file}` } : result; }, remoteBash: (ctx, next) => { if (ctx.request.command.includes('rm -rf')) { return ctx.deny('Destructive bash commands are blocked.'); } return next(); }, onAuthLink: async (ctx, next) => { await sendConnectionLinkToUser(ctx.url); return shouldShowLinkToModel(ctx) ? next() : { message: 'Connection link sent out-of-band.' }; }, }, }); ``` Available hooks, each keyed on `PiSessionHooks`: | Hook | Wraps | | ------------------- | -------------------------------------------------------- | | `search` | Tool discovery; rewrite `query` or `toolkits` | | `manageConnections` | Connection checks and auth | | `execute` | Tool execution; rewrite `toolSlug`, `args`, or `account` | | `remoteWorkbench` | The remote Python helper | | `remoteBash` | The remote bash helper | | `onAuthLink` | Any auth link found in a result | Every `ctx.request` is fully typed, so your editor surfaces the exact fields. `ctx.deny` is also exported as `denyPiToolCall(reason)`. # Next - [What is a session?](/docs/how-composio-works): How sessions scope users, tools, and auth, and how to reuse them across requests. --- # Eve (/docs/providers/eve) The eve provider adapts Composio tools for the [eve](https://github.com/vercel/eve) agent framework. With `EveProvider` registered, `session.tools()` returns eve-native `defineTool`s, so an eve agent gets the Tool Router meta-tools and any preloaded custom toolkits from one call. The provider also ships a `(ctx, next)` hook interface to intercept, allow, deny, or rewrite meta-tool calls before the model sees the result. > The eve provider is experimental and currently ships from `@composio/experimental` for TypeScript projects. # Usage eve owns the agent loop and discovers tools from files, so you register the provider on the Composio client and expose the session's tools from a file under `agent/tools/`. **Install** ```bash npm install @composio/core @composio/experimental eve @ai-sdk/openai ``` This walkthrough uses OpenAI directly as one concrete model provider. OpenAI is only an example: you can use any model provider supported by eve by installing its AI SDK package and configuring the corresponding credential. **Configure credentials** ```txt title=".env.local" COMPOSIO_API_KEY=xxxxxxxxx OPENAI_API_KEY=xxxxxxxxx ``` `COMPOSIO_API_KEY` authenticates Composio tools. `OPENAI_API_KEY` authenticates the model call directly with OpenAI; this setup does not route through Vercel AI Gateway. **Configure the eve model** ```typescript title="agent/agent.ts" import { openai } from '@ai-sdk/openai'; import { defineAgent } from 'eve'; export default defineAgent({ model: openai('gpt-5.4-mini'), }); ``` To use another provider, replace `@ai-sdk/openai`, `OPENAI_API_KEY`, and `openai(...)` with the equivalent package, credential, and model factory supported by eve. Passing a provider model object calls that provider directly; an eve string model ID such as `openai/gpt-5.4-mini` uses Vercel AI Gateway instead. **Register the provider and create a session** ```typescript title="agent/session.ts" // @noErrors import { Composio } from '@composio/core'; import { EveProvider } from '@composio/experimental/eve'; const composio = new Composio({ provider: new EveProvider() }); export const session = composio.sessions.create('user_123', { toolkits: ['github', 'hackernews'], // optional: scope the session to specific apps }); ``` **Expose the tools to eve** ```typescript title="agent/tools/composio.ts" // @noErrors import { defineComposioTools } from '@composio/experimental/eve'; import { session } from '../session'; export default defineComposioTools(session); ``` `defineComposioTools(session)` returns a `step.started` dynamic resolver and memoizes `session.tools()`. It resolves per step because the wrapped `execute` holds a live function eve keeps only for step-scoped tools. The fetch is cached per resolved Composio session, and transient failures are retried on the next step. For a multi-user channel, pass a resolver instead: `defineComposioTools((ctx) => sessionFor(ctx.session.auth.current?.principalId))`. Your agent now has the Tool Router meta-tools (`COMPOSIO_SEARCH_TOOLS`, `COMPOSIO_MULTI_EXECUTE_TOOL`, `COMPOSIO_MANAGE_CONNECTIONS`) plus any preloaded custom toolkits, with auth handled in chat. > eve's `defineTool` takes plain JSON Schema, so `EveProvider` passes Composio's `inputParameters` straight through. It does not convert to a zod schema, which would trip eve's dynamic-tool normalizer. # Hooks Pass a `hooks` object to the constructor to wrap the Tool Router meta-tools. Each hook is `(ctx, next)`: `await next()` runs the default behavior, returning a value replaces what the model sees, and `ctx.deny(reason)` blocks the call. `ctx.request` is mutable; `ctx.context` is read-only. ```typescript // @noErrors import { EveProvider } from '@composio/experimental/eve'; const provider = new EveProvider({ hooks: { search: (ctx, next) => { ctx.request.args.toolkits = ['github']; return next(); }, remoteBash: async (ctx, next) => { if (String(ctx.request.args.command ?? '').includes('rm -rf')) { return ctx.deny('Destructive commands are blocked.'); } return next(); }, onAuthLink: async (ctx, next) => { await sendConnectionLinkToUser(ctx.url); return next(); }, }, }); ``` Available hooks, each keyed on `EveProviderHooks`: | Hook | Wraps | | ------------------- | --------------------------------------------------------- | | `search` | `COMPOSIO_SEARCH_TOOLS` | | `manageConnections` | `COMPOSIO_MANAGE_CONNECTIONS` | | `execute` | `COMPOSIO_MULTI_EXECUTE_TOOL` and `COMPOSIO_EXECUTE_TOOL` | | `remoteWorkbench` | `COMPOSIO_REMOTE_WORKBENCH` | | `remoteBash` | `COMPOSIO_REMOTE_BASH_TOOL` | | `onAuthLink` | Any auth link found in a result | `ctx.request` carries the meta-tool's raw `{ slug, args }`, and `ctx.deny` is also exported as `denyEveToolCall(reason)`. ## Require approval Map Composio tools onto eve's durable approval flow with `needsApproval`. The callback receives the original Composio tool plus eve's approval context: ```typescript // @noErrors import { EveProvider, requireApprovalForTools } from '@composio/experimental/eve'; const provider = new EveProvider({ needsApproval: requireApprovalForTools('LOCAL_IMESSAGE_SEND'), }); ``` When this returns `true`, eve pauses before execution and asks the user to approve the call. `requireApprovalForTools` protects both direct calls and matching entries inside `COMPOSIO_MULTI_EXECUTE_TOOL`. Use an exact slug allowlist for side-effecting tools rather than approving every tool in a toolkit. # Next - [iMessage on eve](/examples/imessage-agent): A full example: a custom toolkit for local iMessage plus the eve provider, on one session. - [What is a session?](/docs/how-composio-works): How sessions scope users, tools, and auth, and how to reuse them across requests. --- # Google (/docs/providers/google) The Google provider formats Composio tools for [Gemini](https://ai.google.dev/) and the [Google Agent Development Kit (ADK)](https://google.github.io/adk-docs/). Pick the tab that matches your setup. > Choose your integration type · [Use this guide to decide](/docs/native-tools-vs-mcp) ### Gemini In Python, the Gemini provider (`composio_gemini`) wraps Composio tools as typed callables, and the `google-genai` SDK's Automatic Function Calling executes tool calls inside the chat loop for you. In TypeScript, the Google provider transforms Composio tools into Gemini function declarations, and you run the loop: execute each call with `composio.provider.executeToolCall`, feed the result back, and repeat until the model replies with text. **Install** **Python:** **TypeScript:** **Configure API Keys** > Set `COMPOSIO_API_KEY` with your API key from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys) and `GOOGLE_API_KEY` with your [Google API key](https://aistudio.google.com/apikey). ```txt title=".env" COMPOSIO_API_KEY=xxxxxxxxx GOOGLE_API_KEY=xxxxxxxxx ``` **Create session and run** **Python:** ```python from composio import Composio from composio_gemini import GeminiProvider from google import genai from google.genai import types composio = Composio(provider=GeminiProvider()) client = genai.Client() # Create a session for your user session = composio.create(user_id="user_123") tools = session.tools() config = types.GenerateContentConfig(tools=tools) chat = client.chats.create(model="gemini-3-pro-preview", config=config) # Automatic Function Calling executes tool calls inside the chat loop response = chat.send_message( "Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'" ) print(response.text) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; import { GoogleProvider } from '@composio/google'; import { GoogleGenAI, type Part } from '@google/genai'; const composio = new Composio({ provider: new GoogleProvider(), }); const ai = new GoogleGenAI({ apiKey: process.env.GOOGLE_API_KEY! }); // Create a session for your user const session = await composio.create("user_123"); const tools = await session.tools(); const chat = ai.chats.create({ model: 'gemini-3-pro-preview', config: { tools: [{ functionDeclarations: tools }], }, }); let response = await chat.sendMessage({ message: "Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'", }); // Agentic loop: keep executing tool calls until the model responds with text while (response.functionCalls && response.functionCalls.length > 0) { const parts: Part[] = []; for (const fc of response.functionCalls) { const result = await composio.provider.executeToolCall("user_123", { name: fc.name || '', args: (fc.args || {}) as Record, }); parts.push({ functionResponse: { id: fc.id, name: fc.name, response: JSON.parse(result), }, }); } response = await chat.sendMessage({ message: parts }); } console.log(response.text); ``` ### ADK The Google ADK provider transforms Composio tools into ADK's `FunctionTool` format. Unlike Gemini function calling, ADK runs the tool loop for you: hand the tools to an `Agent`, and the `Runner` executes calls and continues until the agent produces a final response. ADK integration is Python-only. **Install** **Configure API Keys** > Set `COMPOSIO_API_KEY` with your API key from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys) and `GOOGLE_API_KEY` with your [Google API key](https://aistudio.google.com/apikey). ```txt title=".env" COMPOSIO_API_KEY=xxxxxxxxx GOOGLE_API_KEY=xxxxxxxxx ``` **Create session and run** ```python from composio import Composio from composio_google_adk import GoogleAdkProvider from google.adk.agents import Agent from google.adk.runners import Runner from google.adk.sessions import InMemorySessionService from google.genai import types composio = Composio(provider=GoogleAdkProvider()) # Create a session for your user session = composio.create(user_id="user_123") tools = session.tools() agent = Agent( name="email_agent", model="gemini-3-pro-preview", instruction="You are an AI agent that sends emails using Gmail.", tools=tools, ) session_service = InMemorySessionService() adk_session = session_service.create_session_sync( app_name="email_agent", user_id="user_123", session_id="session_1", ) runner = Runner( agent=agent, app_name="email_agent", session_service=session_service, ) content = types.Content( role="user", parts=[types.Part(text="Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'")], ) events = runner.run(user_id="user_123", session_id="session_1", new_message=content) for event in events: if event.is_final_response() and event.content and event.content.parts: print(event.content.parts[0].text) ``` # Next - [What is a session?](/docs/how-composio-works): How sessions scope users, tools, and auth, and how to reuse them across requests. --- # LangChain (/docs/providers/langchain) The LangChain provider formats Composio tools for [LangChain](https://python.langchain.com/) and [LangGraph](https://langchain-ai.github.io/langgraph/) agents. Pick the tab that matches your setup. > Choose your integration type · [Use this guide to decide](/docs/native-tools-vs-mcp) ### LangChain The LangChain provider transforms each Composio tool into a LangChain [`DynamicStructuredTool`](https://js.langchain.com/docs/concepts/tools/) with built-in execution. You can hand the tools to `create_agent` in Python or wire them into a graph node in TypeScript, and the framework runs the tool loop for you. **Install** **Python:** **TypeScript:** **Configure API Keys** > Set `COMPOSIO_API_KEY` with your API key from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys) and `OPENAI_API_KEY` with your [OpenAI API key](https://platform.openai.com/api-keys). ```txt title=".env" COMPOSIO_API_KEY=xxxxxxxxx OPENAI_API_KEY=xxxxxxxxx ``` **Create session and run** **Python:** ```python from composio import Composio from composio_langchain import LangchainProvider from langchain.agents import create_agent from langchain_openai import ChatOpenAI composio = Composio(provider=LangchainProvider()) llm = ChatOpenAI(model="gpt-5.2") # Create a session for your user session = composio.create(user_id="user_123") tools = session.tools() agent = create_agent(tools=tools, model=llm) result = agent.invoke({"messages": [("user", "Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'")]}) print(result["messages"][-1].content) ``` **TypeScript:** ```typescript import { ChatOpenAI } from '@langchain/openai'; import { HumanMessage, AIMessage } from '@langchain/core/messages'; import { ToolNode } from '@langchain/langgraph/prebuilt'; import { StateGraph, MessagesAnnotation } from '@langchain/langgraph'; import { Composio } from '@composio/core'; import { LangchainProvider } from '@composio/langchain'; const composio = new Composio({ provider: new LangchainProvider(), }); // Create a session for your user const session = await composio.create("user_123"); const tools = await session.tools(); const toolNode = new ToolNode(tools); const model = new ChatOpenAI({ model: 'gpt-5.2', temperature: 0, }).bindTools(tools); function shouldContinue({ messages }: typeof MessagesAnnotation.State) { const lastMessage = messages[messages.length - 1] as AIMessage; if (lastMessage.tool_calls?.length) { return 'tools'; } return '__end__'; } async function callModel(state: typeof MessagesAnnotation.State) { const response = await model.invoke(state.messages); return { messages: [response] }; } const workflow = new StateGraph(MessagesAnnotation) .addNode('agent', callModel) .addEdge('__start__', 'agent') .addNode('tools', toolNode) .addEdge('tools', 'agent') .addConditionalEdges('agent', shouldContinue); const app = workflow.compile(); const finalState = await app.invoke({ messages: [new HumanMessage("Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'")], }); console.log(finalState.messages[finalState.messages.length - 1].content); ``` ### LangGraph The LangGraph provider transforms Composio tools into the same LangChain `DynamicStructuredTool` format, ready to use with LangGraph agents. LangGraph integration is Python-only. **Install** **Configure API Keys** > Set `COMPOSIO_API_KEY` with your API key from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys) and `OPENAI_API_KEY` with your [OpenAI API key](https://platform.openai.com/api-keys). ```txt title=".env" COMPOSIO_API_KEY=xxxxxxxxx OPENAI_API_KEY=xxxxxxxxx ``` **Create session and run** ```python from composio import Composio from composio_langgraph import LanggraphProvider from langchain.agents import create_agent from langchain_openai import ChatOpenAI composio = Composio(provider=LanggraphProvider()) llm = ChatOpenAI(model="gpt-5.2") # Create a session for your user session = composio.create(user_id="user_123") tools = session.tools() agent = create_agent(tools=tools, model=llm) result = agent.invoke({"messages": [("user", "Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'")]}) print(result["messages"][-1].content) ``` # Next - [What is a session?](/docs/how-composio-works): How sessions scope users, tools, and auth, and how to reuse them across requests. --- # AutoGen (/docs/providers/autogen) The AutoGen provider turns Composio tools into AutoGen [`FunctionTool`](https://microsoft.github.io/autogen/) objects and registers them with your agents. You connect an account, fetch the tools, register them with a caller and executor agent, and AutoGen handles the conversation and tool calls. The provider runs on the [`ag2`](https://github.com/ag2ai/ag2) distribution, the community-maintained continuation of AutoGen 0.2, so it works for AG2 projects out of the box. **Install** **Configure API Keys** > Set `COMPOSIO_API_KEY` with your API key from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys) and `OPENAI_API_KEY` with your [OpenAI API key](https://platform.openai.com/api-keys). ```txt title=".env" COMPOSIO_API_KEY=xxxxxxxxx OPENAI_API_KEY=xxxxxxxxx ``` **Create session and run** ```python import os from autogen import AssistantAgent, LLMConfig, UserProxyAgent from composio import Composio from composio_autogen import AutogenProvider composio = Composio(provider=AutogenProvider()) # Create a session for your user session = composio.create(user_id="user_123") tools = session.tools() chatbot = AssistantAgent( "chatbot", system_message="Reply TERMINATE when the task is done or when user's content is empty", llm_config=LLMConfig({ "api_type": "openai", "model": "gpt-5.2", "api_key": os.environ["OPENAI_API_KEY"], }), ) user_proxy = UserProxyAgent( "user_proxy", is_termination_msg=lambda msg: "TERMINATE" in (msg.get("content", "") or ""), human_input_mode="NEVER", code_execution_config={"use_docker": False}, ) # Register tools with both agents composio.provider.register_tools(caller=chatbot, executor=user_proxy, tools=tools) response = user_proxy.initiate_chat( chatbot, message="Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'", ) print(response.chat_history) ``` # Provider specifics AutoGen needs tools registered with two agents, not passed once. Call `composio.provider.register_tools(caller=..., executor=..., tools=tools)`: the `caller` decides which tool to invoke, and the `executor` runs it. Each tool comes back as an AutoGen `FunctionTool` with a generated `name`. AutoGen caps function names at 64 characters, so the provider hashes and truncates long tool slugs to stay under the limit. The registered name will not always match the original Composio slug. > `register_tools` is unique to the AutoGen provider. Other providers pass tools straight into the agent constructor, so don't expect this method elsewhere. # Next - [What is a session?](/docs/how-composio-works): How sessions scope users, tools, and auth, and how to reuse them across requests. --- # CrewAI (/docs/providers/crewai) The CrewAI provider turns Composio tools into CrewAI [`BaseTool`](https://docs.crewai.com/concepts/tools) objects that execute themselves. You connect an account, fetch the tools, pass them to an `Agent`, and CrewAI runs the task end to end. **Install** **Configure API Keys** > Set `COMPOSIO_API_KEY` with your API key from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys) and `OPENAI_API_KEY` with your [OpenAI API key](https://platform.openai.com/api-keys). ```txt title=".env" COMPOSIO_API_KEY=xxxxxxxxx OPENAI_API_KEY=xxxxxxxxx ``` **Create session and run** ```python from crewai import Agent, Crew, Task from composio import Composio from composio_crewai import CrewAIProvider composio = Composio(provider=CrewAIProvider()) # Create a session for your user session = composio.create(user_id="user_123") tools = session.tools() agent = Agent( role="Email Agent", goal="Send emails on behalf of the user", backstory="You are an AI agent that sends emails using Gmail.", tools=tools, llm="gpt-5.2", ) task = Task( description="Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'", agent=agent, expected_output="Confirmation that the email was sent", ) crew = Crew(agents=[agent], tasks=[task]) result = crew.kickoff() print(result) ``` # Provider specifics Each Composio tool becomes a CrewAI `BaseTool` whose `args_schema` is built from the tool's input schema, so CrewAI validates arguments before running anything. When validation fails, the tool does not raise. It returns a structured result instead: ```python {"successful": False, "error": "", "data": None} ``` Check `successful` in your task output rather than wrapping calls in `try`/`except`. # Next - [What is a session?](/docs/how-composio-works): How sessions scope users, tools, and auth, and how to reuse them across requests. --- # LlamaIndex (/docs/providers/llamaindex) The LlamaIndex provider turns Composio tools into LlamaIndex [`FunctionTool`](https://docs.llamaindex.ai/en/stable/module_guides/deploying/agents/) objects that execute themselves. You connect an account, fetch the tools, hand them to a `FunctionAgent`, and LlamaIndex drives the calls. The provider ships for both Python and TypeScript. **Install** **Python:** **TypeScript:** **Configure API Keys** > Set `COMPOSIO_API_KEY` with your API key from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys) and `OPENAI_API_KEY` with your [OpenAI API key](https://platform.openai.com/api-keys). ```txt title=".env" COMPOSIO_API_KEY=xxxxxxxxx OPENAI_API_KEY=xxxxxxxxx ``` **Create session and run** **Python:** ```python import asyncio from composio import Composio from composio_llamaindex import LlamaIndexProvider from llama_index.core.agent.workflow import FunctionAgent from llama_index.llms.openai import OpenAI composio = Composio(provider=LlamaIndexProvider()) llm = OpenAI(model="gpt-5.2") # Create a session for your user session = composio.create(user_id="user_123") tools = session.tools() agent = FunctionAgent(tools=tools, llm=llm) async def main(): result = await agent.run( user_msg="Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'" ) print(result) asyncio.run(main()) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; import { LlamaindexProvider } from '@composio/llamaindex'; import { openai } from '@llamaindex/openai'; import { agent } from '@llamaindex/workflow'; const composio = new Composio({ provider: new LlamaindexProvider(), }); // Create a session for your user const session = await composio.create("user_123"); const tools = await session.tools(); const myAgent = agent({ llm: openai({ model: 'gpt-5.2' }), tools, }); const result = await myAgent.run( "Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'" ); console.log(result.data.result); ``` # Next - [What is a session?](/docs/how-composio-works): How sessions scope users, tools, and auth, and how to reuse them across requests. --- # Mastra (/docs/providers/mastra) The Mastra provider transforms Composio tools into [Mastra's tool format](https://mastra.ai/en/docs/tools-mcp/overview#creating-tools) with built-in execution. Pass the wrapped tools to a Mastra `Agent`, and the agent calls them automatically. Each tool gets both an input and an output schema, so Mastra can validate tool results as well as arguments. **Install** **Configure API Keys** > Set `COMPOSIO_API_KEY` with your API key from [Settings](https://dashboard.composio.dev/~/project/settings/api-keys) and `OPENAI_API_KEY` with your [OpenAI API key](https://platform.openai.com/api-keys). ```txt title=".env" COMPOSIO_API_KEY=xxxxxxxxx OPENAI_API_KEY=xxxxxxxxx ``` **Create session and run** ```typescript import { Composio } from "@composio/core"; import { MastraProvider } from "@composio/mastra"; import { Agent } from "@mastra/core/agent"; import { openai } from "@ai-sdk/openai"; const composio = new Composio({ provider: new MastraProvider(), }); // Create a session for your user const session = await composio.create("user_123"); const tools = await session.tools(); const agent = new Agent({ id: "my-agent", name: "My Agent", instructions: "You are a helpful assistant.", model: openai("gpt-5.2"), tools, }); const { text } = await agent.generate([ { role: "user", content: "Send an email to john@example.com with the subject 'Hello' and body 'Hello from Composio!'" }, ]); console.log(text); ``` # Provider specifics **Strict mode.** Pass `strict: true` to drop every non-required property from each tool's input schema before Mastra compiles it: ```typescript // @noErrors import { Composio } from "@composio/core"; import { MastraProvider } from "@composio/mastra"; const composio = new Composio({ provider: new MastraProvider({ strict: true }) }); ``` > The provider runs each tool's JSON Schema through Mastra's schema-compat layer and inlines internal `$ref` pointers first. A few Composio tools reference `$defs` entries that the upstream API does not emit. Rather than crash `tools.get`, the provider falls back to a permissive object schema for that property and logs one warning per tool, so the affected field validates loosely. # Next - [What is a session?](/docs/how-composio-works): How sessions scope users, tools, and auth, and how to reuse them across requests. --- # Custom Providers (/docs/providers/custom-providers) Providers transform Composio tools into the format your AI framework expects. If your framework isn't listed in our supported providers, you can build your own. } /> } /> --- # TypeScript Custom Provider (/docs/providers/custom-providers/typescript) A **provider** adapts Composio tools to the format your AI framework expects. Write one, and any framework can call Composio's 1000+ tools. This guide shows you how to build your own in TypeScript. # Provider architecture A provider does three things: * **Transforms tool format**: converts Composio tools into the shape your AI platform expects. * **Executes tools**: runs tool calls and returns results. * **Adds platform helpers**: exposes convenience methods specific to your platform. There are two kinds, depending on whether the target platform runs its own agent loop: | Type | When to use | Examples | | --------------- | ------------------------------------------------------------ | ------------------ | | **Non-agentic** | The platform has no agency of its own. You drive the loop. | OpenAI | | **Agentic** | The platform runs its own agent loop and calls tools itself. | LangChain, AutoGPT | Both extend `BaseProvider`: ``` BaseProvider (Abstract) ├── BaseNonAgenticProvider (Abstract) │ └── OpenAIProvider (Concrete) │ └── [Your Custom Non-Agentic Provider] (Concrete) └── BaseAgenticProvider (Abstract) └── [Your Custom Agentic Provider] (Concrete) ``` # Non-agentic provider A non-agentic provider extends `BaseNonAgenticProvider`. You supply a `name`, `wrapTool`, and `wrapTools`, and call the built-in `executeTool` when you're ready to run a tool. ```typescript import { BaseNonAgenticProvider, Tool } from '@composio/core'; // Define your tool format interface MyAITool { name: string; description: string; parameters: { type: string; properties: Record; required?: string[]; }; } // Define your tool collection format type MyAIToolCollection = MyAITool[]; // Create your provider export class MyAIProvider extends BaseNonAgenticProvider { // Required: Unique provider name for telemetry readonly name = 'my-ai-platform'; // Required: Method to transform a single tool override wrapTool(tool: Tool): MyAITool { return { name: tool.slug, description: tool.description || '', parameters: { type: 'object', properties: tool.inputParameters?.properties || {}, required: tool.inputParameters?.required || [], }, }; } // Required: Method to transform a collection of tools override wrapTools(tools: Tool[]): MyAIToolCollection { return tools.map(tool => this.wrapTool(tool)); } // Optional: Custom helper methods for your AI platform async executeMyAIToolCall( userId: string, toolCall: { name: string; arguments: Record; } ): Promise { // Use the built-in executeTool method const result = await this.executeTool(toolCall.name, { userId, arguments: toolCall.arguments, }); return JSON.stringify(result.data); } } ``` # Agentic provider An **agentic provider** extends `BaseAgenticProvider`. The difference from the non-agentic case: `wrapTool` and `wrapTools` receive an `executeToolFn`, which you embed in each tool so the framework's agent can run the tool itself. ```typescript import { BaseAgenticProvider, Tool, ExecuteToolFn } from '@composio/core'; // Define your tool format interface AgentTool { name: string; description: string; execute: (args: Record) => Promise; schema: Record; } // Define your tool collection format interface AgentToolkit { tools: AgentTool[]; createAgent: (config: Record) => unknown; } // Create your provider export class MyAgentProvider extends BaseAgenticProvider { // Required: Unique provider name for telemetry readonly name = 'my-agent-platform'; // Required: Method to transform a single tool with execute function override wrapTool(tool: Tool, executeToolFn: ExecuteToolFn): AgentTool { return { name: tool.slug, description: tool.description || '', schema: tool.inputParameters || {}, execute: async (args: Record) => { const result = await executeToolFn(tool.slug, args); if (!result.successful) { throw new Error(result.error || 'Tool execution failed'); } return result.data; }, }; } // Required: Method to transform a collection of tools with execute function override wrapTools(tools: Tool[], executeToolFn: ExecuteToolFn): AgentToolkit { const agentTools = tools.map(tool => this.wrapTool(tool, executeToolFn)); return { tools: agentTools, createAgent: config => { // Create an agent using the tools return { run: async (prompt: string) => { // Implementation depends on your agent framework console.log(`Running agent with prompt: ${prompt}`); // The agent would use the tools.execute method to run tools }, }; }, }; } // Optional: Custom helper methods for your agent platform async runAgent(agentToolkit: AgentToolkit, prompt: string): Promise { const agent = agentToolkit.createAgent({}); return await agent.run(prompt); } } ``` # Use your provider Pass an instance to `Composio` via the `provider` option. Every tool you fetch comes back in your custom format. ```typescript import { Composio } from '@composio/core'; import { MyAIProvider } from './my-ai-provider'; // Create your provider instance const myProvider = new MyAIProvider(); // Initialize Composio with your provider const composio = new Composio({ apiKey: 'your-composio-api-key', provider: myProvider, }); // Get tools - they will be transformed by your provider const tools = await composio.tools.get('default', { toolkits: ['github'], }); // Use the tools with your AI platform console.log(tools); // These will be in your custom format ``` # Provider state and context A provider is a class, so it can hold state. Use the constructor for config, and instance fields for caches or counters. ```typescript export class StatefulProvider extends BaseNonAgenticProvider { readonly name = 'stateful-provider'; // Provider state private requestCount = 0; private toolCache = new Map(); private config: ProviderConfig; constructor(config: ProviderConfig) { super(); this.config = config; } override wrapTool(tool: Tool): ProviderTool { this.requestCount++; // Use the provider state/config const enhancedTool = { // Transform the tool name: this.config.useUpperCase ? tool.slug.toUpperCase() : tool.slug, description: tool.description, schema: tool.inputParameters, }; // Cache the transformed tool this.toolCache.set(tool.slug, enhancedTool); return enhancedTool; } override wrapTools(tools: Tool[]): ProviderToolCollection { return tools.map(tool => this.wrapTool(tool)); } // Custom methods that use provider state getRequestCount(): number { return this.requestCount; } getCachedTool(slug: string): ProviderTool | undefined { return this.toolCache.get(slug); } } ``` # Advanced: compose providers You don't have to start from a base class. Extend an existing provider to add behavior like analytics or retries, and call `super` to reuse its logic. ```typescript import { OpenAIProvider } from '@composio/openai'; // Extend the OpenAI provider with custom functionality export class EnhancedOpenAIProvider extends OpenAIProvider { // Add properties private analytics = { toolCalls: 0, errors: 0, }; // Override methods to add functionality override async executeToolCall(userId, tool, options, modifiers) { this.analytics.toolCalls++; try { // Call the parent implementation const result = await super.executeToolCall(userId, tool, options, modifiers); return result; } catch (error) { this.analytics.errors++; throw error; } } // Add new methods getAnalytics() { return this.analytics; } async executeWithRetry(userId, tool, options, modifiers, maxRetries = 3) { let attempts = 0; let lastError; while (attempts < maxRetries) { try { return await this.executeToolCall(userId, tool, options, modifiers); } catch (error) { lastError = error; attempts++; await new Promise(resolve => setTimeout(resolve, 1000 * attempts)); } } throw lastError; } } ``` # Best practices * **Keep providers focused**: each provider should target one platform. * **Handle errors gracefully**: catch and transform errors from tool execution. * **Follow platform conventions**: adopt the naming and structure of the target platform. * **Cache transformed tools**: reuse wrapped tools instead of rebuilding them. * **Add helper methods**: expose convenience methods for common platform operations. * **Document your provider**: describe its features and usage. * **Set a meaningful `name`**: it's used for telemetry insights. --- # Python Custom Provider (/docs/providers/custom-providers/python) A **custom provider** adapts Composio tools to the format your AI framework expects, so you can use 1000+ tools with a platform Composio doesn't ship support for. This guide shows you how to build one in Python. # Provider architecture A provider does two things: 1. **Tool format transformation**: converts Composio tools into the format your AI platform understands. 2. **Platform-specific compatibility**: adds helper methods for executing tool calls and handling responses. There are two kinds of providers: | Type | Use it for | Examples | | --------------- | ------------------------------------------------------------------------- | ----------------- | | **Non-agentic** | Platforms that don't have their own agency. You drive the tool-call loop. | OpenAI, Anthropic | | **Agentic** | Platforms that run their own agent loop and call tools themselves. | LangChain, CrewAI | Each type extends a different abstract base class: ``` BaseProvider (Abstract) ├── NonAgenticProvider (Abstract) │ └── OpenAIProvider (Concrete) │ └── AnthropicProvider (Concrete) │ └── [Your Custom Non-Agentic Provider] (Concrete) └── AgenticProvider (Abstract) └── LangchainProvider (Concrete) └── [Your Custom Agentic Provider] (Concrete) ``` # Quick start Scaffold a working provider with the `make create-provider` script: ```bash # Create a non-agentic provider make create-provider name=myprovider # Create an agentic provider make create-provider name=myagent agentic=true # Write to a custom output directory make create-provider name=myprovider output=/path/to/custom/dir # Combine options make create-provider name=myagent agentic=true output=/my/custom/path ``` This generates a provider in `python/providers//` (or your `output` directory) with a `pyproject.toml`, a provider template, a demo script, a `README`, and type annotations. The template runs as-is. You implement the tool transformation for your platform, and you can keep your provider in your own repository. The generated structure: ``` python/providers// ├── README.md # Documentation and usage examples ├── pyproject.toml # Project configuration ├── setup.py # Setup script for pip compatibility ├── _demo.py # Demo script showing usage └── composio_/ # Package directory ├── __init__.py # Package initialization ├── provider.py # Provider implementation └── py.typed # PEP 561 type marker ``` From there: 1. Navigate to the provider directory: `cd python/providers/`. 2. Install in development mode: `uv pip install -e .`. 3. Implement your provider logic in `composio_/provider.py`. 4. Test with the demo script: `python _demo.py`. # Creating a non-agentic provider A non-agentic provider extends `NonAgenticProvider` and implements two methods: `wrap_tool` (transform one tool) and `wrap_tools` (transform a collection). Add helper methods to execute tool calls in your platform's format. ```python from typing import List, Optional, Sequence, TypeAlias from composio.core.provider import NonAgenticProvider from composio.types import Tool, Modifiers, ToolExecutionResponse # Define your tool format class MyAITool: def __init__(self, name: str, description: str, parameters: dict): self.name = name self.description = description self.parameters = parameters # Define your tool collection format MyAIToolCollection: TypeAlias = List[MyAITool] # Create your provider class MyAIProvider(NonAgenticProvider[MyAITool, MyAIToolCollection], name="my-ai-platform"): """Custom provider for My AI Platform""" def wrap_tool(self, tool: Tool) -> MyAITool: """Transform a single tool to platform format""" return MyAITool( name=tool.slug, description=tool.description or "", parameters={ "type": "object", "properties": tool.input_parameters.get("properties", {}), "required": tool.input_parameters.get("required", []) } ) def wrap_tools(self, tools: Sequence[Tool]) -> MyAIToolCollection: """Transform a collection of tools""" return [self.wrap_tool(tool) for tool in tools] # Optional: Custom helper methods for your AI platform def execute_my_ai_tool_call( self, user_id: str, tool_call: dict, modifiers: Optional[Modifiers] = None ) -> ToolExecutionResponse: """Execute a tool call in your platform's format Example usage: result = my_provider.execute_my_ai_tool_call( user_id="default", tool_call={"name": "GITHUB_STAR_REPO", "arguments": {"owner": "composiohq", "repo": "composio"}} ) """ # Use the built-in execute_tool method return self.execute_tool( slug=tool_call["name"], arguments=tool_call["arguments"], modifiers=modifiers, user_id=user_id ) ``` # Creating an agentic provider An agentic provider extends `AgenticProvider`. Because the platform calls tools itself, `wrap_tool` receives an `execute_tool` function that you wrap into a callable the framework can invoke directly. ```python from typing import Callable, Dict, List, Sequence from composio.core.provider import AgenticProvider, AgenticProviderExecuteFn from composio.types import Tool from my_provider import AgentTool # Import the Tool/Function class that represents a callable tool for your framework # Optionally define your custom tool format below # class AgentTool: # def __init__(self, name: str, description: str, execute: Callable, schema: dict): # self.name = name # self.description = description # self.execute = execute # self.schema = schema # Define your tool collection format (typically a List) AgentToolCollection: TypeAlias = List[AgentTool] # Create your provider class MyAgentProvider(AgenticProvider[AgentTool, List[AgentTool]], name="my-agent-platform"): """Custom provider for My Agent Platform""" def wrap_tool(self, tool: Tool, execute_tool: AgenticProviderExecuteFn) -> AgentTool: """Transform a single tool with execute function""" def execute_wrapper(**kwargs) -> Dict: result = execute_tool(tool.slug, kwargs) if not result.get("successful", False): raise Exception(result.get("error", "Tool execution failed")) return result.get("data", {}) return AgentTool( name=tool.slug, description=tool.description or "", execute=execute_wrapper, schema=tool.input_parameters ) def wrap_tools( self, tools: Sequence[Tool], execute_tool: AgenticProviderExecuteFn ) -> AgentToolCollection: """Transform a collection of tools with execute function""" return [self.wrap_tool(tool, execute_tool) for tool in tools] ``` # Using your custom provider Pass an instance of your provider to `Composio`, and every tool you fetch comes back in your format. ## Non-agentic provider You drive the loop: fetch tools, send them to the platform, then hand the response back to the provider to execute the calls. ```python from composio import Composio from composio_myai import MyAIProvider from myai import MyAIClient # Your AI platform's client # Initialize tools myai_client = MyAIClient() composio = Composio(provider=MyAIProvider()) # Define task task = "Star a repo composiohq/composio on GitHub" # Get GitHub tools that are pre-configured tools = composio.tools.get(user_id="default", toolkits=["GITHUB"]) # Get response from your AI platform (example) response = myai_client.chat.completions.create( model="your-model", tools=tools, # These are in your platform's format messages=[ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": task}, ], ) print(response) # Execute the function calls result = composio.provider.handle_tool_calls(response=response, user_id="default") print(result) ``` ## Agentic provider The framework runs the loop. Fetch tools, attach them to an agent, and the agent calls them itself. ```python import asyncio from agents import Agent, Runner from composio_myagent import MyAgentProvider from composio import Composio # Initialize Composio toolset composio = Composio(provider=MyAgentProvider()) # Get all the tools tools = composio.tools.get( user_id="default", tools=["GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER"], ) # Create an agent with the tools agent = Agent( name="GitHub Agent", instructions="You are a helpful assistant that helps users with GitHub tasks.", tools=tools, ) # Run the agent async def main(): result = await Runner.run( starting_agent=agent, input=( "Star the repository composiohq/composio on GitHub. If done " "successfully, respond with 'Action executed successfully'" ), ) print(result.final_output) asyncio.run(main()) ``` # Best practices * **Keep providers focused**: each provider integrates with one platform. * **Handle errors gracefully**: catch and transform errors from tool execution. * **Follow platform conventions**: adopt the naming and structure of the target platform. * **Use type annotations**: lean on Python's typing for IDE support and documentation. * **Cache transformed tools**: store transformed tools when it helps performance. * **Add helper methods**: provide convenient methods for common platform operations. * **Document your provider**: include docstrings and usage examples. * **Set a meaningful provider name**: the `name` parameter is used for telemetry and debugging. --- # What is a session? (/docs/how-composio-works) A **session** is the runtime context for an agentic run: the scoped environment an AI agent works in while it acts for one of your users. You create it with `composio.create(userId)`, and it ties together the user, the toolkits available, authentication, and connected accounts. By default it gives the agent meta tools to discover, authenticate, and execute app tools at runtime, instead of loading hundreds of tool definitions into context. # The basics Create a session for a user, then read its tools formatted for your framework. To connect over MCP instead, see [Using sessions via MCP](/docs/sessions-via-mcp). **Python:** ```python session = composio.sessions.create(user_id="user_123") tools = session.tools() ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123"); const tools = await session.tools(); ``` A session scopes four things: * **userID**: whose connected accounts and tool executions are in scope. * **Tool access**: all toolkits by default, or a filtered set of toolkits, tools, or tags. * **Authentication**: managed auth, custom auth configs, and connected-account selection. * **Execution state**: logs, tool memory, MCP state, and workbench files for the task. # Tools and toolkits A **toolkit** is a collection of related tools for a service. The `github` toolkit, for example, contains tools for creating issues, managing pull requests, and starring repositories. A **tool** is an individual action your agent can execute. Each tool has an input schema (its parameters) and an output schema (what it returns), and follows a `{TOOLKIT}_{ACTION}` naming pattern, like `GITHUB_CREATE_ISSUE`. Every toolkit in the catalog is discoverable by default. Create a session without a `toolkits` parameter and the agent can find any of them at runtime. To restrict the set, pass `toolkits` when you create the session. See [Enable and disable toolkits](/docs/configuring-sessions). You can also bind local, in-process tools to a session with the experimental [custom tools and toolkits](/docs/extending-sessions/custom-tools-and-toolkits) API. # Users A user is an identifier from your app. Composio stores connections under that ID, so tools run with the right account and stay isolated from other users. Use a stable identifier like your database ID, never one that can change. **userID best practices** * **Recommended:** database UUID or primary key (`user.id`) * **Acceptable:** unique username (`user.username`) * **Avoid:** email addresses (they can change) * **Never:** `default` in production (it exposes other users' data) A user can connect multiple accounts for the same toolkit, like work and personal Gmail. Use the same userID, then select the connected account when a session needs a specific one. See [Managing multiple connected accounts](/docs/managing-multiple-connected-accounts). # Meta tools A session gives your agent meta tools, a small fixed set that discover, authenticate, and execute tools at runtime, so you never load hundreds of tool definitions into context: The agent searches for relevant tools, authenticates if needed, and executes them through the same session. Meta-tool calls share context, so the agent searches in one call and executes in the next without losing state. See the [Meta Tools reference](/toolkits/meta-tools) for each tool's input and output schema. Know the exact tools upfront? The [direct tools preset](/docs/configuring-sessions#direct-tools-preset) returns them directly from `session.tools()` with no search step, while keeping session auth, connected accounts, and the workbench. # Authentication When a tool needs a connection, the session generates a Connect Link with `session.authorize()`, or the agent handles the flow through `COMPOSIO_MANAGE_CONNECTIONS`. In chat, the agent can pause, ask the user to connect an app, then retry the tool once auth completes. Composio manages the OAuth redirects, token exchange, and refresh. Once a user connects a toolkit, the connected account persists and future sessions reuse it without re-authentication. For OAuth toolkits, Composio uses [managed apps](/docs/custom-app-vs-managed-app) by default. Bring your own app when you need your own branding, scopes, or consent screen. # Sandbox Handle large responses and bulk operations in the remote sandbox. Instead of stuffing long tool responses into the model context, the agent reads files, searches outputs, writes Python, transforms data, and calls Composio tools in bulk. The sandbox is scoped to the session, so files, variables, helper functions, and intermediate results stay available while the agent works through a task. # How sessions behave Every `create()` call returns a new session ID. Use it for a fresh task context. Sessions persist on the server and don't expire. For multi-turn conversations, store the session ID and reuse it with `composio.use()` instead of calling `create()` again. **Python:** ```python session = composio.use("session_id") tools = session.tools() ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.use("session_id"); const tools = await session.tools(); ``` You can also update a session in place instead of creating a new one: **Python:** ```python session.update( toolkits=["gmail", "slack"], auth_configs={"gmail": "ac_new_config"}, connected_accounts={"slack": ["ca_work_slack"]}, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.use("session_id"); await session.update({ toolkits: ["gmail", "slack"], authConfigs: { gmail: "ac_new_config" }, connectedAccounts: { slack: ["ca_work_slack"] }, }); ``` Create a new session for a different user or a fundamentally different task setup. Reuse or update a session when the same conversation should keep its tool, auth, and workbench context. # Next - [Configuring Sessions](/docs/configuring-sessions): Enable toolkits, set auth configs, and select connected accounts --- # Configuring Sessions (/docs/configuring-sessions) # Creating a session **Python:** ```python session = composio.sessions.create(user_id="user_123") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123"); ``` By default, a session has access to every toolkit in the Composio catalog. Your agent can discover and use any of them through `COMPOSIO_SEARCH_TOOLS`. Use the options below to restrict or customize what's available. You can also attach local custom tools and custom toolkits that run in-process alongside Composio tools. See [Custom tools and toolkits](/docs/extending-sessions/custom-tools-and-toolkits). # Enabling toolkits To limit a session to specific toolkits, pass an array of toolkit slugs. The agent can only discover and use tools from these toolkits. **Python:** ```python # Using array format session = composio.sessions.create( user_id="user_123", toolkits=["github", "gmail", "slack"] ) # Using object format with enable key session = composio.sessions.create( user_id="user_123", toolkits={"enable": ["github", "gmail", "slack"]} ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); // Using array format const session = await composio.create("user_123", { toolkits: ["github", "gmail", "slack"], }); // Using object format with enable key const session2 = await composio.create("user_123", { toolkits: { enable: ["github", "gmail", "slack"] }, }); ``` # Disabling toolkits To keep every toolkit discoverable except a few, use the `disable` syntax. This is useful when you want broad access but need to exclude specific toolkits. **Python:** ```python session = composio.sessions.create( user_id="user_123", toolkits={"disable": ["exa", "firecrawl"]} ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { toolkits: { disable: ["exa", "firecrawl"] }, }); ``` # Direct tools preset The direct tools preset preloads every tool allowed by session filters into the session's tool list and disables session meta tools by default. Use it for specialized agents with a narrow tool set that don't need dynamic tool discovery, in-chat auth, or workbench helpers. This is not the default mode for broad agents. The default session behavior keeps meta tools available so the agent can search for relevant tools and avoid context bloat. **Python:** ```python from composio import Composio, SESSION_PRESET_DIRECT_TOOLS from composio_openai_agents import OpenAIAgentsProvider composio = Composio( api_key="your_api_key", provider=OpenAIAgentsProvider(), ) session = composio.sessions.create( user_id="user_123", toolkits=["gmail"], tools={ "gmail": { "enable": [ "GMAIL_FETCH_EMAILS", "GMAIL_CREATE_EMAIL_DRAFT", ], }, }, session_preset=SESSION_PRESET_DIRECT_TOOLS, ) tools = session.tools() print([tool.name for tool in tools]) # GMAIL_FETCH_EMAILS # GMAIL_CREATE_EMAIL_DRAFT ``` **TypeScript:** ```typescript import { Composio, SessionPreset } from '@composio/core'; import { OpenAIAgentsProvider } from '@composio/openai-agents'; const composio = new Composio({ apiKey: 'your_api_key', provider: new OpenAIAgentsProvider(), }); const session = await composio.create("user_123", { toolkits: ["gmail"], tools: { gmail: { enable: ["GMAIL_FETCH_EMAILS", "GMAIL_CREATE_EMAIL_DRAFT"], }, }, sessionPreset: SessionPreset.DIRECT_TOOLS, }); const tools = await session.tools(); console.log(tools.map((tool) => tool.name)); // GMAIL_FETCH_EMAILS // GMAIL_CREATE_EMAIL_DRAFT ``` ## Enable selected meta tools With the direct tools preset, you can re-enable supported meta tool groups that your agent still needs. This session loads Gmail tools upfront while keeping connection management and workbench support available: **Python:** ```python from composio import Composio, SESSION_PRESET_DIRECT_TOOLS from composio_openai_agents import OpenAIAgentsProvider composio = Composio( api_key="your_api_key", provider=OpenAIAgentsProvider(), ) session = composio.sessions.create( user_id="user_123", toolkits=["gmail"], tools={ "gmail": { "enable": [ "GMAIL_FETCH_EMAILS", "GMAIL_CREATE_EMAIL_DRAFT", ], }, }, session_preset=SESSION_PRESET_DIRECT_TOOLS, manage_connections={ "enable": True, }, sandbox={ "enable": True, }, ) tools = session.tools() print([tool.name for tool in tools]) # GMAIL_FETCH_EMAILS # GMAIL_CREATE_EMAIL_DRAFT # COMPOSIO_MANAGE_CONNECTIONS # COMPOSIO_REMOTE_WORKBENCH # COMPOSIO_REMOTE_BASH_TOOL ``` **TypeScript:** ```typescript import { Composio, SessionPreset } from '@composio/core'; import { OpenAIAgentsProvider } from '@composio/openai-agents'; const composio = new Composio({ apiKey: 'your_api_key', provider: new OpenAIAgentsProvider(), }); const session = await composio.create("user_123", { toolkits: ["gmail"], tools: { gmail: { enable: ["GMAIL_FETCH_EMAILS", "GMAIL_CREATE_EMAIL_DRAFT"], }, }, sessionPreset: SessionPreset.DIRECT_TOOLS, manageConnections: { enable: true, }, sandbox: { enable: true, }, }); const tools = await session.tools(); console.log(tools.map((tool) => tool.name)); // GMAIL_FETCH_EMAILS // GMAIL_CREATE_EMAIL_DRAFT // COMPOSIO_MANAGE_CONNECTIONS // COMPOSIO_REMOTE_WORKBENCH // COMPOSIO_REMOTE_BASH_TOOL ``` # Enabling or disabling specific tools To control which individual tools are available within a toolkit, use the `tools` configuration. The key is the toolkit slug and the value specifies which tools to enable or disable. To enable only specific tools, pass an `enable` list per toolkit: **Python:** ```python session = composio.sessions.create( user_id="user_123", tools={ # Only these Gmail tools will be available "gmail": {"enable": ["GMAIL_SEND_EMAIL", "GMAIL_FETCH_EMAILS"]}, # Only issue-related GitHub tools "github": {"enable": ["GITHUB_CREATE_ISSUE", "GITHUB_GET_ISSUE"]} } ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { tools: { // Only these Gmail tools will be available gmail: { enable: ["GMAIL_SEND_EMAIL", "GMAIL_FETCH_EMAILS"] }, // Only issue-related GitHub tools github: { enable: ["GITHUB_CREATE_ISSUE", "GITHUB_GET_ISSUE"] } } }); ``` The shorthand array syntax is equivalent to `enable`: **Python:** ```python session = composio.sessions.create( user_id="user_123", tools={ "gmail": ["GMAIL_SEND_EMAIL", "GMAIL_FETCH_EMAILS"], "github": ["GITHUB_CREATE_ISSUE", "GITHUB_GET_ISSUE"] } ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { tools: { gmail: ["GMAIL_SEND_EMAIL", "GMAIL_FETCH_EMAILS"], github: ["GITHUB_CREATE_ISSUE", "GITHUB_GET_ISSUE"] } }); ``` To keep every tool in a toolkit except a few, use `disable`: **Python:** ```python session = composio.sessions.create( user_id="user_123", tools={ # All Slack tools except delete "slack": {"disable": ["SLACK_DELETE_MESSAGE"]}, # All GitHub tools except destructive ones "github": {"disable": ["GITHUB_DELETE_REPO", "GITHUB_DELETE_BRANCH"]} } ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { tools: { // All Slack tools except delete slack: { disable: ["SLACK_DELETE_MESSAGE"] }, // All GitHub tools except destructive ones github: { disable: ["GITHUB_DELETE_REPO", "GITHUB_DELETE_BRANCH"] } } }); ``` # Filtering tools by tags Tools carry behavior tags that you can filter on. The available tags are: | Tag | Description | | ----------------- | ------------------------------------------- | | `readOnlyHint` | Tools that only read data | | `destructiveHint` | Tools that modify or delete data | | `idempotentHint` | Tools that can be safely retried | | `openWorldHint` | Tools that operate in an open world context | To apply tag filters across all toolkits, pass `tags` at the session level: **Python:** ```python # Only include read-only and idempotent tools session = composio.sessions.create( user_id="user_123", tags=["readOnlyHint", "idempotentHint"] ) # Enable some tags, disable others session = composio.sessions.create( user_id="user_123", tags={ "enable": ["readOnlyHint"], "disable": ["destructiveHint"] } ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); // Only include read-only and idempotent tools const session = await composio.create("user_123", { tags: ["readOnlyHint", "idempotentHint"] }); // Enable some tags, disable others const sessionWithTagConfig = await composio.create("user_123", { tags: { enable: ["readOnlyHint"], disable: ["destructiveHint"] } }); ``` To override the global tags for a specific toolkit, set `tags` inside that toolkit's `tools` config: **Python:** ```python session = composio.sessions.create( user_id="user_123", # Global: only read-only tools tags=["readOnlyHint"], tools={ # Override for GitHub: allow all tools except destructive "github": {"tags": {"disable": ["destructiveHint"]}}, # Override for Gmail: only read-only tools (explicit) "gmail": {"tags": ["readOnlyHint"]} } ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { // Global: only read-only tools tags: ["readOnlyHint"], tools: { // Override for GitHub: allow all tools except destructive github: { tags: { disable: ["destructiveHint"] } }, // Override for Gmail: only read-only tools (explicit) gmail: { tags: ["readOnlyHint"] } } }); ``` # Preloading tools Return a known set of tools directly from `session.tools()` and the session MCP tool list, without the agent searching for them first. By default, sessions expose [meta tools](/toolkits/meta-tools) that let the agent discover app tools at runtime. Use `preload.tools` when you already know which tools the agent needs, so it can call them without going through search each time. Keep the preloaded set small, generally fewer than 20 tools, to avoid context bloat. > Requires `@composio/core` ≥ `0.9.0` (TypeScript) or `composio` ≥ `0.13.0` (Python). Older SDKs do not support `preload.tools`, `sessionPreset` / `session_preset`, or custom-tool `preload`. > `preload.tools` is not supported when `multiAccount.enable` is true. See [Managing multiple connected accounts](/docs/managing-multiple-connected-accounts). **Python:** ```python from composio import Composio from composio_openai_agents import OpenAIAgentsProvider composio = Composio( api_key="your_api_key", provider=OpenAIAgentsProvider(), ) session = composio.sessions.create( user_id="user_123", toolkits=["gmail"], preload={ "tools": [ "GMAIL_FETCH_EMAILS", "GMAIL_CREATE_EMAIL_DRAFT", ], }, ) tools = session.tools() print([tool.name for tool in tools]) # GMAIL_FETCH_EMAILS # GMAIL_CREATE_EMAIL_DRAFT # COMPOSIO_SEARCH_TOOLS # ... other default meta tools ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; import { OpenAIAgentsProvider } from '@composio/openai-agents'; const composio = new Composio({ apiKey: 'your_api_key', provider: new OpenAIAgentsProvider(), }); const session = await composio.create("user_123", { toolkits: ["gmail"], preload: { tools: ["GMAIL_FETCH_EMAILS", "GMAIL_CREATE_EMAIL_DRAFT"], }, }); const tools = await session.tools(); console.log(tools.map((tool) => tool.name)); // GMAIL_FETCH_EMAILS // GMAIL_CREATE_EMAIL_DRAFT // COMPOSIO_SEARCH_TOOLS // ... other default meta tools ``` For SDK custom tools, set `preload: true` on the custom tool or custom toolkit. See [Preloading custom tools](/docs/extending-sessions/custom-tools-and-toolkits#preloading-custom-tools). To preload every tool allowed by the session filters, use the `preload.tools = "all"` shortcut (`preload={"tools": "all"}` in Python, `preload: { tools: "all" }` in TypeScript). The `all` shorthand works for both Composio tools and SDK custom tools. # Custom auth configs Use your own OAuth credentials instead of Composio's defaults. Pass an auth config ID per toolkit: **Python:** ```python session = composio.sessions.create( user_id="user_123", auth_configs={ "github": "ac_your_github_config", "slack": "ac_your_slack_config" } ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { authConfigs: { github: "ac_your_github_config", slack: "ac_your_slack_config", }, }); ``` See [White-labeling authentication](/docs/white-labeling-authentication) for branding, or [Managed vs custom auth](/docs/custom-app-vs-managed-app) for toolkits that require your own credentials. # Account selection When a user has multiple connected accounts for the same toolkit, specify which one the session uses: **Python:** ```python session = composio.sessions.create( user_id="user_123", connected_accounts={ "gmail": ["ca_work_gmail"], "github": ["ca_personal_github"], } ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { connectedAccounts: { gmail: ["ca_work_gmail"], github: ["ca_personal_github"], }, }); ``` > Arrays are the preferred format for `connectedAccounts`. A single string (e.g. `"ca_work_gmail"`) is still accepted for backwards compatibility and is automatically coerced to a single-element array. Only one account per toolkit is allowed when [multi-account mode](/docs/managing-multiple-connected-accounts) is disabled. ## Precedence When executing a tool, the session selects the connected account in this order: 1. The `connectedAccounts` override, if provided in the session config. 2. The `authConfigs` override, which finds or creates a connection on that config. 3. An auth config previously created for this toolkit. 4. A new auth config created using Composio managed auth. 5. Otherwise, an error if no Composio managed auth scheme exists for the toolkit. When a user has multiple connected accounts for a toolkit, the session uses the most recently connected one. # Disabling the sandbox By default, sessions include the [sandbox](/docs/sandbox/remote), a persistent environment that provides `COMPOSIO_REMOTE_WORKBENCH` and `COMPOSIO_REMOTE_BASH_TOOL`. If your use case doesn't need code execution, disable it: **Python:** ```python session = composio.sessions.create( user_id="user_123", sandbox={ "enable": False } ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { sandbox: { enable: false, }, }); ``` When disabled: * `COMPOSIO_REMOTE_WORKBENCH` and `COMPOSIO_REMOTE_BASH_TOOL` are excluded from the session * Sandbox-related system prompt lines are stripped * Direct sandbox calls are rejected with a 400 error > `sandbox` is the preferred config key. `workbench` still works as a fully supported alias and isn't deprecated, so existing code keeps running unchanged. # Sandbox compute tier The sandbox runs per session. Pick a compute tier to match the workload: heavier code execution or larger in-memory data benefits from a bigger sandbox. Pass the tier via `sandbox.sandbox_size` (snake\_case on the wire, `sandboxSize` in the TypeScript SDK). > Requires `@composio/core` ≥ `0.8.1` (TypeScript) or `composio` ≥ `0.12.1` (Python). Older SDKs reject `sandboxSize` (TypeScript) or silently drop `sandbox_size` (Python). See the [release notes](/docs/changelog/2026/04/28). | Tier | vCPU | RAM | | ---------- | ---- | ---- | | `standard` | 1 | 1 GB | | `medium` | 2 | 2 GB | | `large` | 4 | 4 GB | | `xlarge` | 8 | 8 GB | Defaults to `standard` when omitted. **Python:** ```python session = composio.sessions.create( user_id="user_123", sandbox={ "sandbox_size": "large", }, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { sandbox: { enable: true, sandboxSize: "large", }, }); ``` > **Pricing:** Sandboxes are not billed today. Composio plans to begin billing for sandbox usage soon (metered by tier and runtime). Pick a tier that matches your workload, but expect future pricing to track actual usage. Changing `sandbox_size` on an existing session recreates the sandbox on the next access. The sandbox's in-memory filesystem state is lost, but the persistent [`/mnt/files/` mount](/docs/sandbox/remote#files-and-mounts) survives the restart. # Session methods For framework examples, see provider-specific documentation like [OpenAI](/docs/providers/openai) or [Vercel AI SDK](/docs/providers/vercel). To connect over MCP instead, see [Using sessions via MCP](/docs/sessions-via-mcp). ## tools() Get the tools the session exposes for your AI framework. By default these are the session's [meta tools](/toolkits/meta-tools), formatted for your configured provider. **Python:** ```python tools = session.tools() ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123"); const tools = await session.tools(); ``` ## authorize() Manually authenticate a user to a toolkit outside of the chat flow. **Python:** ```python connection_request = session.authorize("github") print(connection_request.redirect_url) connected_account = connection_request.wait_for_connection() ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123"); const connectionRequest = await session.authorize("github", { callbackUrl: "https://myapp.com/callback", }); console.log(connectionRequest.redirectUrl); const connectedAccount = await connectionRequest.waitForConnection(); ``` For more details, see [Manually authenticating users](/docs/manually-authenticating). ## toolkits() List the toolkits enabled for the session and their connection status, sorted by popularity. Use it to build a UI showing which apps are connected. Each toolkit includes its `slug`, `name`, `logo`, and connection status, and the call returns the first 20 by default. **Python:** ```python toolkits = session.toolkits() for toolkit in toolkits.items: status = toolkit.connection.connected_account.id if toolkit.connection.is_active else "Not connected" print(f"{toolkit.name}: {status}") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123"); const toolkits = await session.toolkits(); toolkits.items.forEach((toolkit) => { console.log(`${toolkit.name}: ${toolkit.connection?.connectedAccount?.id ?? "Not connected"}`); }); ``` Filter to only connected toolkits: **Python:** ```python connected = session.toolkits(is_connected=True) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123"); const connected = await session.toolkits({ isConnected: true }); ``` Paginate through every toolkit with `limit` and the returned cursor: **Python:** ```python all_toolkits = [] cursor = None while True: result = session.toolkits(limit=20, next_cursor=cursor) all_toolkits.extend(result.items) cursor = result.next_cursor if not cursor: break ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123"); const allToolkits: any[] = []; let cursor: string | undefined; do { const { items, cursor: nextCursor } = await session.toolkits({ limit: 20, cursor }); allToolkits.push(...items); cursor = nextCursor; } while (cursor); ``` ## delete() Delete a session when you're done with it. Deleted sessions immediately stop being retrievable or executable, and the call returns the deleted `session_id`. Deleting a missing or already-deleted session surfaces the backend `404`. > Requires `@composio/core` ≥ `0.13.1` (TypeScript) or `composio` ≥ `0.17.1` (Python). **Python:** ```python result = session.delete() print(result["session_id"], result["deleted"]) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create('user_123'); const result = await session.delete(); console.log(result.sessionId, result.deleted); ``` # Browsing the catalog Before configuring a session, explore the toolkits and tools available. Browse them visually at [dashboard.composio.dev](https://dashboard.composio.dev) or in the [docs catalog](/toolkits), or fetch them programmatically: **Python:** ```python # List toolkits toolkits = composio.toolkits.get() # List tools within a toolkit (top 20 by default) tools = composio.tools.get("user_123", toolkits=["GITHUB"]) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const userId = 'user_123'; // List toolkits const toolkits = await composio.toolkits.get(); // List tools within a toolkit (top 20 by default) const tools = await composio.tools.get(userId, { toolkits: ["GITHUB"] }); ``` Inspect a tool's input and output schema without a user context with `getRawComposioToolBySlug`: **Python:** ```python tool = composio.tools.get_raw_composio_tool_by_slug("GMAIL_SEND_EMAIL") print(tool.name) print(tool.description) print(tool.input_parameters) print(tool.output_parameters) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const tool = await composio.tools.getRawComposioToolBySlug("GMAIL_SEND_EMAIL"); console.log(tool.name); console.log(tool.description); console.log(tool.inputParameters); console.log(tool.outputParameters); ``` # Next - [Sandbox](/docs/sandbox/remote): Give sessions a persistent compute environment with COMPOSIO_REMOTE_WORKBENCH and COMPOSIO_REMOTE_BASH_TOOL --- # Using sessions via MCP (/docs/sessions-via-mcp) By default Composio gives your agent **tools it can call directly**. Composio formats them as function schemas for your framework through a [provider package](/docs/providers). That's what the [Quickstart](/docs/quickstart) uses, and it's the right choice for most agents. If you'd rather connect over the [Model Context Protocol](https://modelcontextprotocol.io), every session also exposes a hosted MCP endpoint. This is useful when you want to plug a session into an MCP-native client like Claude Desktop, Cursor, or any framework's MCP transport. No provider package required. # The MCP endpoint Opt into MCP by passing `mcp: true` when you create the session. The session then exposes its hosted MCP server. Read the URL and headers off `session.mcp`: **Python:** ```python from composio import Composio composio = Composio() session = composio.sessions.create(user_id="user_123", mcp=True) mcp_url = session.mcp.url mcp_headers = session.mcp.headers ``` **TypeScript:** ```typescript import { Composio } from "@composio/core"; const composio = new Composio(); const session = await composio.create("user_123", { mcp: true }); const mcpUrl = session.mcp.url; const mcpHeaders = session.mcp.headers; ``` You don't need a provider package to use the MCP endpoint, so you can drop it from your `Composio()` setup if MCP is all you need. > Resuming a stored session? Pass the same flag, `composio.use(sessionId, { mcp: true })` (TypeScript) or `composio.use(session_id, mcp=True)` (Python), to surface `session.mcp` on the reused session. > The MCP endpoint and `session.tools()` are backed by the same session. Toolkits, auth configs, and connected accounts you set when [configuring the session](/docs/configuring-sessions) apply to both. # A single URL for a fixed set of tools Combine `mcp: true` with the [direct-tools preset](/docs/configuring-sessions) to get one MCP URL that serves exactly the tools you list, with no search or meta tools in front of them. This is the closest equivalent to a classic hosted MCP server scoped to a handful of tools. **Python:** ```python from composio import Composio, SESSION_PRESET_DIRECT_TOOLS composio = Composio() session = composio.sessions.create( user_id="user_123", toolkits=["gmail"], tools={"gmail": {"enable": ["GMAIL_FETCH_EMAILS", "GMAIL_CREATE_EMAIL_DRAFT"]}}, session_preset=SESSION_PRESET_DIRECT_TOOLS, mcp=True, ) # A single MCP URL that exposes just these two tools print(session.mcp.url) ``` **TypeScript:** ```typescript import { Composio, SessionPreset } from "@composio/core"; const composio = new Composio(); const session = await composio.create("user_123", { toolkits: ["gmail"], tools: { gmail: { enable: ["GMAIL_FETCH_EMAILS", "GMAIL_CREATE_EMAIL_DRAFT"] } }, sessionPreset: SessionPreset.DIRECT_TOOLS, mcp: true, }); // A single MCP URL that exposes just these two tools console.log(session.mcp.url); ``` Any MCP client pointed at that URL sees only `GMAIL_FETCH_EMAILS` and `GMAIL_CREATE_EMAIL_DRAFT`. See [Configuring Sessions](/docs/configuring-sessions) for the full set of toolkit, tool, and auth filters. # Wire it into your framework Pass `session.mcp.url` and `session.mcp.headers` to your framework's MCP client. **OpenAI Agents (Python):** ```python from agents import Agent, HostedMCPTool agent = Agent( name="Assistant", tools=[ HostedMCPTool( tool_config={ "type": "mcp", "server_label": "composio", "server_url": session.mcp.url, "headers": session.mcp.headers, "require_approval": "never", } ) ], ) ``` **Claude Agent SDK (Python):** ```python from claude_agent_sdk import ClaudeAgentOptions options = ClaudeAgentOptions( mcp_servers={ "composio": { "type": "http", "url": session.mcp.url, "headers": session.mcp.headers, } }, ) ``` **Vercel AI SDK (TypeScript):** ```typescript import { Composio } from "@composio/core"; import { createMCPClient } from "@ai-sdk/mcp"; const composio = new Composio(); const { mcp } = await composio.create("user_123", { mcp: true }); const client = await createMCPClient({ transport: { type: "http", url: mcp.url, headers: mcp.headers, }, }); const tools = await client.tools(); ``` # Trade-offs MCP is the more portable option. Any MCP-compatible client connects with just a URL, and it's supported across more frameworks and apps (Claude Desktop, Cursor, the OpenAI Responses API, and others) without a provider package. The trade-off is that the MCP client talks to Composio's server directly, so anything the SDK does *around* tool execution doesn't apply: * **Tool-call modifiers don't run.** `beforeExecute` / `afterExecute` hooks and `modifySchema` transforms live in the SDK's execution path. Over MCP the client executes tools against the server and bypasses them, so you can't intercept, reshape, log, or gate calls the way you can with tools your agent calls directly. * **Session-bound custom tools and toolkits don't work.** Tools created with `experimental_createTool` / `experimental_createToolkit` in TypeScript, or `composio.experimental.tool` / `composio.experimental.Toolkit` in Python, run in your process. The MCP server only exposes Composio's hosted tools, so your local custom tools aren't available over the endpoint. If you need any of those, call tools directly through a [provider](/docs/providers) instead of over MCP. # Next - [Configuring Sessions](/docs/configuring-sessions): Restrict toolkits, set custom auth configs, and select connected accounts --- # Remote sandbox (/docs/sandbox/remote) The **sandbox** is a persistent Python environment where your agent writes and executes code. It has programmatic access to all Composio tools, plus helper functions for calling LLMs, uploading files, and making API requests. State persists across calls within a [session](/docs/how-composio-works). Your agent runs code in it through the `COMPOSIO_REMOTE_WORKBENCH` meta tool, and shell commands through `COMPOSIO_REMOTE_BASH_TOOL`. > **Renamed from workbench**: This feature used to be called the **workbench**. The preferred session config key is now `sandbox`, but `workbench` still works as a fully supported alias, in both SDKs and on the wire. It isn't deprecated, so existing code keeps running unchanged. The `COMPOSIO_REMOTE_WORKBENCH` and `COMPOSIO_REMOTE_BASH_TOOL` meta tools keep their names. # Where it fits Use the sandbox when a task is too complex for individual tool calls. Your agent starts with [`SEARCH_TOOLS` to find the right tools, then uses `MULTI_EXECUTE`](/docs/how-composio-works#meta-tools) for straightforward calls. When the task involves bulk operations, data transformations, or multi-step logic, the agent reaches for `COMPOSIO_REMOTE_WORKBENCH` instead. # What the sandbox provides ## Built-in helpers These functions are pre-initialized in every sandbox, so your agent can call them without any setup: | Helper | What it does | | -------------------- | ----------------------------------------------------------------------------------------------------- | | `run_composio_tool` | Execute any Composio tool (e.g., `GMAIL_SEND_EMAIL`, `SLACK_SEND_MESSAGE`) and get structured results | | `invoke_llm` | Call an LLM for classification, summarization, content generation, or data extraction | | `upload_local_file` | Upload generated files (reports, CSVs, images) to cloud storage and get a download URL | | `proxy_execute` | Make direct API calls to connected services when no pre-built tool exists | | `web_search` | Search the web and return results for research or data enrichment | | `smart_file_extract` | Extract text from PDFs, images, and other file formats in the sandbox | ## Libraries The sandbox ships with common packages pre-installed: `pandas`, `numpy`, `matplotlib`, `Pillow`, `PyTorch`, and `reportlab`. Beyond these, the sandbox maintains a list of supported packages and their dependencies. If the agent uses a package that isn't already installed, the sandbox installs it automatically. ## Error correction The sandbox corrects common mistakes in the code your agent generates. For example, if a script accesses `result["apiKey"]` but the actual field name is `api_key`, the sandbox resolves the mismatch instead of failing. ## Persistent state The sandbox runs as a persistent Jupyter notebook. Variables, imports, files, and in-memory state from one call are available in the next. ## Compute tier Sandboxes default to `standard` (1 vCPU, 1 GB RAM). For heavier workloads (large dataframes, ML preprocessing, or big bulk operations), pick a larger tier when creating the session via `sandbox.sandboxSize` (TypeScript) or `sandbox.sandbox_size` (Python). Available tiers: * `standard` (1 vCPU, 1 GB RAM) * `medium` (2 vCPU, 2 GB) * `large` (4 vCPU, 4 GB) * `xlarge` (8 vCPU, 8 GB) Larger tiers require `@composio/core` ≥ `0.8.1` or `composio` ≥ `0.12.1`. See [Configuring sessions → Sandbox compute tier](/docs/configuring-sessions#sandbox-compute-tier) for examples. > **Pricing:** Sandboxes are not billed today. Composio plans to begin billing for sandbox usage soon (metered by tier and runtime). # Files and mounts The sandbox has a persistent file mount at `/mnt/files/`. Code running in the sandbox reads and writes files there, and the mount survives sandbox restarts: changing the [compute tier](#compute-tier) recreates the sandbox and clears in-memory state, but `/mnt/files/` persists. Move files between your app and the mount with `session.experimental.files`. Upload an input file and the agent reads it at `/mnt/files/`. When the agent writes a result, download it from your app. > The files API ships under `session.experimental`, so the surface may change in a future release. **Python:** ```python session = composio.create("user_123") # Upload a local file; the sandbox sees it at /mnt/files/sales.csv uploaded = session.experimental.files.upload("./sales.csv") print(uploaded.sandbox_mount_prefix, uploaded.mount_relative_path) # /mnt/files sales.csv # After the agent writes a result in the sandbox, download it report = session.experimental.files.download("/report.pdf") report.save("./report.pdf") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123"); // Upload a local file; the sandbox sees it at /mnt/files/sales.csv const uploaded = await session.experimental.files.upload("./sales.csv"); console.log(uploaded.sandboxMountPrefix, uploaded.mountRelativePath); // /mnt/files sales.csv // After the agent writes a result in the sandbox, download it const report = await session.experimental.files.download("/report.pdf"); await report.save("./report.pdf"); ``` The mount exposes four methods: | Method | What it does | | -------------------------- | --------------------------------------------------------------------------- | | `upload(input, options?)` | Upload from a local path, URL, `File`, or buffer. Returns a `RemoteFile`. | | `list(options?)` | List files under `path` on the mount, with `cursor` and `limit` pagination. | | `download(path, options?)` | Fetch a file from the mount as a `RemoteFile`. | | `delete(path, options?)` | Remove a file or directory from the mount. | A `RemoteFile` carries the file's bytes and a presigned `downloadUrl`. Read it with `text()` or `buffer()`, or write it to disk with `save(path)`. Its `expiresAt` is when that download link expires, not a TTL on the file: the mount itself has no expiry you set. Every file lives on the session's default `files` mount, surfaced at `/mnt/files/`. Each method takes a `mountId` to address a mount by ID, but there's no SDK call to create custom mounts today, so you'll normally work with `files`. # Next - [Local sandbox](/docs/sandbox/local): Run the same tool calls in a sandbox you own, while Composio keeps managed auth and discovery. --- # Local sandbox (/docs/sandbox/local) A **local sandbox** runs your agent's code in your own infrastructure instead of Composio's hosted [remote sandbox](/docs/sandbox/remote), so code execution never leaves your security boundary. # When to use it Reach for a local sandbox when: * **Sensitive code or data.** The agent runs untrusted code or touches data that can't leave your infrastructure. * **You already run sandboxes.** You'd rather run agent code in your own VM, container, or CI worker than a hosted runtime. * **You need your own filesystem and shell.** The task installs packages, runs a build, or shells out to local tools. If none of that applies, use the [remote sandbox](/docs/sandbox/remote). It's the same helper surface with no infrastructure to run. # Create a local sandbox session A local sandbox session is a [session](/docs/how-composio-works) created with the remote sandbox turned off. Set `workbench.enable: false`, then pass the session to `experimental_createLocalWorkbenchSession` (from the `@composio/experimental` package), which returns the two pieces you run yourself. ```typescript import { Composio } from '@composio/core'; import { experimental_createLocalWorkbenchSession } from '@composio/experimental/workbench'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); // Create the session with the remote sandbox disabled, so code runs in your box. const session = await composio.create('user_123', { toolkits: ['github'], workbench: { enable: false }, }); const { helperSource, env } = await experimental_createLocalWorkbenchSession(composio, session); ``` You get back two things: | Field | What it is | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `helperSource` | A Python helper, as source you write into your sandbox (for example as `composio_helper.py`). It exposes the in-sandbox tool surface the agent calls. | | `env` | The environment variables that helper needs to reach Composio from inside your box. Pass them to the process you run the agent in. | `experimental_createLocalWorkbenchSession` validates that the session is local: it throws if the session has the remote workbench enabled, because the remote sandbox and a local one can't both run for a single session. The session you pass in must have `workbench.enable: false`. > The API ships under the `experimental_` prefix and the `@composio/experimental/workbench` entry point, so the surface may change in a future release. # The helper contract `helperSource` is a Python module your agent imports. It exposes the same helpers the [remote sandbox](/docs/sandbox/remote#built-in-helpers) runs, with the same signatures, so the agent calls tools without knowing that execution is local: ```python from composio_helper import run_composio_tool, invoke_llm, web_search response, error = run_composio_tool( "GITHUB_GET_A_PULL_REQUEST", {"owner": "composiohq", "repo": "composio", "pull_number": 1}, ) ``` Every `run_composio_tool` call routes back through the Tool Router under the session's connections, so auth and discovery stay managed. Most of the workbench helpers run in a local sandbox today. Only the file helpers, which depend on the managed `/mnt/files` mount, are remote-only: | Helper | What it does | Local sandbox | | -------------------- | ---------------------------------------------------------------------------------- | ------------- | | `run_composio_tool` | Execute any Composio tool | ✅ | | `invoke_llm` | Call an LLM | ✅ | | `web_search` | Search the web | ✅ | | `proxy_execute` | Call a toolkit API directly when no tool exists (needs a proxy-execute-scoped key) | ✅ | | `upload_local_file` | Upload generated files to storage | ❌ | | `smart_file_extract` | Extract text from PDFs and images | ❌ | If your agent needs one of the file helpers, use the [remote sandbox](/docs/sandbox/remote) for now. # Security: the sandbox is your boundary The helper reaches Composio with your project `COMPOSIO_API_KEY`, injected into the sandbox through `env`. Any code or output in the sandbox can read it, and that key acts across every connection on the project. > The sandbox is your security boundary. Isolate it like anything else holding a project credential, and rotate `COMPOSIO_API_KEY` if a run could have leaked it. # Next - [What is a session?](/docs/how-composio-works): How sessions scope tools, auth, and sandbox state to a user --- # Proxy execute (/docs/extending-sessions/proxy-execute) `session.proxyExecute()` calls any HTTP endpoint on a toolkit your session can already reach, and Composio injects the authentication (OAuth token, API key, basic auth, and so on) on the server side. Your code never handles raw credentials. The session is already scoped to a [userID](/docs/how-composio-works), so you pass a `toolkit` slug rather than an account ID. Composio resolves the user's connected account for that toolkit and signs the request with it. Proxy execute is a building block, not just a fallback. Use it to build experiences on top of Composio that reach past predefined tools, like the [Pi + Slack bot example](/examples/general-agent-with-pi#reach-the-gaps-with-the-proxy), which drops down to the proxy for the Slack Web API calls the toolkit doesn't wrap as tools. We'll link more examples here as we add them. > **Use a scoped API key**: Proxy execute is gated behind its own permission. Authenticate with a [scoped project API key](/reference/authenticating-to-composio/project-api-key-permissions) that has the **Proxy execute** permission granted. Default full-access keys already include it. # When to use it * **Endpoints with no predefined tool.** You need a specific endpoint (an unusual GitHub, LinkedIn, or Notion route) that isn't exposed as a Composio tool. Send it through the proxy instead of extracting the raw token and calling the API yourself. * **Request shapes a tool can't express.** Custom query parameters, partial field masks, or advanced filters on Gmail, Drive, Sheets, and similar. The proxy gives you the full HTTP surface of the upstream API while Composio keeps managing auth. # Quick start **Python:** ```python from composio import Composio composio = Composio(api_key="your_api_key") session = composio.create("user_123", toolkits=["github"]) response = session.proxy_execute( toolkit="github", endpoint="/repos/composiohq/composio/issues/1", method="GET", parameters=[ {"name": "Accept", "value": "application/vnd.github.v3+json", "in": "header"}, ], ) print(response.status) print(response.data) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create('user_123', { toolkits: ['github'] }); const { status, data } = await session.proxyExecute({ toolkit: 'github', endpoint: '/repos/composiohq/composio/issues/1', method: 'GET', parameters: [ { name: 'Accept', value: 'application/vnd.github.v3+json', in: 'header' }, ], }); console.log(status); console.log(data); ``` The `endpoint` is a path relative to the toolkit's base URL (`/repos/...` resolves against `api.github.com`). Pass an absolute URL only when you need a host that isn't the toolkit's standard API, such as a regional Salesforce or Zendesk domain. > Proxy execute rejects cross-domain requests. The `endpoint` must resolve to the same domain as the toolkit's connected account (a GitHub connection can only call `api.github.com` paths). This is an intentional security boundary, not a quota, so you can't work around it by reshaping the request. # Parameters | Parameter | Required | Type | Description | | ------------ | -------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | `toolkit` | Yes | `string` | Toolkit slug (`github`, `gmail`, and so on). Composio uses the session user's connected account for this toolkit. | | `endpoint` | Yes | `string` | Path relative to the toolkit's base URL, or an absolute URL. | | `method` | Yes | `"GET" \| "POST" \| "PUT" \| "PATCH" \| "DELETE"` | HTTP verb. | | `body` | No | `object` | JSON request body. Used with `POST`, `PUT`, and `PATCH`. | | `parameters` | No | `Array<{ name, value, in }>` | Extra headers or query parameters. `in` is `"header"` or `"query"`. | # Response shape The call returns the upstream response verbatim: | Field | Type | Description | | ------------ | ------------------------ | ------------------------------------------------------------------------------------------ | | `status` | `number` | HTTP status code from the upstream API. | | `data` | `unknown` | Parsed JSON body the API returned. | | `headers` | `Record` | Response headers. | | `binaryData` | `object` | Present only when the upstream returns a file (`url`, `contentType`, `size`, `expiresAt`). | > Don't set the `Authorization` header yourself through `parameters`. Composio injects the correct one from the connected account's auth scheme, and setting it manually overrides that credential and usually produces a `401`. # Error handling `status` and `data` reflect exactly what the toolkit API returned, so check `status` and branch on the common failures. | Status | Typical cause | How to resolve | | ----------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | `400 Bad Request` | Malformed endpoint path, invalid body, or unsupported `method`. | Check the upstream API docs for the expected shape. The proxy doesn't validate upstream schemas. | | `401 Unauthorized` | The connected account's token expired or was revoked. | Re-authenticate the user, or [import fresh credentials](/docs/importing-existing-connections). | | `403 Forbidden` | The user's OAuth scopes or API key don't cover this endpoint. | Update the [auth config scopes](/docs/auth-configuration/custom-auth-configs) and have the user re-consent. | | `429 Too Many Requests` | Upstream rate limit (GitHub, Google, and so on). | Honor the `Retry-After` header and back off. Composio doesn't retry automatically. | # Next - [Custom tools and toolkits](/docs/extending-sessions/custom-tools-and-toolkits): Define in-process tools and toolkits that run alongside Composio tools --- # Custom Tools and Toolkits (/docs/extending-sessions/custom-tools-and-toolkits) Custom tools let you define tools that run in-process alongside remote Composio tools within a session. You have three patterns: * **Standalone tools**: 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`, using `ctx.proxyExecute()` / `ctx.proxy_execute()` for authenticated requests. * **Custom toolkits**: group related standalone tools under a namespace. > Choose your integration type · [Use this guide to decide](/docs/native-tools-vs-mcp) ### Standalone Tool **Install** **TypeScript:** **Python:** **Initialize the client** **TypeScript:** ```typescript import { Composio } from "@composio/core"; const composio = new Composio({ apiKey: "your_api_key" }); ``` **Python:** ```python 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. **TypeScript:** ```typescript import { Composio, experimental_createTool } from "@composio/core"; import { z } from "zod/v3"; const profiles: Record = { "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; }, }); ``` **Python:** ```python from pydantic import BaseModel, Field from composio import Composio from composio_openai_agents import OpenAIAgentsProvider composio = Composio( api_key="your_api_key", provider=OpenAIAgentsProvider(), ) 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 profile ``` **Bind to a session** Pass custom tools via the `experimental` option. `session.tools()` returns both remote Composio tools and your custom tools. **TypeScript:** ```typescript import { Composio, experimental_createTool } from "@composio/core"; import { z } from "zod/v3"; declare const getUserProfile: ReturnType; const composio = new Composio({ apiKey: "your_api_key" }); const session = await composio.create("user_1", { experimental: { customTools: [getUserProfile], }, }); const tools = await session.tools(); ``` **Python:** ```python 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() ``` ### Extension Tool **Install** **TypeScript:** **Python:** **Initialize the client** **TypeScript:** ```typescript import { Composio } from "@composio/core"; const composio = new Composio({ apiKey: "your_api_key" }); ``` **Python:** ```python from composio import Composio composio = Composio(api_key="your_api_key") ``` **Create the tool** An extension tool wraps a Composio toolkit's API with custom business logic. It inherits auth via `extendsToolkit` / `extends_toolkit`, so `ctx.proxyExecute()` / `ctx.proxy_execute()` handles credentials automatically. Prefer relative `endpoint` values in proxy calls. They resolve against the toolkit base URL. Composio only accepts absolute URLs when they stay on the same scheme and registrable domain as that base URL. **TypeScript:** ```typescript import { Composio, experimental_createTool } from "@composio/core"; import { z } from "zod/v3"; 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 address"), }), execute: async (input, ctx) => { const subject = "You're invited to try MyApp Pro"; const body = "Hi there,\n\nWe'd love for you to try MyApp Pro — free for 14 days.\n\nBest,\nThe MyApp Team"; const raw = btoa(`To: ${input.to}\r\nSubject: ${subject}\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n${body}`); const res = await ctx.proxyExecute({ toolkit: "gmail", endpoint: "/gmail/v1/users/me/messages/send", method: "POST", body: { raw }, }); return { status: res.status, to: input.to }; }, }); ``` **Python:** ```python import base64 from pydantic import BaseModel, Field from composio import Composio composio = Composio(api_key="your_api_key") class PromoEmailInput(BaseModel): to: str = Field(description="Recipient email address") @composio.experimental.tool(extends_toolkit="gmail") def send_promo_email(input: PromoEmailInput, ctx): """Send the standard promotional email to a recipient.""" subject = "You're invited to try MyApp Pro" body = ( "Hi there,\n\n" "We'd love for you to try MyApp Pro — free for 14 days.\n\n" "Best,\nThe MyApp Team" ) raw_msg = ( f"To: {input.to}\r\n" f"Subject: {subject}\r\n" "Content-Type: text/plain; charset=UTF-8\r\n\r\n" f"{body}" ) raw = base64.urlsafe_b64encode(raw_msg.encode()).decode().rstrip("=") res = ctx.proxy_execute( toolkit="gmail", endpoint="/gmail/v1/users/me/messages/send", method="POST", body={"raw": raw}, ) return {"status": res.status, "to": input.to} ``` **Bind to a session** Pass custom tools via the `experimental` option. Extension tools inherit auth from the toolkit specified in `extendsToolkit`. **TypeScript:** ```typescript import { Composio, experimental_createTool } from "@composio/core"; import { z } from "zod/v3"; declare const sendPromoEmail: ReturnType; const composio = new Composio({ apiKey: "your_api_key" }); const session = await composio.create("user_1", { toolkits: ["gmail"], experimental: { customTools: [sendPromoEmail], }, }); const tools = await session.tools(); ``` **Python:** ```python from composio import Composio composio = Composio(api_key="your_api_key") session = composio.create( user_id="user_1", toolkits=["gmail"], experimental={ "custom_tools": [send_promo_email], }, ) tools = session.tools() ``` ### Custom Toolkit **Install** **TypeScript:** **Python:** **Initialize the client** **TypeScript:** ```typescript import { Composio } from "@composio/core"; const composio = new Composio({ apiKey: "your_api_key" }); ``` **Python:** ```python from composio import Composio composio = Composio(api_key="your_api_key") ``` **Create the toolkit** A custom toolkit groups related standalone tools under a namespace. Tools inside a toolkit cannot use `extendsToolkit`. **TypeScript:** ```typescript import { Composio, experimental_createTool, experimental_createToolkit, } from "@composio/core"; import { z } from "zod/v3"; const userManagement = experimental_createToolkit("USER_MANAGEMENT", { name: "User scoping", 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 }), }), ], }); ``` **Python:** ```python from pydantic import BaseModel, Field from composio import Composio composio = Composio(api_key="your_api_key") user_management = composio.experimental.Toolkit( slug="USER_MANAGEMENT", name="User scoping", 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} ``` **Bind to a session** Pass custom toolkits via the `experimental` option. `session.tools()` returns both remote Composio tools and your custom toolkit's tools. **TypeScript:** ```typescript import { Composio, experimental_createToolkit } from "@composio/core"; import { z } from "zod/v3"; declare const userManagement: ReturnType; const composio = new Composio({ apiKey: "your_api_key" }); const session = await composio.create("user_1", { experimental: { customToolkits: [userManagement], }, }); const tools = await session.tools(); ``` **Python:** ```python from composio import Composio composio = Composio(api_key="your_api_key") session = composio.create( user_id="user_1", experimental={ "custom_toolkits": [user_management], }, ) tools = session.tools() ``` # Preloading custom tools Custom tools are searchable by default. Set `preload: true` / `preload=True` on a custom tool when it should be returned directly from `session.tools()`. Toolkit preload applies to all tools in that toolkit; set `preload: false` / `preload=False` on one tool to opt it out. **TypeScript:** ```typescript import { Composio, experimental_createTool } from "@composio/core"; import { OpenAIAgentsProvider } from "@composio/openai-agents"; import { z } from "zod/v3"; const composio = new Composio({ apiKey: "your_api_key", provider: new OpenAIAgentsProvider(), }); const replyGuide = experimental_createTool("GET_REPLY_STYLE_GUIDE", { name: "Get reply style guide", description: "Return the team's email reply style guide", preload: true, inputParams: z.object({ topic: z.string().describe("Email topic"), }), execute: async ({ topic }) => ({ topic, tone: "concise and helpful" }), }); const session = await composio.create("user_1", { experimental: { customTools: [replyGuide], }, }); const tools = await session.tools(); console.log(tools.map((tool) => tool.name)); // LOCAL_GET_REPLY_STYLE_GUIDE // COMPOSIO_SEARCH_TOOLS // ... other default meta tools ``` **Python:** ```python from pydantic import BaseModel, Field from composio import Composio from composio_openai_agents import OpenAIAgentsProvider composio = Composio( api_key="your_api_key", provider=OpenAIAgentsProvider(), ) class ReplyGuideInput(BaseModel): topic: str = Field(description="Email topic") @composio.experimental.tool(preload=True) def get_reply_style_guide(input: ReplyGuideInput, ctx): """Return the team's email reply style guide.""" return {"topic": input.topic, "tone": "concise and helpful"} session = composio.create( user_id="user_1", experimental={ "custom_tools": [get_reply_style_guide], }, ) tools = session.tools() print([tool.name for tool in tools]) # LOCAL_GET_REPLY_STYLE_GUIDE # COMPOSIO_SEARCH_TOOLS # ... other default meta 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 the sandbox. # 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. **TypeScript:** | Property / Method | Description | | --------------------------------------------------------------------- | ------------------------------------------------------------- | | `ctx.userId` | The userID 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 | **Python:** | Property / Method | Description | | ------------------------------------------------------------------------ | ------------------------------------------------------------- | | `ctx.user_id` | The userID 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](/reference/sdk-reference/typescript/session-context-impl) | [Python](/reference/sdk-reference/python/session-context-impl) # 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. **TypeScript:** ```typescript import { Composio } from "@composio/core"; const composio = new Composio({ apiKey: "your_api_key" }); const session = await composio.create("user_1"); const customTools = session.customTools(); const customToolkits = session.customToolkits(); ``` **Python:** ```python custom_tools = session.custom_tools() custom_toolkits = session.custom_toolkits() ``` # Reusing a session with custom tools When [reusing a session](/docs/how-composio-works#how-sessions-behave) via `composio.use()`, you can attach custom tools at the same time: **TypeScript:** ```typescript import { Composio, experimental_createTool } from "@composio/core"; import { z } from "zod/v3"; declare const getUserProfile: ReturnType; const composio = new Composio({ apiKey: "your_api_key" }); const session = await composio.use("session_id", { customTools: [getUserProfile], }); ``` **Python:** ```python from composio import Composio composio = Composio(api_key="your_api_key") session = composio.use( "session_id", custom_tools=[get_user_profile], ) ``` # 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. **TypeScript:** ```typescript import { Composio } from "@composio/core"; const composio = new Composio({ apiKey: "your_api_key" }); const session = await composio.create("user_1"); const result = await session.execute("GET_USER_PROFILE"); ``` **Python:** ```python 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 calls an API that requires user credentials (Gmail, GitHub, and so on), set `extendsToolkit` / `extends_toolkit` to the toolkit name. Composio handles authentication automatically, and the agent can prompt users to connect their account when 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_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. # Next - [Configuring sessions](/docs/configuring-sessions): Filter toolkits and tools, set auth configs, and read tools with session.tools() --- # Authentication (/docs/authentication) Composio organizes everything around your **users**. A user is whoever your agent acts on behalf of: a person in your app, identified by a [`userID`](/docs/how-composio-works) you choose. Authentication is always per user. Each user connects their own accounts, their Gmail, their GitHub, their Slack, and Composio stores and refreshes those credentials against that `userID`. This is the core idea: your agent runs the same tools for many people, and every tool call runs as a specific user against that user's connected accounts. User A's agent never touches user B's data. You pass the `userID` when you create a session, and Composio handles the auth from there. Because connections are stored under the `userID`, use a stable identifier, like your database ID, never one that can change. **userID best practices** * **Recommended:** database UUID or primary key (`user.id`) * **Acceptable:** unique username (`user.username`) * **Avoid:** email addresses (they can change) * **Never:** `default` in production (it exposes other users' data) Your users connect their accounts through a secure [Connect Link](/reference/api-reference/connected-accounts/postConnectedAccountsLink), and Composio manages their tokens for you. # How Composio handles authentication Every session includes the [`COMPOSIO_MANAGE_CONNECTIONS`](/toolkits/meta-tools/manage_connections) meta tool. When a tool needs an account, it reads the toolkit's **auth config** (how that toolkit authenticates: method, scopes, credentials), creates a connection, and returns a secure [Connect Link](/reference/api-reference/connected-accounts/postConnectedAccountsLink). This works for all Composio managed connections, so you don't have to set up any OAuth credentials yourself. The user signs in on the hosted link and Composio stores the resulting connected account. Credentials never pass through your app or the model, so it's safe to surface the link right in the chat. You only need a [custom auth config](/docs/custom-app-vs-managed-app) to bring your own OAuth app, request specific scopes, or use a toolkit without managed auth. # In-chat authentication You can also call `COMPOSIO_MANAGE_CONNECTIONS` yourself, intercept the Connect Link, and surface it wherever you need: DM it to the user, render it in your own UI, or email it. See [redirect auth links](/examples/general-agent-with-pi#redirect-auth-links) for a worked example. ## Custom callback URL To send users back to your app after they connect, pass a `callback_url`: **Python:** ```python session = composio.create( user_id="user_123", manage_connections={"callback_url": "https://yourapp.com/chat"}, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { manageConnections: { callbackUrl: "https://yourapp.com/chat" }, }); ``` # Manually triggering authentication Don't want to wait for the agent? Call `session.authorize()` to generate a Connect Link on demand, for onboarding, a settings page, or a pre-flight check before a task. - [Manual auth management](/docs/manually-authenticating): Generate Connect Links yourself, check connection status, and disable in-chat prompts --- # Manual auth management (/docs/manually-authenticating) Manual authentication lets you connect users to toolkits outside the chat flow. Reach for it when you want to: * Pre-authenticate users before they start chatting. * Build a custom connections UI in your app. # Authorize a toolkit Call `session.authorize()` to generate a [Connect Link](/docs/tools-direct/authenticating-tools#hosted-authentication-connect-link) URL, redirect the user, and wait for them to finish: **Python:** ```python session = composio.create(user_id="user_123") connection_request = session.authorize("gmail") print(connection_request.redirect_url) # https://connect.composio.dev/link/ln_abc123 connected_account = connection_request.wait_for_connection(60000) print(f"Connected: {connected_account.id}") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123"); const connectionRequest = await session.authorize("gmail"); console.log(connectionRequest.redirectUrl); // https://connect.composio.dev/link/ln_abc123 const connectedAccount = await connectionRequest.waitForConnection(60000); console.log(`Connected: ${connectedAccount.id}`); ``` Redirect the user to the `redirectUrl`. After they authenticate, they'll return to your callback URL. The connection request polls until the user completes authentication (default timeout: 60 seconds). > If the user closes the Connect Link without completing auth, the connection remains in `INITIATED` status until it expires. # Redirecting users after authentication Pass a `callbackUrl` to control where users land after authenticating. You can include query parameters to carry context through the flow, for example to identify which `userID` or session triggered the connection. **Python:** ```python connection_request = session.authorize( "gmail", callback_url="https://your-app.com/callback?user_id=user_123&source=onboarding" ) print(connection_request.redirect_url) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123"); const connectionRequest = await session.authorize("gmail", { callbackUrl: "https://your-app.com/callback?user_id=user_123&source=onboarding", }); console.log(connectionRequest.redirectUrl); ``` After authentication, Composio redirects the user to your callback URL with the following parameters appended, while preserving your existing ones: | Parameter | Description | | ---------------------- | --------------------------------------------- | | `status` | `success` or `failed` | | `connected_account_id` | The ID of the newly created connected account | ``` https://your-app.com/callback?user_id=user_123&source=onboarding&status=success&connected_account_id=ca_abc123 ``` # Check connection status Use `session.toolkits()` to see all toolkits in the session and their connection status: **Python:** ```python toolkits = session.toolkits() for toolkit in toolkits.items: status = toolkit.connection.connected_account.id if toolkit.connection.is_active else "Not connected" print(f"{toolkit.name}: {status}") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123"); const toolkits = await session.toolkits(); toolkits.items.forEach((toolkit) => { console.log(`${toolkit.name}: ${toolkit.connection?.connectedAccount?.id ?? "Not connected"}`); }); ``` # Disabling in-chat auth By default, sessions include the `COMPOSIO_MANAGE_CONNECTIONS` meta-tool that prompts users to authenticate during chat. To turn it off and handle auth entirely in your own UI, set `manage_connections` to `False`: **Python:** ```python session = composio.create( user_id="user_123", manage_connections=False, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { manageConnections: false, }); ``` # Putting it together A common pattern is to verify all required connections before starting the agent: **Python:** ```python from composio import Composio composio = Composio(api_key="your-api-key") required_toolkits = ["gmail", "github"] session = composio.create( user_id="user_123", manage_connections=False, # Disable in-chat auth prompts ) toolkits = session.toolkits() connected = {t.slug for t in toolkits.items if t.connection.is_active} pending = [slug for slug in required_toolkits if slug not in connected] print(f"Connected: {connected}") print(f"Pending: {pending}") for slug in pending: connection_request = session.authorize(slug) print(f"Connect {slug}: {connection_request.redirect_url}") connection_request.wait_for_connection() print("All toolkits connected!") ``` **TypeScript:** ```typescript import { Composio } from "@composio/core"; const composio = new Composio({ apiKey: "your-api-key" }); const requiredToolkits = ["gmail", "github"]; const session = await composio.create("user_123", { manageConnections: false, // Disable in-chat auth prompts }); const toolkits = await session.toolkits(); const connected = toolkits.items .filter((t) => t.connection?.connectedAccount) .map((t) => t.slug); const pending = requiredToolkits.filter((slug) => !connected.includes(slug)); console.log("Connected:", connected); console.log("Pending:", pending); for (const slug of pending) { const connectionRequest = await session.authorize(slug); console.log(`Connect ${slug}: ${connectionRequest.redirectUrl}`); await connectionRequest.waitForConnection(); } console.log("All toolkits connected!"); ``` # Next - [Managing multiple connected accounts](/docs/managing-multiple-connected-accounts): Let a user connect work and personal accounts for the same toolkit, then pick which one runs --- # Managing multiple connected accounts (/docs/managing-multiple-connected-accounts) Users can connect multiple accounts for the same toolkit (e.g., personal and work Gmail accounts). This guide covers how to enable multi-account mode, label accounts with aliases, and select which account to use. # Multi-account mode By default, each session uses **one account per toolkit**. Enable multi-account mode to let users connect and use multiple accounts for the same toolkit within a single session. **Python:** ```python session = composio.create( user_id="user_123", toolkits=["gmail"], multi_account={ "enable": True, "max_accounts_per_toolkit": 3, }, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { toolkits: ["gmail"], multiAccount: { enable: true, maxAccountsPerToolkit: 3, }, }); ``` ## Configuration options | Option (TS / Python) | Type | Default | Description | | --------------------------------------------------------- | --------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `enable` | `boolean` | `false` | Enable multi-account mode for this session | | `maxAccountsPerToolkit` / `max_accounts_per_toolkit` | `number` | `5` | Maximum connected accounts per toolkit (2-10) | | `requireExplicitSelection` / `require_explicit_selection` | `boolean` | `false` | When true and a toolkit has multiple active connected accounts, the agent must provide the `account` parameter in the tool execution call to select which account to use. `account` can be either a connected account ID or an alias (see the Aliases section below). When false, the default account (most recently connected active account) is used automatically | When multi-account mode is disabled (the default), each session uses the most recently connected account for each toolkit. # Connecting multiple accounts Call `session.authorize()` multiple times for the same toolkit. Each call creates a separate connected account. **Python:** ```python session = composio.create(user_id="user_123", multi_account={"enable": True}) # Connect work account work_auth = session.authorize("gmail", alias="work-gmail") print(f"Connect work Gmail: {work_auth.redirect_url}") work_connection = work_auth.wait_for_connection() # Connect personal account personal_auth = session.authorize("gmail", alias="personal-gmail") print(f"Connect personal Gmail: {personal_auth.redirect_url}") personal_connection = personal_auth.wait_for_connection() ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { toolkits: ["gmail"], multiAccount: { enable: true }, }); // Connect work account const workAuth = await session.authorize("gmail", { alias: "work-gmail" }); console.log(`Connect work Gmail: ${workAuth.redirectUrl}`); const workConnection = await workAuth.waitForConnection(); // Connect personal account const personalAuth = await session.authorize("gmail", { alias: "personal-gmail" }); console.log(`Connect personal Gmail: ${personalAuth.redirectUrl}`); const personalConnection = await personalAuth.waitForConnection(); ``` # Aliases Aliases are human-readable labels for connected accounts (e.g., `"work-gmail"`, `"personal-github"`). They make it easier for agents and users to identify which account is which. * Must be unique per user and toolkit within a project * Can be set during connection or updated after ## Setting an alias during connection Pass `alias` to `session.authorize()`: **Python:** ```python session = composio.create(user_id="user_123") connection_request = session.authorize("gmail", alias="work-gmail") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123"); const connectionRequest = await session.authorize("gmail", { alias: "work-gmail" }); ``` The direct-execution methods `connectedAccounts.initiate()` and `connectedAccounts.link()` accept the same `alias` parameter. ## Updating or clearing an alias **Python:** ```python # Set or update an alias composio.connected_accounts.update("ca_abc123", alias="work-gmail") # Clear an alias composio.connected_accounts.update("ca_abc123", alias="") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); // Set or update an alias await composio.connectedAccounts.update("ca_abc123", { alias: "work-gmail" }); // Clear an alias await composio.connectedAccounts.update("ca_abc123", { alias: "" }); ``` # Selecting a specific account for a session Pin a session to specific accounts by passing their IDs in the session config. To retrieve connected account IDs, see [List accounts](/docs/auth-configuration/connected-accounts#list-accounts). **Python:** ```python session = composio.create( user_id="user_123", connected_accounts={ "gmail": ["ca_work_gmail_id"], "github": ["ca_personal_github_id"], }, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { connectedAccounts: { gmail: ["ca_work_gmail_id"], github: ["ca_personal_github_id"], }, }); ``` # Viewing session's active accounts Use `session.toolkits()` to see which accounts are currently active: **Python:** ```python toolkits = session.toolkits() for toolkit in toolkits.items: if toolkit.connection and toolkit.connection.connected_account: print(f"{toolkit.name}: {toolkit.connection.connected_account.id}") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123"); const toolkits = await session.toolkits(); for (const toolkit of toolkits.items) { if (toolkit.connection?.connectedAccount) { console.log(`${toolkit.name}: ${toolkit.connection.connectedAccount.id}`); } } ``` # Next - [Shared connections](/docs/shared-connections): Make one connected account usable by multiple users with a per-user access control list --- # Shared connections (/docs/shared-connections) By default, a connected account is **PRIVATE**: only the `userID` that created it can use it. A **SHARED** connection can be reached by other `userID`s, subject to a per-connection access control list (ACL). You use a shared connection by [pinning it into a session](#using-a-shared-connection): the session's `userID` doesn't own the connection, but the pin makes it available. A SHARED connection is never resolved implicitly, so a session reaches one only when you pin it explicitly. Typical use cases: * **Org-managed credentials.** One Gmail, Salesforce, or GitHub connection that every user in your app can call against, without each user having to authenticate separately. * **Background agents acting on behalf of multiple users.** The agent runs as a single service account but executes work for many `userID`s. * **Team mailboxes.** `support@` or `sales@` accounts where any teammate can send and read mail through your app. # SHARED vs PRIVATE | | PRIVATE (default) | SHARED | | ---------------------------------- | --------------------------- | ---------------------------------------------------------------------------------------- | | **Who can use it** | Only the owning `userID` | The creator plus every `userID` permitted by the ACL | | **Default access for other users** | Always denied | Deny-by-default (the creator must grant access explicitly) | | **How it's used** | Implicit lookup by `userID` | Must be **explicitly pinned** in a session | | **ACL fields** | Ignored | `allowAllUsers`, `allowedUserIds`, `notAllowedUserIds` (inside the `experimental` block) | # Creating a SHARED connection Pass an `experimental` block to `link()` (`accountType` in TypeScript, `account_type` in Python) set to `"SHARED"`, and optionally an initial ACL. Omit the ACL block to keep the default deny-by-default state (only the creator can use it until you grant access). **Python:** ```python # Create a SHARED Gmail connection that any userId can use, # except `user_bob`. connection_request = composio.connected_accounts.link( user_id="user_admin", auth_config_id="ac_gmail_shared", experimental={ "account_type": "SHARED", "acl_config_for_shared": { "allow_all_users": True, "not_allowed_user_ids": ["user_bob"], }, }, ) print(connection_request.redirect_url) # Have user_admin complete the OAuth flow at the redirect URL, # then wait for the connection to become ACTIVE. connected = connection_request.wait_for_connection() print(f"Shared connection ready: {connected.id}") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); // Create a SHARED Gmail connection that any userId can use, // except `user_bob`. const connectionRequest = await composio.connectedAccounts.link( "user_admin", "ac_gmail_shared", { experimental: { accountType: "SHARED", aclConfigForShared: { allowAllUsers: true, notAllowedUserIds: ["user_bob"], }, }, }, ); console.log(connectionRequest.redirectUrl); // Have user_admin complete the OAuth flow at the redirect URL, // then wait for the connection to become ACTIVE. const connected = await connectionRequest.waitForConnection(); console.log(`Shared connection ready: ${connected.id}`); ``` The returned `connectedAccountId` (`ca_...`) is the ID you'll pin into other users' sessions. > ACL fields are only valid on SHARED connections. Sending an `experimental.acl_config_for_shared` block on a PRIVATE connection raises `ComposioAclOnlyForSharedError`. # Using a shared connection Pin the SHARED connection into a session through `connectedAccounts`. The session belongs to a *different* `userID` than the creator, and the pin is what makes the SHARED connection visible to that session. The session config itself is **not** experimental. You pin the connection by ID exactly as you would a PRIVATE one. **Python:** ```python # user_alice starts a session that pins the shared Gmail connection. # Gmail tools loaded from this session will resolve to that connection # even though user_alice did not create it. session = composio.sessions.create( user_id="user_alice", connected_accounts={ "gmail": ["ca_gmail_shared"], }, ) tools = session.tools() ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); // user_alice starts a session that pins the shared Gmail connection. // Gmail tools loaded from this session will resolve to that connection // even though user_alice did not create it. const session = await composio.create("user_alice", { connectedAccounts: { gmail: ["ca_gmail_shared"], }, }); const tools = await session.tools(); ``` > A session may pin **at most one SHARED connection per toolkit**. Pinning two SHARED Gmail connections in the same session is rejected at session create time. Mixing one SHARED with multiple PRIVATE pins is allowed. # ACL resolution rule When a non-creator `userID` attempts to use a SHARED connection, the ACL is evaluated in this order: 1. `userId` ∈ `notAllowedUserIds` → **DENY** 2. `allowAllUsers === true` → **ALLOW** 3. `userId` ∈ `allowedUserIds` → **ALLOW** 4. otherwise → **DENY** *(deny-by-default)* Deny wins on conflict, which lets you express *"share with everyone except a few people"* by setting `allowAllUsers: true` and naming the exceptions in `notAllowedUserIds`. The creator can always use their own connection. The ACL only governs other `userID`s. ## Common ACL patterns The table below shows the inner shape of the ACL block (`aclConfigForShared` in TypeScript, `acl_config_for_shared` in Python). Wrap it inside the `experimental` block at the call site. Field names are camelCase in the TypeScript samples; Python callers translate to snake\_case (`allow_all_users`, `allowed_user_ids`, `not_allowed_user_ids`). | Goal | ACL block | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | **Only the creator** (default) | `{}` (or omit the block) | | **Allow every `userId`** | `{ allowAllUsers: true }` | | **Targeted allow list** | `{ allowedUserIds: ["user_alice", "user_bob"] }` | | **Everyone except a few users** | `{ allowAllUsers: true, notAllowedUserIds: ["user_bob"] }` | | **Combined: open + targeted deny** | `{ allowAllUsers: true, notAllowedUserIds: ["user_bob"], allowedUserIds: ["user_alice"] }` (Bob still denied, deny wins) | Each list accepts up to 1000 entries; each `userID` is 1..256 characters. # Updating the ACL Call `updateAcl()` on the connected accounts namespace to change access after creation. It follows PATCH semantics: pass only the fields you want to change, omit a field to leave it unchanged, and pass an empty array to clear an allow or deny list. **Python:** ```python # Open access to everyone. composio.connected_accounts.update_acl( "ca_gmail_shared", allow_all_users=True, ) # Add a targeted allow list (without touching the wildcard or deny list). composio.connected_accounts.update_acl( "ca_gmail_shared", allowed_user_ids=["user_alice", "user_bob"], ) # Revoke the allow list; only the creator can use it again # (unless allow_all_users is True). composio.connected_accounts.update_acl( "ca_gmail_shared", allowed_user_ids=[], ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); // Open access to everyone. await composio.connectedAccounts.updateAcl("ca_gmail_shared", { allowAllUsers: true, }); // Add a targeted allow list (without touching the wildcard or deny list). await composio.connectedAccounts.updateAcl("ca_gmail_shared", { allowedUserIds: ["user_alice", "user_bob"], }); // Revoke the allow list; only the creator can use it again // (unless allowAllUsers is true). await composio.connectedAccounts.updateAcl("ca_gmail_shared", { allowedUserIds: [], }); ``` > Passing `notAllowedUserIds: []` **clears the deny list**, which silently re-grants access to users you previously blocked. Always audit the allow side when clearing a deny list. ACL writes are restricted to the connection's creator or an API key caller. Other callers get a permission error. # Listing SHARED connections By default `list()` returns **PRIVATE only**, so shared accounts must be requested explicitly. Pass an `account_type` (Python) or `accountType` (TypeScript) filter to scope the query. | Value | Returns | | ------------------------------------ | ------------------------ | | `'PRIVATE'` *(default when omitted)* | Only PRIVATE connections | | `'SHARED'` | Only SHARED connections | | `'ALL'` | PRIVATE + SHARED | The filter is a flat query param on the wire (`?account_type=...`), so it stays flat in both SDKs, unlike the create and update surfaces, which nest under `experimental`. **Python:** ```python # List every SHARED connection the caller has visibility into. shared = composio.connected_accounts.list(account_type="SHARED") for item in shared.items: print(item.id, item.toolkit.slug) # Scope to a single user's SHARED connections. shared_for_alice = composio.connected_accounts.list( account_type="SHARED", user_ids=["user_alice"], ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); // List every SHARED connection the caller has visibility into. const shared = await composio.connectedAccounts.list({ accountType: "SHARED" }); for (const item of shared.items) { console.log(item.id, item.toolkit.slug); } // Scope to a single user's SHARED connections. const sharedForAlice = await composio.connectedAccounts.list({ accountType: "SHARED", userIds: ["user_alice"], }); ``` # Inspecting the ACL `get()` and `list()` responses surface `accountType` and `aclConfigForShared` under the same `experimental` block as the request shape. The `aclConfigForShared` field is populated only when the caller is the connection's creator or is using an API key. Other callers see the `experimental` block without that field. **Python:** ```python account = composio.connected_accounts.get("ca_gmail_shared") if account.experimental: print(f"Type: {account.experimental.account_type}") # "PRIVATE" or "SHARED" if account.experimental.acl_config_for_shared: acl = account.experimental.acl_config_for_shared print(f"Allow all users: {acl.allow_all_users}") print(f"Allowed: {acl.allowed_user_ids}") print(f"Denied: {acl.not_allowed_user_ids}") else: # You're not authorised to see the ACL on this connection. print("ACL hidden") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const account = await composio.connectedAccounts.get("ca_gmail_shared"); if (account.experimental) { console.log("Type:", account.experimental.accountType); // "PRIVATE" or "SHARED" if (account.experimental.aclConfigForShared) { const acl = account.experimental.aclConfigForShared; console.log("Allow all users:", acl.allowAllUsers); console.log("Allowed:", acl.allowedUserIds); console.log("Denied:", acl.notAllowedUserIds); } else { // You're not authorised to see the ACL on this connection. console.log("ACL hidden"); } } ``` # Error handling | Error | When | | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ComposioAclOnlyForSharedError` (400) | ACL fields sent on a PRIVATE connection (at create or update time). | | `ComposioSharedAccessDeniedError` (403) | Direct execute with a SHARED `connectedAccountId` that the requesting `userId` isn't permitted to use. | | `ComposioSharedConnectionNotAccessibleError` (400) | A session pinned a SHARED connection that the session's `userID` cannot use. The error is raised at session create time, so the session never enters a state that fails mid-execution. | The access errors are caught the same way as any other Composio exception (`ComposioAclOnlyForSharedError` and `ComposioSharedAccessDeniedError` are exported from `@composio/core`, and live under `composio.exceptions` in Python). # Next - [Configuring sessions](/docs/configuring-sessions): Pin connected accounts, auth configs, and toolkit restrictions into a session --- # Importing existing connections (/docs/importing-existing-connections) If your users have already authenticated with a service and you have their credentials (API keys, bearer tokens, etc.), you can pass those directly into Composio. No re-authentication required. This is useful when: * Your app already stores API keys or tokens for users * You're adopting Composio and want to onboard existing users without disrupting them * You want to use bearer tokens with OAuth toolkits (Gmail, GitHub, Slack, etc.) without setting up an OAuth app # How it works # Prerequisites 1. **An [auth config](/docs/programmatic-auth-configs)** for the toolkit you're importing into 2. **The existing credentials** for each user (API keys, bearer tokens, username/password, etc.) 3. **A userID** for each user. Any string that uniquely identifies them in your system. # API keys For services that use API key authentication (e.g., SendGrid, Tavily, PostHog): **Python:** ```python from composio import Composio from composio.types import auth_scheme composio = Composio(api_key="your-api-key") connection = composio.connected_accounts.initiate( user_id="user_123", auth_config_id="ac_your_auth_config", config=auth_scheme.api_key({ "api_key": "sg-existing-sendgrid-key", }), ) # API key connections are immediately active print(f"Connected: {connection.id}") ``` **TypeScript:** ```typescript import { Composio, AuthScheme } from '@composio/core'; const composio = new Composio({ apiKey: 'your-api-key' }); const connection = await composio.connectedAccounts.initiate( 'user_123', 'ac_your_auth_config', { config: AuthScheme.APIKey({ api_key: 'sg-existing-sendgrid-key', }), } ); // API key connections are immediately active console.log('Connected:', connection.id); ``` # Bearer tokens If you manage your own OAuth flow and already have an access token for a service, you can import it into Composio as a bearer token. This lets you bring existing OAuth connections into Composio without re-authenticating your users. It works with **all toolkits that support OAuth2 or S2S auth** (Gmail, GitHub, Slack, Google Docs, and more). Any additional parameters the toolkit supports (e.g., `subdomain`, `base_url`) work the same way. Since you're providing your own token, Composio won't handle OAuth refresh. You're responsible for refreshing the token on your end and pushing the updated value to Composio via the [PATCH method](#updating-credentials) whenever it changes. After [creating an auth config](/docs/programmatic-auth-configs) with `authScheme: "BEARER_TOKEN"`, use the snippet below to create a connected account: **Python:** ```python from composio import Composio from composio.types import auth_scheme composio = Composio(api_key="your-api-key") connection = composio.connected_accounts.initiate( user_id="user_123", auth_config_id="ac_your_auth_config", config=auth_scheme.bearer_token({ "token": "existing-bearer-token", }), ) # Bearer token connections are immediately active print(f"Connected: {connection.id}") ``` **TypeScript:** ```typescript import { Composio, AuthScheme } from '@composio/core'; const composio = new Composio({ apiKey: 'your-api-key' }); const connection = await composio.connectedAccounts.initiate( 'user_123', 'ac_your_auth_config', { config: AuthScheme.BearerToken({ token: 'existing-bearer-token', }), } ); // Bearer token connections are immediately active console.log('Connected:', connection.id); ``` # Basic auth **Python:** ```python from composio import Composio from composio.types import auth_scheme composio = Composio(api_key="your-api-key") connection = composio.connected_accounts.initiate( user_id="user_123", auth_config_id="ac_your_auth_config", config=auth_scheme.basic({ "username": "user@example.com", "password": "existing-password", }), ) # Basic auth connections are immediately active print(f"Connected: {connection.id}") ``` **TypeScript:** ```typescript import { Composio, AuthScheme } from '@composio/core'; const composio = new Composio({ apiKey: 'your-api-key' }); const connection = await composio.connectedAccounts.initiate( 'user_123', 'ac_your_auth_config', { config: AuthScheme.Basic({ username: 'user@example.com', password: 'existing-password', }), } ); // Basic auth connections are immediately active console.log('Connected:', connection.id); ``` # Updating credentials When credentials expire or rotate, update them in place without recreating the connection. Fields you omit are preserved. Fields set to `null` are removed. **Bearer token:** ```python composio.connected_accounts.update( "ca_your_connection_id", connection={ "state": { "authScheme": "BEARER_TOKEN", "val": {"token": "new-access-token"}, }, }, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your-api-key' }); await composio.connectedAccounts.update('ca_your_connection_id', { connection: { state: { authScheme: 'BEARER_TOKEN', val: { token: 'new-access-token' }, }, }, }); ``` **curl:** ```bash curl -X PATCH https://backend.composio.dev/api/v3.1/connected_accounts/ca_xxx \ -H 'x-api-key: YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -d '{"connection":{"state":{"authScheme":"BEARER_TOKEN","val":{"token":"new-access-token"}}}}' ``` **API key:** ```python composio.connected_accounts.update( "ca_your_connection_id", connection={ "state": { "authScheme": "API_KEY", "val": {"generic_api_key": "new-api-key"}, }, }, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your-api-key' }); await composio.connectedAccounts.update('ca_your_connection_id', { connection: { state: { authScheme: 'API_KEY', val: { generic_api_key: 'new-api-key' }, }, }, }); ``` **curl:** ```bash curl -X PATCH https://backend.composio.dev/api/v3.1/connected_accounts/ca_xxx \ -H 'x-api-key: YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -d '{"connection":{"state":{"authScheme":"API_KEY","val":{"generic_api_key":"new-api-key"}}}}' ``` **Basic auth:** ```python composio.connected_accounts.update( "ca_your_connection_id", connection={ "state": { "authScheme": "BASIC", "val": {"username": "user@example.com", "password": "new-password"}, }, }, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your-api-key' }); await composio.connectedAccounts.update('ca_your_connection_id', { connection: { state: { authScheme: 'BASIC', val: { username: 'user@example.com', password: 'new-password' }, }, }, }); ``` **curl:** ```bash curl -X PATCH https://backend.composio.dev/api/v3.1/connected_accounts/ca_xxx \ -H 'x-api-key: YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -d '{"connection":{"state":{"authScheme":"BASIC","val":{"username":"user@example.com","password":"new-password"}}}}' ``` # Using in your session Pass the auth config or connection ID when creating a session: **Python:** ```python session = composio.create( "user_123", auth_configs={"gmail": "ac_your_auth_config"}, toolkits=["gmail"], ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your-api-key' }); const session = await composio.create('user_123', { authConfigs: { gmail: 'ac_your_auth_config' }, toolkits: ['gmail'], }); ``` # Next - [Managing multiple accounts](/docs/managing-multiple-connected-accounts): Pin and select connected accounts for a user --- # Managed vs custom auth (/docs/custom-app-vs-managed-app) Composio supports two ways to authenticate users with toolkits. * **[Composio managed apps](/toolkits/managed-auth)**: Composio registers and maintains OAuth apps for popular toolkits (GitHub, Gmail, Slack, etc.). Zero setup, works out of the box. * **Custom auth configs**: You provide your own OAuth app, API key, bearer token, or other credentials and tell Composio to use them for a toolkit. This page covers when to use each, how to create a custom auth config, and how to wire it into a session. # When to use Composio managed apps Managed apps are the default. Every toolkit that supports OAuth has a Composio managed app ready to go. Use them when: * **You're building and iterating.** No OAuth app registration, no credentials to manage. Create a session and start testing immediately. * **Default scopes cover your needs.** Composio requests sensible defaults for each toolkit. * **Branding on consent screens doesn't matter yet.** Users will see "Composio wants to access your account" during OAuth. Fine for internal tools, prototypes, and development. You can still [white-label the Connect Link page](/docs/white-labeling-authentication#customizing-the-connect-link) with your logo and app title without needing your own OAuth app. # When to use a custom auth config Bring your own credentials when any of these apply: * **Your users see OAuth consent screens.** In production, users should see your app name, not "Composio." This is the most common reason to switch. * **You need custom scopes.** Composio's default scopes may not include everything you need (e.g., write access to a specific Google API). * **You're hitting rate limits.** Managed apps share quota across all Composio users. Your own app gets a dedicated quota. * **You need faster polling triggers.** Managed auth enforces a 15-minute minimum polling interval; your own app can use shorter polling intervals where supported. * **You're connecting to a custom instance.** Self-hosted or regional variants (e.g., a private Salesforce subdomain) need their own OAuth app. * **Enterprise customers require your branding end-to-end.** # Create a custom auth config To check whether a toolkit already has a Composio managed app, see the [managed auth toolkit list](/toolkits/managed-auth). You can still create a custom auth config for branding, scopes, rate limits, polling intervals, or custom instances. The steps below use the dashboard. To create auth configs in code instead, for example one per customer, see [Programmatic auth configs](/docs/programmatic-auth-configs). #### Create the auth config in the Composio dashboard In the [Composio dashboard](https://dashboard.composio.dev/~/project/auth-configs): 1. Click **Create Auth Config** 2. Select the toolkit 3. Choose the auth scheme (OAuth2, API Key, Bearer Token, etc.) 4. Follow the dashboard instructions for the required credential fields For OAuth toolkits, the dashboard shows the redirect URI to add in the provider's developer portal. #### Collect credentials from the provider For OAuth toolkits, register an app in the provider's developer portal and add the redirect URI from the dashboard. Then copy the **Client ID** and **Client Secret** back into Composio. For API key, bearer token, basic auth, or other auth schemes, collect the credential fields the toolkit requires and enter them in the dashboard. Step-by-step OAuth guides: [Google](https://composio.dev/auth/googleapps) | [GitHub](https://composio.dev/auth/github) | [Slack](https://composio.dev/auth/slack) | [HubSpot](https://composio.dev/auth/hubspot) | [All toolkits](https://composio.dev/auth) #### Save and copy the auth config ID After you enter the required credentials, click **Create** and copy the auth config ID (for example, `ac_1234abcd`). #### Pass the auth config ID in your session Creating the auth config isn't enough on its own. A session uses your config only when you pass its ID to `authConfigs`, keyed by toolkit. Toolkits you leave out keep using Composio managed auth. **Python:** ```python from composio import Composio composio = Composio() session = composio.create( user_id="user_123", auth_configs={ "github": "ac_your_github_config", # toolkits not listed here still use Composio managed auth }, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const session = await composio.create("user_123", { authConfigs: { github: "ac_your_github_config", // toolkits not listed here still use Composio managed auth }, }); ``` # Mixing per toolkit You don't have to pick one approach for all toolkits. Use your own credentials for toolkits where users see consent screens (GitHub, Google, Slack) and Composio managed auth for the rest. Each toolkit gets its own auth config independently. **Python:** ```python session = composio.create( user_id="user_123", auth_configs={ "github": "ac_your_github_config", "google": "ac_your_google_config", # everything else uses Composio managed auth automatically }, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const session = await composio.create("user_123", { authConfigs: { github: "ac_your_github_config", google: "ac_your_google_config", // everything else uses Composio managed auth automatically }, }); ``` ## Toolkits without managed auth Some toolkits don't have Composio managed auth. For these, the setup is the same as above, except the provider may ask for API keys or instance details instead of OAuth credentials. Browse the full list on the [managed auth page](/toolkits/managed-auth) or check individual toolkit pages on the [toolkits page](/toolkits). # Next - [Programmatic auth configs](/docs/programmatic-auth-configs): Create auth configs in code and pass them to a session --- # Programmatic auth configs (/docs/programmatic-auth-configs) An [auth config](/docs/authentication#behind-the-scenes) is a blueprint for how a toolkit authenticates: the method, scopes, and credentials. Most of the time you create one in the [dashboard](https://dashboard.composio.dev/~/project/auth-configs) and reuse it. Create them in code when you provision auth dynamically: a config per customer, per environment, or spun up and torn down as part of your app's lifecycle. `composio.authConfigs.create()` returns an auth config ID like `ac_xxxxxxxx`. Store that ID, then [pass it to a session](#use-the-auth-config-in-a-session) so the session authenticates with it. # Composio managed auth For OAuth2 toolkits, Composio maintains a managed app so you can create an auth config without bringing your own credentials. This is the fastest way to start. **Python:** ```python from composio import Composio composio = Composio() auth_config = composio.auth_configs.create( toolkit="github", options={"type": "use_composio_managed_auth", "name": "GitHub"}, ) print(auth_config.id) # ac_xxxxxxxx ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const authConfig = await composio.authConfigs.create('github', { type: 'use_composio_managed_auth', name: 'GitHub', }); console.log(authConfig.id); // ac_xxxxxxxx ``` # Your own OAuth2 credentials Bring your own OAuth app to show your branding on consent screens, request custom scopes, or get a dedicated rate-limit quota. Register the app in the provider's developer portal, set its authorized redirect URI to Composio's callback, then pass the client ID and secret. ``` https://backend.composio.dev/api/v3.1/toolkits/auth/callback ``` **Python:** ```python import os from composio import Composio composio = Composio() auth_config = composio.auth_configs.create( toolkit="notion", options={ "type": "use_custom_auth", "auth_scheme": "OAUTH2", "name": "Notion", "credentials": { "client_id": os.environ["NOTION_CLIENT_ID"], "client_secret": os.environ["NOTION_CLIENT_SECRET"], "oauth_redirect_uri": "https://backend.composio.dev/api/v3.1/toolkits/auth/callback", }, }, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const authConfig = await composio.authConfigs.create('notion', { type: 'use_custom_auth', authScheme: 'OAUTH2', name: 'Notion', credentials: { client_id: process.env.NOTION_CLIENT_ID!, client_secret: process.env.NOTION_CLIENT_SECRET!, oauth_redirect_uri: 'https://backend.composio.dev/api/v3.1/toolkits/auth/callback', }, }); ``` > Omit `oauth_redirect_uri` to use Composio's default callback. Set it only when you [route the callback through your own domain](/docs/white-labeling-authentication#routing-the-callback-through-your-domain). # Other auth types Toolkits that use API keys, bearer tokens, basic auth, or no auth follow the same call. Set `auth_scheme` to the toolkit's scheme and put the required fields in `credentials`. For a toolkit whose key the user supplies at connect time, pass empty `credentials`. **Python:** ```python auth_config = composio.auth_configs.create( toolkit="perplexityai", options={ "type": "use_custom_auth", "auth_scheme": "API_KEY", "name": "Perplexity AI", "credentials": {}, }, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const authConfig = await composio.authConfigs.create('perplexityai', { type: 'use_custom_auth', authScheme: 'API_KEY', name: 'Perplexity AI', credentials: {}, }); ``` # Use the auth config in a session Creating an auth config does not change which credentials a session uses. Pass the auth config ID to `authConfigs` (keyed by toolkit) when you create the session, and the session authenticates that toolkit with your config. Toolkits you leave out keep using Composio managed auth. **Python:** ```python session = composio.sessions.create( user_id="user_123", auth_configs={"notion": auth_config.id}, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const authConfig = { id: 'ac_your_notion_config' }; const session = await composio.create('user_123', { authConfigs: { notion: authConfig.id }, }); ``` See [Configuring sessions](/docs/configuring-sessions#custom-auth-configs) for how `authConfigs` interacts with account selection and precedence. # Find auth configs In TypeScript, filter `authConfigs.list()` by name or ID with `search`. Disabled configs are excluded by default; set `showDisabled` to include them. Auth config responses use `id` as their canonical identifier. ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const configs = await composio.authConfigs.list({ search: 'github', showDisabled: true, }); for (const config of configs.items) { console.log(config.id); } ``` # Discover the required fields Different schemes need different credential fields. To build the `credentials` object dynamically, ask the toolkit which fields it requires for a given scheme before you create the config. **Python:** ```python fields = composio.toolkits.get_auth_config_creation_fields( toolkit="notion", auth_scheme="OAUTH2", required_only=True, ) print(fields) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const fields = await composio.toolkits.getAuthConfigCreationFields('notion', 'OAUTH2', { requiredOnly: true, }); console.log(fields); ``` # Next - [Controlling scopes](/docs/controlling-scopes): Override the default OAuth scopes Composio requests for a toolkit --- # Controlling scopes (/docs/controlling-scopes) Scopes are the permissions an OAuth toolkit grants your app: read email, write to a repo, manage calendar events. Composio requests a sensible set of default scopes for each toolkit, so most apps never set scopes at all. Override them when the defaults grant too much or too little: to follow least privilege, or to reach an API the defaults don't cover. You control scopes on an [auth config](/docs/authentication#behind-the-scenes), then [pass that auth config to a session](#use-the-auth-config-in-a-session) so the session requests your scopes when users connect. > Scopes apply to OAuth toolkits. Toolkits that authenticate with API keys or bearer tokens don't have scopes to set. # Set scopes with Composio managed auth Pass a `scopes` field in `credentials` to override the defaults while still using Composio's managed OAuth app. Give scopes as a comma-separated string. **Python:** ```python from composio import Composio composio = Composio() auth_config = composio.auth_configs.create( toolkit="hubspot", options={ "type": "use_composio_managed_auth", "name": "HubSpot", "credentials": {"scopes": "sales-email-read,tickets"}, }, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const authConfig = await composio.authConfigs.create('hubspot', { type: 'use_composio_managed_auth', name: 'HubSpot', credentials: { scopes: 'sales-email-read,tickets' }, }); ``` # Set scopes with your own OAuth app When you bring your own OAuth credentials, put `scopes` alongside the client ID and secret. Make sure your OAuth app has those scopes approved in the provider's portal. **Python:** ```python import os auth_config = composio.auth_configs.create( toolkit="github", options={ "type": "use_custom_auth", "auth_scheme": "OAUTH2", "name": "GitHub", "credentials": { "client_id": os.environ["GITHUB_CLIENT_ID"], "client_secret": os.environ["GITHUB_CLIENT_SECRET"], "scopes": "repo,read:org", }, }, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const authConfig = await composio.authConfigs.create('github', { type: 'use_custom_auth', authScheme: 'OAUTH2', name: 'GitHub', credentials: { client_id: process.env.GITHUB_CLIENT_ID!, client_secret: process.env.GITHUB_CLIENT_SECRET!, scopes: 'repo,read:org', }, }); ``` # Update scopes on an existing config Change the scopes on an auth config you already created without recreating it. **Python:** ```python composio.auth_configs.update( "ac_1234", {"type": "default", "scopes": "repo,read:org,read:user"}, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); await composio.authConfigs.update('ac_1234', { type: 'default', scopes: 'repo,read:org,read:user', }); ``` > Changing scopes affects new connections only. Users with an existing [connected account](/docs/authentication#behind-the-scenes) keep the scopes they already granted until they reconnect. To apply new scopes to a current user, have them re-authenticate. # Use the auth config in a session Setting scopes on an auth config does nothing until a session uses it. Pass the auth config ID to `authConfigs` (keyed by toolkit) when you create the session, and the session requests your scopes when the user connects that toolkit. **Python:** ```python session = composio.create( user_id="user_123", auth_configs={"github": auth_config.id}, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const authConfig = { id: 'ac_your_github_config' }; const session = await composio.create('user_123', { authConfigs: { github: authConfig.id }, }); ``` # Next - [White-labeling authentication](/docs/white-labeling-authentication): Remove Composio branding from your auth flows --- # White-labeling authentication (/docs/white-labeling-authentication) There are four places where Composio branding shows up during authentication: | Where | What users see | How to fix | | --------------------------------------------------------------------- | --------------------------------------------------------------- | --------------------------------------------------- | | [**Connect Link page**](#customizing-the-connect-link) | Composio logo and name on the hosted auth page | Change logo + app title in dashboard | | [**OAuth consent screen**](#using-your-own-oauth-apps) | "Composio wants to access your account" on Google, GitHub, etc. | Use your own OAuth app | | [**Browser address bar**](#routing-the-callback-through-your-domain) | `backend.composio.dev` flashes during OAuth redirect-back | Proxy the redirect through your domain | | [**Post-auth success page**](#redirecting-users-after-authentication) | Composio-branded success page after OAuth completes | Pass a `callbackUrl` when initiating the connection | # Customizing the Connect Link The Connect Link is the hosted page your users see when connecting their accounts. By default it shows Composio branding. To replace it with your own branding: 1. Go to **Project Settings** → [**Auth Screen**](https://dashboard.composio.dev/~/project/settings/auth-screen) 2. Upload your **Logo** and set your **App Title** This applies to all Connect Link flows across all toolkits, for both [in-chat](/docs/authentication#in-chat-authentication) and [manual](/docs/manually-authenticating) authentication. Each project has one logo and app title, so if you need different branding per product, use separate projects. > Customizing the Connect Link only changes the Composio-hosted page. For OAuth toolkits like Gmail, Google Sheets, GitHub, and Slack, users still see a consent screen saying "Composio wants to access your account." To change that, and to remove the "Secured by Composio" badge, set up your own OAuth app as described [below](#using-your-own-oauth-apps). > **Troubleshooting**: * **"Secured by Composio" badge won't go away:** This badge is removed when you use your own OAuth app. See [Using your own OAuth apps](#using-your-own-oauth-apps). * **Logo doesn't appear after uploading:** Clear browser cache or try incognito. * **Upload fails with "failed to fetch":** Retry or use a smaller image. # Using your own OAuth apps OAuth toolkits like Google and GitHub show a consent screen that says which app is requesting access. By default this reads "Composio wants to connect to your account." To show your app name instead, create a custom auth config with your own OAuth credentials and pass that auth config when creating a session. > **You don't need this for every toolkit**: Only white-label toolkits where users see a consent screen (Google, GitHub, Slack, etc.). Toolkits that use API keys don't show consent screens, so there's nothing to white-label. You can mix and match freely. - [Managed vs custom auth](/docs/custom-app-vs-managed-app): Decide when to use custom credentials, create an auth config, and pass it to sessions. ## Switching from Composio-managed to your own OAuth app Existing connected accounts are tied to the auth config they were created with. Switching to a custom auth config affects new connections for that toolkit; existing users keep using their current connected accounts until they re-authenticate or you import/migrate their credentials. * To use the custom config for new connections, pass `authConfigs` when creating or updating the session. * Existing connections continue refreshing with their original auth config. * To fully migrate an existing user, delete the old connected account and have them re-authenticate with the new auth config, or import their credentials into the new config where supported. # Routing the callback through your domain During OAuth, the browser briefly redirects through `backend.composio.dev` so Composio can capture the auth token. Some toolkits also display this URL on the consent screen. If you need to hide Composio's domain, you can proxy the redirect through your own domain instead. #### Set the redirect URI to your domain In your OAuth app's settings, set the authorized redirect URI to your own endpoint: ``` https://yourdomain.com/api/composio-redirect ``` #### Create a proxy endpoint This endpoint receives the OAuth callback and immediately 302-redirects it to Composio: **Python:** ```python from fastapi import FastAPI, Request from fastapi.responses import RedirectResponse app = FastAPI() @app.get("/api/composio-redirect") def composio_redirect(request: Request): composio_url = "https://backend.composio.dev/api/v3.1/toolkits/auth/callback" return RedirectResponse(url=f"{composio_url}?{request.url.query}") ``` **TypeScript:** ```typescript // pages/api/composio-redirect.ts (Next.js) import type { NextApiRequest, NextApiResponse } from "next"; export default function handler(req: NextApiRequest, res: NextApiResponse) { const composioUrl = "https://backend.composio.dev/api/v3.1/toolkits/auth/callback"; const params = new URLSearchParams(req.query as Record); res.redirect(302, `${composioUrl}?${params.toString()}`); } ``` > Your endpoint must return a **302 redirect**. Do not follow the redirect server-side or make a fetch call to Composio. The user's browser needs to be redirected so the OAuth flow completes correctly. #### Update your auth config In the Composio dashboard, update your auth config to use your custom redirect URI. ![Auth Config Settings](/images/custom-redirect-uri.png) *Setting the custom redirect URI in your auth config* Here's how the redirect flow works. Your proxy just forwards the browser redirect to Composio. It never touches the authorization code or token. > For FAQs and setup guides for individual toolkits, browse the [toolkits page](/toolkits). # Redirecting users after authentication By default, after OAuth completes, users land on a Composio-hosted success page that shows Composio branding. To bypass this page and send users to your own domain instead, pass a `callbackUrl` when calling `session.authorize()`: **Python:** ```python connection_request = session.authorize( "gmail", callback_url="https://your-app.com/callback" ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123"); const connectionRequest = await session.authorize("gmail", { callbackUrl: "https://your-app.com/callback", }); ``` After authentication, Composio redirects the user to your callback URL instead of the default success page. For full details on the parameters appended to your callback URL, see [Manually authenticating users → Redirecting users after authentication](/docs/manually-authenticating#redirecting-users-after-authentication). # Next - [Managed vs custom auth](/docs/custom-app-vs-managed-app): Set up auth configs for OAuth apps, API keys, and toolkits without managed auth --- # Triggers (/docs/triggers) When something happens in a connected app (a new Slack message, a GitHub commit, an incoming email), a **trigger** sends that event to your app as a structured payload. You write the handler. Composio handles the connection to the provider, delivery, retries, and signing. # Where events arrive Composio delivers every event to one destination you control: your webhook URL. You register it once per project, and Composio `POST`s every trigger event there, signed so you can verify it. # Realtime vs polling Under the hood, Composio learns about events in one of two ways. You don't configure this. It's a property of the trigger type, and it only affects how quickly an event reaches you. | Kind | How Composio learns about the event | Latency | Examples | | ------------ | ---------------------------------------------------------------- | --------------------------------------- | ----------------------------- | | **Realtime** | The provider pushes the event to Composio the moment it happens. | Near-instant | Slack, Asana, Notion, Outlook | | **Polling** | Composio checks the provider on a schedule. | Up to \~15 min on Composio-managed auth | Gmail, Google Calendar | Either way, the event lands in the same place: your subscription or webhook URL, in the same payload shape. > If you **bring your own OAuth app**, some providers only deliver to URLs registered on that app, so you register Composio's ingress URL there once. See [Custom OAuth webhooks](/docs/setting-up-triggers/custom-oauth-webhooks). # Trigger types and instances A **trigger type** is a kind of event you can listen for, like `GITHUB_COMMIT_EVENT` or a new Slack message. Each toolkit has its own set. A **trigger instance** is a trigger type you've activated for one [user's connected account](/docs/how-composio-works). It has its own `ti_*` ID that you can enable, disable, or delete independently. # Working with triggers 0. **Authenticate** the user for the toolkit: an [auth config](/docs/authentication#behind-the-scenes) and a [connected account](/docs/authentication). See [Authentication](/docs/authentication). 1. **Create** a trigger for the user's connected account. See [Creating triggers](/docs/setting-up-triggers/creating-triggers). 2. **Receive** its events: locally with `subscribe()`, or in production at your webhook URL. See [Receiving events](/docs/setting-up-triggers/subscribing-to-events). 3. **Manage** triggers: enable, disable, or delete. See [Managing triggers](/docs/setting-up-triggers/managing-triggers). # Next - [Creating triggers](/docs/setting-up-triggers/creating-triggers): Activate a trigger for a user so events start flowing --- # Creating triggers (/docs/setting-up-triggers/creating-triggers) A trigger watches for one event (like `GITHUB_COMMIT_EVENT`) on one user's connected account. Create one, and events start flowing to your [subscription or webhook URL](/docs/setting-up-triggers/subscribing-to-events). For the bigger picture, see [Triggers](/docs/triggers). The user needs a [connected account](/docs/authentication) for the toolkit you want to monitor. See [Authentication](/docs/authentication) if you haven't set that up. # Inspect the trigger type Each trigger type declares the config it needs. Check it before you create, so you pass the right fields. **Python:** ```python from composio import Composio composio = Composio() trigger_type = composio.triggers.get_type("GITHUB_COMMIT_EVENT") print(trigger_type.config) # {"properties": {"owner": {...}, "repo": {...}}, "required": ["owner", "repo"]} ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const triggerType = await composio.triggers.getType('GITHUB_COMMIT_EVENT'); console.log(triggerType.config); // {"properties": {"owner": {...}, "repo": {...}}, "required": ["owner", "repo"]} ``` # Create the trigger Pass the user, the trigger slug, and the config the type requires. **Python:** ```python from composio import Composio composio = Composio() user_id = "user-id-123435" # The user needs a connected account for this toolkit before this runs. # Set it up first. See /docs/authentication. trigger = composio.triggers.create( slug="GITHUB_COMMIT_EVENT", user_id=user_id, trigger_config={"owner": "your-repo-owner", "repo": "your-repo-name"}, ) print(f"Trigger created: {trigger.trigger_id}") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const userId = 'user-id-123435'; // The user needs a connected account for this toolkit before this runs. // Set it up first. See /docs/authentication. const trigger = await composio.triggers.create(userId, 'GITHUB_COMMIT_EVENT', { triggerConfig: { owner: 'your-repo-owner', repo: 'your-repo-name' }, }); console.log(`Trigger created: ${trigger.triggerId}`); ``` You only pass a `user_id`. Composio resolves that user's connected account for the toolkit automatically. ## Targeting a specific connected account If a user has more than one connected account for the toolkit, Composio uses the first active connection for the user and the trigger's toolkit. Pass a connected account ID to pick exactly which account the trigger watches. **Python:** ```python trigger = composio.triggers.create( slug="GITHUB_COMMIT_EVENT", user_id=user_id, connected_account_id="ca_def456", trigger_config={"owner": "your-repo-owner", "repo": "your-repo-name"}, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const userId = 'user-id-123435'; const trigger = await composio.triggers.create(userId, 'GITHUB_COMMIT_EVENT', { connectedAccountId: 'ca_def456', triggerConfig: { owner: 'your-repo-owner', repo: 'your-repo-name' }, }); ``` Trigger instances default to the `'latest'` toolkit version. If you parse payloads against a fixed schema, [pin a version](/docs/tools-direct/toolkit-versioning#choosing-between-latest-and-a-pinned-version) at SDK initialization. That's it. The trigger is active. Next, [receive its events](/docs/setting-up-triggers/subscribing-to-events). # Next - [Receiving events](/docs/setting-up-triggers/subscribing-to-events): Get trigger events locally with the SDK or in production over your webhook URL --- # Receiving events (/docs/setting-up-triggers/subscribing-to-events) Once a trigger is active, its events come to you as the same payload, whether you're testing locally or running in production. Develop against your local handler first, then point Composio at your production URL when you ship. # Receive events locally While developing, you want trigger events on your machine. The best option forwards them to the real webhook handler you'll run in production, so you test the exact path (including `parse()` and signature verification) before you ship. ## Quick look with `subscribe()` `subscribe()` streams events straight to your process over a WebSocket, with no webhook URL, no tunnel, and no signing. It's the fastest way to eyeball what a trigger sends, but it bypasses your real webhook handler. Use it only for basic prototyping; for anything you intend to ship, forward events to your handler with one of the options below. > Under the hood `subscribe()` opens the WebSocket via [Pusher](https://pusher.com/). This is an implementation detail, but worth knowing if your runtime restricts WebSocket clients — prefer the webhook/forwarding options below for anything you ship. **Python:** ```python from composio import Composio composio = Composio() subscription = composio.triggers.subscribe() @subscription.handle(trigger_id="your_trigger_id") def handle_event(data): print(f"Event received: {data}") subscription.wait_forever() ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); await composio.triggers.subscribe( data => { console.log('Event received:', data); }, { triggerId: 'your_trigger_id' } ); ``` Filter the stream by `triggerId`, `triggerSlug`, `connectedAccountId`, `toolkits`, or `userId`, or pass no filters to receive every trigger event in the project. ## Forward to your local handler with the CLI (recommended) The Composio CLI streams realtime events and forwards each one to your local URL, signed exactly like production. No public URL, no tunnel, and it runs your real handler (and [`parse()`](#handling-events)) end to end. ```bash composio dev triggers listen --forward "http://localhost:8000/webhooks/composio" ``` Events are signed with `COMPOSIO_WEBHOOK_SECRET` if it's set, otherwise the CLI prints a generated secret to verify against. Filter the stream with `--toolkits`, `--trigger-slug`, or `--trigger-id`, and tee events to a file with `--out events.jsonl`. ## Cloudflare Tunnel Expose your local server with [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/), no account needed for quick runs: ```bash cloudflared tunnel --url http://localhost:8000 ``` Register the printed `trycloudflare.com` URL as your [webhook URL](#receive-events-in-production), then events reach your handler at `http://localhost:8000/webhooks/composio`. ## ngrok Expose your local server with [ngrok](https://ngrok.com): ```bash ngrok http 8000 ``` Register the printed `ngrok-free.app` URL as your [webhook URL](#receive-events-in-production) the same way. # Receive events in production Register your webhook URL once per project. Composio then `POST`s every trigger event to it. Set it from the SDK: **Python:** ```python from composio import Composio composio = Composio() subscription = composio.triggers.set_webhook_subscription( webhook_url="https://your-app.com/webhooks/composio", ) print(f"Delivering events to {subscription['webhook_url']}") # Store subscription['secret'] to verify signatures ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const subscription = await composio.triggers.setWebhookSubscription({ webhookUrl: 'https://your-app.com/webhooks/composio', }); console.log(`Delivering events to ${subscription.webhookUrl}`); // Store subscription.secret to verify signatures ``` Your webhook endpoint must be publicly reachable. Composio's outbound IPs are dynamic, so IP allowlists and VPN-only endpoints won't work. Authenticate payloads with [signature verification](#verifying-signatures) instead. # Handling events In your handler, pass the incoming request to `parse()`. It returns the typed, normalized payload. Pass `verifySecret` and it verifies the signature first, so one call both authenticates and parses. **Python:** ```python import os from composio import Composio composio = Composio() @app.post("/webhooks/composio") async def webhook_handler(request: Request): # On async frameworks (FastAPI) read the raw body and pass body=/headers=. # On sync frameworks (Flask, Django) you can pass the request directly. # Use the raw body so the signature verifies. Omit verify_secret to skip it. result = composio.triggers.parse( body=await request.body(), headers=request.headers, verify_secret=os.environ["COMPOSIO_WEBHOOK_SECRET"], ) if result["raw_payload"]["type"] == "composio.trigger.message": event = result["payload"] if event["trigger_slug"] == "GITHUB_COMMIT_EVENT": data = event["payload"] print(f"New commit by {data['author']}: {data['message']}") return {"status": "ok"} ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); // Next.js App Router, Hono, or any Fetch-style handler export async function POST(request: Request) { // parse() takes a Fetch Request (shown) or an Express-style { body, headers }. // Pass the raw body so the signature verifies; omit verifySecret to skip it. const result = await composio.triggers.parse(request, { verifySecret: process.env.COMPOSIO_WEBHOOK_SECRET, }); if (result.rawPayload.type === 'composio.trigger.message') { const event = result.payload; if (event.triggerSlug === 'GITHUB_COMMIT_EVENT') { const data = event.payload; console.log(`New commit by ${data.author}: ${data.message}`); } } return Response.json({ status: 'ok' }); } ``` > Composio delivers other project events (like [connection expiry](/docs/authentication#connection-lifecycle)) to this same URL. `parse()` returns those too. Route on `result.payload.triggerSlug` and ignore what you don't handle. ## Inspecting trigger payload schemas Each trigger type declares the shape of the `data` it sends. Inspect it before you write your handler: **Python:** ```python from composio import Composio composio = Composio() trigger_type = composio.triggers.get_type("GITHUB_COMMIT_EVENT") print(trigger_type.payload) # {"properties": {"author": {...}, "id": {...}, "message": {...}, "timestamp": {...}, "url": {...}}} ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const triggerType = await composio.triggers.getType('GITHUB_COMMIT_EVENT'); console.log(triggerType.payload); // {"properties": {"author": {...}, "id": {...}, "message": {...}, "timestamp": {...}, "url": {...}}} ``` From the CLI, inspect a single trigger type's config and payload schema: ```bash composio triggers info "GITHUB_COMMIT_EVENT" ``` Or generate typed stubs for your project (scope to the toolkits you use with `--toolkits`), so your handler is type-checked against the trigger's payload: ```bash composio generate --toolkits github # auto-detects TypeScript or Python ``` ## Webhook payload shape Every trigger event arrives in the same envelope. `metadata` tells you where the event came from; `data` holds the event itself, in the shape the trigger type declares. ```json { "id": "msg_abc123", "type": "composio.trigger.message", "metadata": { "log_id": "log_abc123", "trigger_slug": "GITHUB_COMMIT_EVENT", "trigger_id": "ti_xyz789", "connected_account_id": "ca_def456", "auth_config_id": "ac_xyz789", "user_id": "user-id-123435" }, "data": { "commit_sha": "a1b2c3d", "message": "fix: resolve null pointer", "author": "jane" }, "timestamp": "2026-01-15T10:30:00Z" } ``` | `metadata` field | What it tells you | | ---------------------- | ---------------------------------------------------- | | `trigger_id` | Which trigger instance fired this event | | `trigger_slug` | The trigger type (for example `GITHUB_COMMIT_EVENT`) | | `connected_account_id` | Which connected account it belongs to | | `user_id` | Which user it's for | | `auth_config_id` | Which auth config was used | > This is the V3 payload, the default for new organizations. See [webhook payload versions](#webhook-payload-versions) for V2 and V1. # Verifying signatures Composio signs every webhook request. `parse({ verifySecret })` verifies the signature for you (and `verifyWebhook()` does the same at a lower level), so most handlers need nothing more. You only need this section if you're **not** using the Composio SDK. > Store the webhook secret securely as `COMPOSIO_WEBHOOK_SECRET`. Fetch it from the [webhook subscription](/reference/api-reference/webhook-subscriptions/getWebhookSubscriptionsById) any time, or [rotate it](/reference/api-reference/webhook-subscriptions/postWebhookSubscriptionsByIdRotateSecret) if it leaks. Every request includes `webhook-signature`, `webhook-id`, and `webhook-timestamp` headers. Compute `HMAC-SHA256` over `{webhook-id}.{webhook-timestamp}.{rawBody}` with your secret and compare it against the signature: **Python:** ```python import hmac import hashlib import base64 import json import os def verify_webhook(webhook_id: str, webhook_timestamp: str, body: str, signature: str) -> dict: secret = os.getenv("COMPOSIO_WEBHOOK_SECRET", "") signing_string = f"{webhook_id}.{webhook_timestamp}.{body}" expected = base64.b64encode( hmac.new(secret.encode(), signing_string.encode(), hashlib.sha256).digest() ).decode() received = signature.split(",", 1)[1] if "," in signature else signature if not hmac.compare_digest(expected, received): raise ValueError("Invalid webhook signature") payload = json.loads(body) # V3 payload return { "trigger_slug": payload["metadata"]["trigger_slug"], "data": payload["data"], } ``` **TypeScript:** ```typescript import crypto from 'crypto'; function verifyWebhook( webhookId: string, webhookTimestamp: string, body: string, signature: string ) { const secret = process.env.COMPOSIO_WEBHOOK_SECRET ?? ''; const signingString = `${webhookId}.${webhookTimestamp}.${body}`; const expected = crypto .createHmac('sha256', secret) .update(signingString) .digest('base64'); const received = signature.split(',')[1] ?? signature; if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))) { throw new Error('Invalid webhook signature'); } const payload = JSON.parse(body); // V3 payload return { triggerSlug: payload.metadata.trigger_slug, data: payload.data, }; } ``` > Reject requests whose `webhook-timestamp` is too old to block replays. The SDK's `parse()` and `verifyWebhook()` enforce a 300-second tolerance by default; pass `tolerance` to change it, or `0` to disable the check. # Webhook payload versions `parse()` and `verifyWebhook()` auto-detect the version. If you process payloads manually, here are the formats: **V3 (default):** Metadata is separated from event data. New organizations receive V3 payloads by default. ```json { "id": "msg_abc123", "type": "composio.trigger.message", "metadata": { "log_id": "log_abc123", "trigger_slug": "GITHUB_COMMIT_EVENT", "trigger_id": "ti_xyz789", "connected_account_id": "ca_def456", "auth_config_id": "ac_xyz789", "user_id": "user-id-123435" }, "data": { "commit_sha": "a1b2c3d", "message": "fix: resolve null pointer", "author": "jane" }, "timestamp": "2026-01-15T10:30:00Z" } ``` **V2 (legacy):** Metadata fields are mixed into the `data` object alongside event data. ```json { "type": "github_commit_event", "data": { "commit_sha": "a1b2c3d", "message": "fix: resolve null pointer", "author": "jane", "connection_id": "ca_def456", "connection_nano_id": "cn_abc123", "trigger_nano_id": "tn_xyz789", "trigger_id": "ti_xyz789", "user_id": "user-id-123435" }, "timestamp": "2026-01-15T10:30:00Z", "log_id": "log_abc123" } ``` **V1 (legacy):** ```json { "trigger_name": "github_commit_event", "trigger_id": "ti_xyz789", "connection_id": "ca_def456", "payload": { "commit_sha": "a1b2c3d", "message": "fix: resolve null pointer", "author": "jane" }, "log_id": "log_abc123" } ``` # Next - [Managing triggers](/docs/setting-up-triggers/managing-triggers): List, enable, disable, and delete trigger instances --- # Managing triggers (/docs/setting-up-triggers/managing-triggers) After a trigger is created, you manage it over its lifecycle: list active instances, pause one with `disable()`, bring it back with `enable()`, or remove it for good with `delete()`. # Listing active triggers List the trigger instances you've created. Results are cursor-paginated. **Python:** ```python from composio import Composio composio = Composio() active = composio.triggers.list_active( connected_account_ids=["ca_def456"], ) for trigger in active.items: print(f"{trigger.id} ({trigger.trigger_name}) - disabled: {trigger.disabled_at is not None}") # Paginate with cursor if active.next_cursor: next_page = composio.triggers.list_active(cursor=active.next_cursor) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const active = await composio.triggers.listActive({ connectedAccountIds: ['ca_def456'], }); for (const trigger of active.items) { console.log(`${trigger.id} (${trigger.triggerName}) - disabled: ${trigger.disabledAt !== null}`); } // Paginate with cursor if (active.nextCursor) { const nextPage = await composio.triggers.listActive({ cursor: active.nextCursor }); } ``` | Filter | Description | | ----------------------------------------------- | -------------------------------------------- | | `connected_account_ids` / `connectedAccountIds` | Array of connected account IDs | | `trigger_ids` / `triggerIds` | Array of trigger instance IDs | | `trigger_names` / `triggerNames` | Array of trigger type slugs | | `auth_config_ids` / `authConfigIds` | Array of auth config IDs | | `show_disabled` / `showDisabled` | Include disabled triggers (default: `false`) | # Enable / Disable triggers Pause a trigger temporarily without deleting it: **Python:** ```python # Disable a trigger composio.triggers.disable(trigger_id="ti_abcd123") # Re-enable when needed composio.triggers.enable(trigger_id="ti_abcd123") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); // Disable a trigger await composio.triggers.disable('ti_abcd123'); // Re-enable when needed await composio.triggers.enable('ti_abcd123'); ``` # Deleting triggers Permanently remove a trigger instance: **Python:** ```python composio.triggers.delete(trigger_id="ti_abcd123") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); await composio.triggers.delete('ti_abcd123'); ``` > Deleting a trigger is permanent. Use `disable()` instead to temporarily stop receiving events. --- # Custom OAuth webhooks (/docs/setting-up-triggers/custom-oauth-webhooks) This page is only for **realtime triggers where you bring your own OAuth app**. With Composio-managed OAuth, ingress is already set up and you can skip this entirely. Create the trigger and events flow. Some providers only deliver events to URLs you've registered on your OAuth app. When you bring your own app, you register Composio's ingress URL there once, so events can reach Composio. You need this only when the trigger type's `requires_webhook_endpoint_setup` flag is `true`. Each OAuth app you bring gets its own ingress URL within a project: ``` https://backend.composio.dev/api/v3.1/webhook_ingress/{toolkit_slug}/{we_xxx}/trigger_event ``` A single OAuth app can serve at most one Composio project: providers accept only one callback URL per OAuth app, and each ingress URL routes to a single project. In return, every project becomes its own webhook tenant, with: * **Its own ingress rate limit and backpressure budget** * **Project-scoped credentials**: the signing secret and app-level token you provide are stored against this project alone, never shared across projects. Repeat verification handshakes are rejected after the endpoint is verified, so the signing secret can't be silently swapped by a forged challenge. * **Clean fan-out**: events reach only that project's trigger instances * **Per-project metering** Every inbound event is signature-checked at ingress before any trigger fires: * **HMAC-SHA256** for Slack, **Ed25519** or shared-token matching for other providers * **Timestamp replay protection**: when the provider signs a request timestamp, requests outside the allowed skew window are rejected * **Unsigned or tampered requests** are rejected with `400` at ingress, so third parties can't spoof events onto your triggers > **Sharing one OAuth app across projects?** Consolidate to a single project or register separate OAuth apps per project before continuing. The walkthrough below uses Slack as the example and the [Webhook Endpoints API](/reference/api-reference/webhook-endpoints). For setup notes specific to each toolkit, see its FAQ section, for example [Slack](/toolkits/slack) or [Notion](/toolkits/notion). # Step 1: Discover what credentials the endpoint needs Call the schema endpoint for the toolkit. The `setup_fields` in the response tell you exactly what to collect from the provider's app dashboard. ```bash curl "https://backend.composio.dev/api/v3.1/webhook_endpoints/schema?toolkit_slug=slack" \ -H "x-api-key: " ``` Sample response: ```json { "toolkit_slug": "slack", "setup_fields": { "webhook_signing_secret": { "display_name": "Signing Secret", "description": "Webhook request signing secret from your Slack app dashboard", "is_required": true, "is_secret": true }, "app_token": { "display_name": "App-Level Token", "description": "Slack xapp- token with authorizations:read scope for event authorization", "is_required": true, "is_secret": true } } } ``` # Step 2: Create the endpoint ```bash curl -X POST "https://backend.composio.dev/api/v3.1/webhook_endpoints" \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "toolkit_slug": "slack", "client_id": "" }' ``` Sample response: ```json { "id": "we_abc123", "toolkit_slug": "slack", "client_id": "", "webhook_url": "https://backend.composio.dev/api/v3.1/webhook_ingress/slack/we_abc123/trigger_event", "data": null, "created_at": "2026-04-24T10:00:00.000Z" } ``` Hold on to two values from the response: `id` (used as `` below) and `webhook_url` (you'll paste this into the provider's app dashboard in step 4). The call is **idempotent per `(toolkit_slug, client_id)` within a project**. Calling it again with the same pair returns the existing endpoint without rotating the URL or wiping the secret. # Step 3: Store the credentials returned by the schema `PATCH` all the fields the schema returned in a single request. For Slack, that's the signing secret and (when needed) the app-level token together: ```bash curl -X PATCH "https://backend.composio.dev/api/v3.1/webhook_endpoints/" \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "data": { "webhook_signing_secret": "", "app_token": "xapp-..." } }' ``` For Slack, the credentials come from: * **Signing secret**: Slack app → Basic Information → App Credentials → Signing Secret. * **App-level token**: Slack app → Basic Information → App-Level Tokens, with scope `authorizations:read`. Required for direct messages, private channels, and reactions. Omit it if you only need public-channel events. > **Store the credentials before you switch the provider's callback URL in step 4.** If the provider posts to the URL without a secret on the endpoint, every request fails with `400`, and the provider may auto-disable the endpoint after a window of consecutive failures (Slack: \~36 hours). # Step 4: Point the provider's app dashboard at the URL Paste the `webhook_url` from step 2 into the provider's app dashboard: * **Slack** → Event Subscriptions → Request URL * **Notion** → Webhook Endpoints (in the integration's settings) For providers that issue a verification challenge on save (Slack `url_verification`, Notion's verification token, and so on), Composio responds automatically, with no handshake code on your side. Once the provider accepts the URL, go [create your trigger](/docs/setting-up-triggers/creating-triggers). # Updating an endpoint To rotate the signing secret or update any single field, `PATCH` it (other fields are preserved): ```bash curl -X PATCH "https://backend.composio.dev/api/v3.1/webhook_endpoints/" \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "data": { "webhook_signing_secret": "" } }' ``` To **replace** `data` wholesale (any field you don't include is cleared), `POST` to the same URL: ```bash curl -X POST "https://backend.composio.dev/api/v3.1/webhook_endpoints/" \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "data": { "webhook_signing_secret": "", "app_token": "" } }' ``` The `webhook_url` is immutable for the lifetime of the endpoint. Rotating the signing secret on the provider side is a `PATCH` on the existing endpoint, not a new one. To inspect a single endpoint: ```bash curl "https://backend.composio.dev/api/v3.1/webhook_endpoints/" \ -H "x-api-key: " ``` To list every endpoint in the current project: ```bash curl "https://backend.composio.dev/api/v3.1/webhook_endpoints" \ -H "x-api-key: " ``` # Next - [Creating triggers](/docs/setting-up-triggers/creating-triggers): Activate a trigger for a user so events start flowing --- # Composio CLI (/docs/cli) The Composio CLI gives Claude Code a local tool surface. From your terminal, Claude can connect apps, execute tools, inspect schemas, call authenticated APIs, and debug Composio projects while it works with you. It's the command-line side of [Composio for You](https://composio.dev/for-you). Reach for it when you want Claude to act in your connected apps directly, instead of pasting API keys, schemas, and one-off scripts into the chat. # Install ```bash curl -fsSL https://composio.dev/install | bash composio login ``` Open Claude Code in the project you want to work in. `composio login` installs the `composio-cli` skill for Claude Code by default. To install it manually: ```bash composio --install-skill composio-cli claude ``` Check your local state: ```bash composio whoami composio --version composio upgrade ``` # Knowledge work in Claude Code This is the recommended way to use Composio from inside Claude Code. The CLI executes tools, connects accounts, scripts workflows, calls authenticated APIs, and inspects trigger events, all without you wiring up a custom integration first. ## Search, connect, and execute tools Use this flow when Claude needs to act in one of your connected apps: ```bash # Find the right tool composio search "summarize my unread gmail" # Inspect the required input schema composio execute GMAIL_FETCH_EMAILS --get-schema # Connect the app if needed composio link gmail # Execute the tool composio execute GMAIL_FETCH_EMAILS \ -d '{ query: "is:unread newer_than:1d", max_results: 10 }' ``` The commands you'll reach for most: | Command | Use it for | | ------------------ | --------------------------------------------- | | `composio search` | Find relevant tools by natural language | | `composio execute` | Execute a known tool slug | | `composio link` | Connect an app account | | `composio proxy` | Call provider APIs with Composio-managed auth | Use `composio proxy` when Claude already knows the provider's API endpoint and just needs Composio to inject auth from your connected account: ```bash composio proxy https://gmail.googleapis.com/gmail/v1/users/me/profile --toolkit gmail ``` ## Run scripts and sub-agents Reach for `composio run` when Claude needs a multi-step workflow: loops, parallel fan-out, data transformation, or LLM-assisted summarization. It runs inline TS/JS or a file, with `execute()`, `search()`, `proxy()`, `experimental_subAgent()`, `result.prompt()`, and `z` injected. Run a single scripted workflow: ```bash composio run ' const messages = await execute("GMAIL_FETCH_EMAILS", { query: "is:unread newer_than:1d", max_results: 10, }); console.log(messages); ' ``` Fan out across multiple tools: ```bash composio run ' const [emails, issues, events] = await Promise.all([ execute("GMAIL_FETCH_EMAILS", { max_results: 5 }), execute("GITHUB_LIST_REPOSITORY_ISSUES", { owner: "composiohq", repo: "composio", state: "open" }), execute("GOOGLECALENDAR_FIND_EVENT", { calendar_id: "primary" }), ]); console.log({ emails: emails.data, issues: issues.data, events: events.data }); ' ``` Ask a sub-agent to summarize tool output and return structured data: ```bash composio run --logs-off ' const [emails, issues] = await Promise.all([ execute("GMAIL_FETCH_EMAILS", { max_results: 5 }), execute("GITHUB_LIST_REPOSITORY_ISSUES", { owner: "composiohq", repo: "composio", state: "open" }), ]); const brief = await experimental_subAgent( `Create a morning brief from these emails and issues.\n\n${emails.prompt()}\n\n${issues.prompt()}`, { schema: z.object({ brief: z.string(), urgentEmails: z.array(z.string()), urgentIssues: z.array(z.string()), }), } ); console.log(brief.structuredOutput); ' ``` Run a checked-in script: ```bash composio run --file ./workflow.ts -- --repo composiohq/composio ``` ## Listen to trigger events Use trigger listening when Claude needs to wait for new events, inspect incoming payloads, or forward events while debugging. Event streaming lives in the developer namespace: ```bash # Compact table view for matching events composio dev listen --toolkits gmail --table # Raw JSON payloads, then stop after five events composio dev listen --trigger-slug GMAIL_NEW_GMAIL_MESSAGE --json --max-events 5 # Forward each matching event to a local or hosted webhook composio dev listen --toolkits github --forward https://example.com/webhook # Append matching events to a local file for Claude to inspect composio dev listen --toolkits slack --out ./events.jsonl ``` Filter by toolkit, trigger slug, trigger ID, connected account ID, or userID to focus Claude on a single event source. # Build on the Composio platform Use these commands while building on the Composio developer platform. They initialize local project context, create auth configs, manage connected accounts, test tool execution, inspect logs, and debug trigger flows. ## Initialize project context ```bash # Initialize local project context composio dev init # Toggle developer mode composio dev --mode on composio dev --mode off # Switch or inspect project scope composio dev projects list composio dev projects switch ``` ## Inspect toolkits and versions ```bash composio dev toolkits list composio dev toolkits search "email" composio dev toolkits info github composio dev toolkits version github ``` ## Create and inspect auth configs ```bash # List existing auth configs composio dev auth-configs list composio dev auth-configs list --toolkits github,gmail composio dev auth-configs info ac_xxx # Create an auth config from provider credentials composio dev auth-configs create "GitHub OAuth" \ --toolkit github \ --auth-scheme OAUTH2 \ --scopes "repo,user" \ --custom-credentials '{ "client_id": "...", "client_secret": "..." }' ``` ## Manage connected accounts Top-level `composio link` is the fastest path for personal knowledge work. Use the developer connected-account commands when you're building against project users, auth configs, and playground flows. ```bash composio dev connected-accounts list composio dev connected-accounts list --toolkits github --user-id user_123 composio dev connected-accounts list --status ACTIVE --limit 20 composio dev connected-accounts info ca_xxx composio dev connected-accounts whoami ca_xxx composio dev connected-accounts link ``` ## Execute and inspect logs ```bash # Execute a tool through the developer playground path composio dev playground-execute GMAIL_SEND_EMAIL \ -d '{ recipient_email: "you@example.com", subject: "Test", body: "Hello" }' # Inspect tool and trigger logs composio dev logs tools --toolkit gmail --limit 20 composio dev logs tools log_xxx composio dev logs triggers --limit 20 ``` ## Work with triggers ```bash composio dev triggers list gmail composio dev triggers info GMAIL_NEW_GMAIL_MESSAGE composio dev triggers status composio dev triggers create composio dev triggers enable ti_xxx composio dev listen --trigger-slug GMAIL_NEW_GMAIL_MESSAGE --json --max-events 5 ``` ## Generate type definitions For legacy direct tool execution projects, generate local TypeScript or Python types from tool schemas: ```bash composio generate composio generate ts --toolkits github,gmail composio generate py --toolkits github,gmail ``` Reach for this section when you're debugging auth configs, connected accounts, trigger delivery, or tool execution in a Composio project. For user-facing app development, start with the SDK and session docs and keep the CLI as a local debugging companion. # Building on top of the CLI > Don't build a production integration on top of the CLI. It's in constant development, and Composio doesn't offer CLI-level SLAs as an application runtime contract. For a stable integration, build on the Composio SDKs and APIs instead. That said, the CLI works well as a bootstrap or helper layer for agent-native products: * Use `composio connections list` to inspect which connected accounts are available locally. * Use `composio run` or `composio proxy` for internal automations where CLI churn is acceptable. For an example of a product built around CLI-driven agent workflows, see [Houston](https://github.com/gethouston/houston). # Help Use `--help` on the root command or any subcommand: ```bash composio --help composio --help full composio execute --help full composio run --help full composio dev --help full ``` --- # Migration guides (/docs/migration-guide) > _Written December 2025._ # Available migration guides **Migrating from Direct Tools to Sessions** Move from manual tool fetching and execution to the sessions paradigm. Your existing auth configs and connected accounts carry over. [View Direct Tools migration guide →](/docs/migration-guide/direct-to-sessions) **Migrating from MCP servers to Sessions** Move from per-toolkit MCP server configs (`composio.mcp.create` / `composio.mcp.generate`) to sessions. Your tools, auth configs, and connected accounts carry over — no re-auth. [View MCP servers migration guide →](/docs/migration-guide/mcp-servers-to-sessions) **Migrating from Experimental Tool Router** Migrate from `composio.experimental.tool_router` to the stable sessions API. [View Tool Router migration guide →](/docs/migration-guide/tool-router-beta) **Toolkit versioning migration** Migrate to use the toolkit versioning system. [View versioning migration guide →](/docs/migration-guide/toolkit-versioning) **New SDK migration** Migrate from the old Composio SDK to the new SDK, including breaking changes, new features, and updated APIs. [View SDK migration guide →](/docs/migration-guide/new-sdk) --- # Migrating from Direct Tools to Sessions (/docs/migration-guide/direct-to-sessions) > _Written February 2026._ This guide is for developers who are using Composio's **direct tool execution** pattern and want to migrate to **sessions**. For a comparison of the two patterns, see [Sessions vs Direct Execution](/docs/sessions-vs-direct-execution). With sessions, you don't need to manage tool fetching, authentication, or execution yourself. You create a session and it handles everything. Your existing auth configs and connected accounts carry over, so your users don't need to re-authenticate. If you're starting fresh, use [Configuring Sessions](/docs/configuring-sessions) instead. # What changes | | Direct tools | Sessions | | -------------------- | ------------------------------------------------- | ---------------------------------------------------------------------- | | **Tool discovery** | You fetch specific tools by name/toolkit | Agent discovers tools at runtime via meta tools | | **Authentication** | You manage connect links and wait for connections | In-chat auth prompts users automatically, or use `session.authorize()` | | **Execution** | You call `tools.execute()` with version pinning | Agent executes tools through `COMPOSIO_MULTI_EXECUTE_TOOL` | | **Toolkit versions** | You manage versions manually | Handled automatically | # Migrating #### Pass your existing auth configs to the session You already have auth configs (e.g. `ac_github_config`, `ac_slack_config`). Pass them when creating a session so it uses your existing OAuth credentials and your users' connected accounts carry over. **Python:** ```python from composio import Composio composio = Composio() # Before: manual tool fetching with user_id # tools = composio.tools.get(user_id="user_123", toolkits=["GITHUB", "SLACK"]) # After: create a session with your existing auth configs session = composio.create( user_id="user_123", auth_configs={ "github": "ac_your_github_config", "slack": "ac_your_slack_config" } ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); // Before: manual tool fetching with userId // const tools = await composio.tools.get("user_123", { toolkits: ["GITHUB", "SLACK"] }); // After: create a session with your existing auth configs const session = await composio.create("user_123", { authConfigs: { github: "ac_your_github_config", slack: "ac_your_slack_config", }, }); ``` Since the session uses the same `user_id` and auth configs, it automatically picks up existing connected accounts. No re-authentication needed. #### Replace manual tool fetching with session tools Instead of fetching specific tools by name, get the session's meta tools. These let the agent discover, authenticate, and execute any tool at runtime. **Python:** ```python # Before: manually specifying which tools to fetch # tools = composio.tools.get(user_id="user_123", toolkits=["GITHUB"]) # After: session provides meta tools that handle discovery tools = session.tools() ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123"); // Before: manually specifying which tools to fetch // const tools = await composio.tools.get("user_123", { toolkits: ["GITHUB"] }); // After: session provides meta tools that handle discovery const tools = await session.tools(); ``` #### Remove manual auth flows If you were manually creating connect links and waiting for connections, sessions handle this automatically. When a tool requires authentication, the agent prompts the user with a connect link in-chat. **Python:** ```python # Before: manual auth flow # connection_request = composio.connected_accounts.link( # user_id="user_123", # auth_config_id="ac_your_github_config", # callback_url="https://your-app.com/callback" # ) # redirect_url = connection_request.redirect_url # After: auth happens automatically in-chat # Or if you need to pre-authenticate outside of chat: connection_request = session.authorize("github") print(connection_request.redirect_url) connected_account = connection_request.wait_for_connection() ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123"); // Before: manual auth flow // const connectionRequest = await composio.connectedAccounts.link("user_123", "ac_your_github_config", { // callbackUrl: "https://your-app.com/callback" // }); // const redirectUrl = connectionRequest.redirectUrl; // After: auth happens automatically in-chat // Or if you need to pre-authenticate outside of chat: const connectionRequest = await session.authorize("github", { callbackUrl: "https://your-app.com/callback", }); console.log(connectionRequest.redirectUrl); const connectedAccount = await connectionRequest.waitForConnection(); ``` #### Remove toolkit version management If you were pinning toolkit versions in your code or environment variables, you can remove that. Sessions handle versioning automatically. **Python:** ```python # Before: manual version pinning # composio = Composio( # toolkit_versions={ # "github": "20251027_00", # "slack": "20251027_00", # } # ) # After: no version management needed composio = Composio() session = composio.create(user_id="user_123") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; // Before: manual version pinning // const composio = new Composio({ // toolkitVersions: { // github: "20251027_00", // slack: "20251027_00", // }, // }); // After: no version management needed const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123"); ``` # Restricting toolkits If you were fetching tools from specific toolkits, you can restrict the session to only those toolkits: **Python:** ```python session = composio.create( user_id="user_123", toolkits=["github", "gmail", "slack"], auth_configs={ "github": "ac_your_github_config", "gmail": "ac_your_gmail_config", "slack": "ac_your_slack_config" } ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { toolkits: ["github", "gmail", "slack"], authConfigs: { github: "ac_your_github_config", gmail: "ac_your_gmail_config", slack: "ac_your_slack_config", }, }); ``` # Multiple connected accounts If your users have multiple accounts for the same toolkit (e.g., work and personal Gmail), you can specify which one to use per session: **Python:** ```python session = composio.create( user_id="user_123", auth_configs={ "gmail": "ac_your_gmail_config" }, connected_accounts={ "gmail": ["ca_work_gmail"] } ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create("user_123", { authConfigs: { gmail: "ac_your_gmail_config", }, connectedAccounts: { gmail: ["ca_work_gmail"], }, }); ``` If you don't specify, the most recently connected account is used. See [Managing multiple connected accounts](/docs/managing-multiple-connected-accounts) for details. # Triggers Triggers don't work with sessions yet. Continue using them the same way you do today with `composio.triggers.create()`, `composio.triggers.enable()`, and webhook subscriptions. See [Triggers](/docs/triggers) for setup instructions. # White labeling If you've set up white-labeled OAuth screens with custom auth configs, those carry over automatically. Just pass the same auth config IDs to your session via `auth_configs` / `authConfigs`. Your users will continue to see your branding on consent screens. See [White-labeling authentication](/docs/white-labeling-authentication) for more. # Next - [Configuring Sessions](/docs/configuring-sessions): Toolkits, auth configs, account selection, and session methods --- # Migrating from MCP servers to Sessions (/docs/migration-guide/mcp-servers-to-sessions) > _Written June 2026._ This guide is for developers using Composio's **MCP servers** (`composio.mcp.create` / `composio.mcp.generate`, the "Single Toolkit MCP" flow) who want to migrate to **sessions**. Sessions are the next generation of the same idea: you still get an MCP URL that any MCP-compatible client connects to — but instead of standing up and managing a separate server config per toolkit, you create a session that handles tool discovery, authentication, context, and versioning for you. > Starting fresh? Skip this guide and read [Configuring Sessions](/docs/configuring-sessions). > Just connecting apps to your own agent for personal use — not building an app? You don't need the SDK. Use **Composio For You** to connect across 1000+ apps in a few clicks — switch to it from the product switcher in the top-left of the dashboard. Reserve `session.mcp.url` for programmatic, in-app use. # What carries over (you keep all of this) * **Your tools** — every tool you exposed on a server is available in a session. * **Your auth configs and connected accounts** — pass the same `ac_…` IDs; your users **do not re-authenticate**. * **The MCP URL pattern** — you still get a URL (`session.mcp.url`) that plugs into your agent — any MCP-compatible client — the same way, the same protocol. * **Per-user isolation** — still keyed by `user_id`. * **Tool restriction** — you can still pin a session to an exact, fixed tool list (see Step 3). # What changes | | MCP servers (today) | Sessions | | --------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | **Setup** | Create + manage a server config per toolkit (`composio.mcp.create`) | One `composio.create(user_id)` — no server object to manage | | **Get a URL** | `composio.mcp.generate(user_id, mcp_config_id)` → `instance.url` | `session.mcp.url` | | **Tools** | Fixed `allowed_tools` list, baked into the server | Dynamic discovery by default, **or** a fixed list (direct-tools preset) — your choice | | **Multiple toolkits** | One server per toolkit | One session spans many toolkits | | **Context** | All allowed tools always loaded | Managed — search/preload keep the agent's context lean | | **Versioning** | Manual | Handled automatically | | **Auth** | Pre-authenticate, then generate | Carries over; in-chat auth available, or `session.authorize()` | The *why*: a session is one managed endpoint that replaces N static server configs, gives the agent runtime tool discovery, and keeps context small — without losing the fixed-tool-list behavior you have today if that's what you want. # Migrating #### Replace the server config + generate with a session The two-step "create a server, then generate a per-user URL" collapses into a single `composio.create(...)`. Pass the **same** toolkit, auth config, and tool list you had on the server. **Python:** ```python from composio import Composio composio = Composio(api_key="YOUR_API_KEY") # Before: create a server config, then generate a per-user URL # server = composio.mcp.create( # name="my-gmail-server", # toolkits=[{"toolkit": "gmail", "auth_config": "ac_xyz123"}], # allowed_tools=["GMAIL_FETCH_EMAILS", "GMAIL_SEND_EMAIL"], # ) # instance = composio.mcp.generate(user_id="user-123", mcp_config_id=server.id) # mcp_url = instance["url"] # After: one session, same toolkit + auth config + tools session = composio.create( user_id="user-123", toolkits=["gmail"], auth_configs={"gmail": "ac_xyz123"}, tools={"gmail": {"enable": ["GMAIL_FETCH_EMAILS", "GMAIL_SEND_EMAIL"]}}, mcp=True, ) mcp_url = session.mcp.url ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); // Before: create a server config, then generate a per-user URL // const server = await composio.mcp.create("my-gmail-server", { // toolkits: [{ toolkit: "gmail", authConfigId: "ac_xyz123" }], // allowedTools: ["GMAIL_FETCH_EMAILS", "GMAIL_SEND_EMAIL"], // }); // const instance = await composio.mcp.generate("user-123", server.id); // const mcpUrl = instance.url; // After: one session, same toolkit + auth config + tools const session = await composio.create("user-123", { toolkits: ["gmail"], authConfigs: { gmail: "ac_xyz123" }, tools: { gmail: { enable: ["GMAIL_FETCH_EMAILS", "GMAIL_SEND_EMAIL"] } }, mcp: true, }); const { mcp } = session; const mcpUrl = mcp.url; ``` Same `user_id` + same auth config → existing connected accounts are picked up automatically. No re-auth. #### Point your MCP client at the new URL Swap the generated server URL for `session.mcp.url` wherever your agent connects. Nothing else about the client changes. ```python # Before: https://backend.composio.dev/v3/mcp/?user_id= # After: session.mcp.url ``` #### Keep an exact, fixed tool list (optional — closest 1:1 with your server) By default a session gives the agent **dynamic** tool discovery (meta-tools). If you want the *same static behavior* as your old server — a fixed set of tools, no discovery — add the **direct-tools preset**. This preloads exactly the tools you enable and turns meta-tools off. **Python:** ```python from composio import Composio, SESSION_PRESET_DIRECT_TOOLS session = composio.create( user_id="user-123", toolkits=["gmail"], auth_configs={"gmail": "ac_xyz123"}, tools={"gmail": {"enable": ["GMAIL_FETCH_EMAILS", "GMAIL_SEND_EMAIL"]}}, session_preset=SESSION_PRESET_DIRECT_TOOLS, ) ``` **TypeScript:** ```typescript import { Composio, SessionPreset } from '@composio/core'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); const session = await composio.create("user-123", { toolkits: ["gmail"], authConfigs: { gmail: "ac_xyz123" }, tools: { gmail: { enable: ["GMAIL_FETCH_EMAILS", "GMAIL_SEND_EMAIL"] } }, sessionPreset: SessionPreset.DIRECT_TOOLS, }); ``` Leave the preset off to get the upgrade: the agent discovers and loads tools at runtime, so you can span many toolkits without bloating context. # Beyond the MCP URL A session isn't only an MCP endpoint — it's also a normal SDK object, which is handy for non-agent code paths. ## Native tools for your framework `session.tools()` returns provider-wrapped native tools for OpenAI, Anthropic, LangChain, the Vercel AI SDK, and others, so you skip manual schema wiring. With the direct-tools preset it returns your exact fixed tool set; by default it returns the meta-tools that drive runtime discovery. **Python:** ```python tools = session.tools() ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); const session = await composio.create("user-123"); const tools = await session.tools(); ``` ## Execute a tool without an LLM For a deterministic, non-agent path, call a tool directly on the session. **Python:** ```python result = session.execute( "GITHUB_CREATE_ISSUE", arguments={"owner": "my-org", "repo": "my-repo", "title": "Fix login bug"}, ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); const session = await composio.create("user-123"); const result = await session.execute("GITHUB_CREATE_ISSUE", { owner: "my-org", repo: "my-repo", title: "Fix login bug", }); ``` # Things to know * **Multiple toolkits on one endpoint** — where you ran several single-toolkit servers, one session spans them all (`toolkits=["gmail", "slack", "github"]`). Fewer moving parts. * **Reuse one session — don't create one per request** — sessions persist and are reusable. Create a session once and keep using it across calls; see [Configuring Sessions](/docs/configuring-sessions). * **Sharing one account across users** — sessions support **shared connections** (`account_type:"SHARED"` + an allow/deny ACL), pinned per session. See [Shared connections](/docs/shared-connections). * **Tenant-specific params** (SharePoint sub-site, Jira subdomain) — prefill them via shared credentials on the auth config. * **White-labeling carries over** — pass the same white-labeled auth config IDs; users keep seeing your branding on consent screens. See [White-labeling authentication](/docs/white-labeling-authentication). * **Triggers** — unchanged; continue using `composio.triggers.*` and webhooks (triggers aren't part of sessions yet). * **Dashboard** — sessions are created via the SDK. For no-code, personal app connections, use **Composio For You** — reachable from the product switcher in the top-left of the dashboard. # Next - [Configuring Sessions](/docs/configuring-sessions): Toolkits, auth configs, account selection, presets, and session methods --- # Migrating from Experimental Tool Router (/docs/migration-guide/tool-router-beta) > **Legacy · written January 2026.** This is a point-in-time migration/legacy guide and may describe outdated APIs. For current guidance, see https://docs.composio.dev. This guide is for users who adopted the **experimental tool router** (`composio.experimental.tool_router`) during its beta period. The tool router has now graduated to a stable, first-class feature called **sessions** — with a simpler API, better auth handling, and full framework support. If you never used `composio.experimental.tool_router`, you can skip this guide and start with [Configuring Sessions](/docs/configuring-sessions). # The basics #### Upgrade composio package Upgrade to the latest stable version: **Python:** ```bash pip install --upgrade composio ``` **TypeScript:** #### Update session creation **Python:** ```python # Beta (before) session = composio.experimental.tool_router.create_session( user_id="user@example.com" ) # Stable (after) session = composio.create( user_id="user@example.com" ) ``` **TypeScript:** ```typescript // Beta (before) const session = await composio.experimental.toolRouter.createSession('user_123'); // Stable (after) const session = await composio.create('user_123'); ``` #### Moving users (optional) If you have existing users on tool router and you don't want them to authenticate again: * Tool Router will auto-detect auth configs and connected accounts it created (from the beta version). * If you have custom auth configs (not created by Tool Router): * Search for the Auth config for that connected account. See [Connected Accounts](/docs/auth-configuration/connected-accounts) to fetch existing accounts programmatically. * While creating a session configure to use this Auth config. * You need to repeat this for each toolkit you want to enable for that session. **Python:** ```python session = composio.create( user_id="user_123", auth_configs={ "github": "ac_your_github_config", "slack": "ac_your_slack_config" } ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const session = await composio.create('user_123', { authConfigs: { github: "ac_your_github_config", slack: "ac_your_slack_config", }, } ); ``` # Next - [Configuring Sessions](/docs/configuring-sessions): Restrict toolkits, set auth configs, and select connected accounts --- # Toolkit versioning migration (/docs/migration-guide/toolkit-versioning) > _Written October 2025._ Starting with Python SDK v0.9.0 and TypeScript SDK v0.2.0, manual tool execution requires explicit version specification. This is a breaking change from earlier versions where toolkit versioning was optional. # Breaking change Manual tool execution now requires explicit version specification. The `tools.execute()` method will fail without a version. ## Before (will fail) **Python:** ```python # Raises ToolVersionRequiredError result = composio.tools.execute( "GITHUB_CREATE_ISSUE", {"user_id": "user-123", "arguments": {...}} ) ``` **TypeScript:** ```typescript // Throws ComposioToolVersionRequiredError const result = await composio.tools.execute({ toolSlug: "GITHUB_CREATE_ISSUE", userId: "user-123", arguments: {...} }); ``` ## After (required) Choose one of three migration strategies: ### Option 1: Configure version at SDK level **Python:** ```python from composio import Composio # Pin specific versions for each toolkit composio = Composio( api_key="YOUR_API_KEY", toolkit_versions={ "github": "20251027_00", "slack": "20251027_00", "gmail": "20251027_00" } ) ``` **TypeScript:** ```typescript import { Composio } from "@composio/core"; // Pin specific versions for each toolkit const composio = new Composio({ apiKey: "YOUR_API_KEY", toolkitVersions: { github: "20251027_00", slack: "20251027_00", gmail: "20251027_00" } }); ``` ### Option 2: Pass version with each execution **Python:** ```python # Specify version directly in execute call result = composio.tools.execute( "GITHUB_LIST_STARGAZERS", arguments={ "owner": "ComposioHQ", "repo": "composio" }, user_id="user-k7334", version="20251027_00" # Override version for this execution ) print(result) ``` **TypeScript:** ```typescript // Specify version directly in execute call const result = await composio.tools.execute("GITHUB_LIST_STARGAZERS", { userId: "user-k7334", arguments: { owner: "ComposioHQ", repo: "composio" }, version: "20251027_00" // Override version for this execution }); console.log(result); ``` ### Option 3: Use environment variables ```bash export COMPOSIO_TOOLKIT_VERSION_GITHUB="20251027_00" ``` # Migration checklist 1. **Audit your code**: Find all `tools.execute()` calls in your codebase 2. **Choose a strategy**: Select one of the three options above based on your needs 3. **Test thoroughly**: Verify tools work correctly with pinned versions 4. **Deploy gradually**: Roll out changes incrementally to minimize risk # Temporary workaround During migration, you can temporarily skip version checks (not recommended for production): **Python:** ```python result = composio.tools.execute( "GITHUB_CREATE_ISSUE", { "user_id": "user-123", "arguments": {...} }, dangerously_skip_version_check=True ) ``` **TypeScript:** ```typescript const result = await composio.tools.execute({ toolSlug: "GITHUB_CREATE_ISSUE", userId: "user-123", arguments: {...}, dangerouslySkipVersionCheck: true }); ``` > The `dangerouslySkipVersionCheck` flag is only for migration or debugging. Never use in production. # Next - [Migrate to sessions](/docs/migration-guide/direct-to-sessions): Move from older tool execution patterns to sessions --- # Our next generation SDKs (/docs/migration-guide/new-sdk) > **Legacy · written December 2025.** This is a point-in-time migration/legacy guide and may describe outdated APIs. For current guidance, see https://docs.composio.dev. > This guide covers migrating from the legacy SDK (v1) to the current SDK (v3). The recommended way to use Composio is now through **sessions** — see [Migrating from Direct Tools to Sessions](/docs/migration-guide/direct-to-sessions) or [Configuring Sessions](/docs/configuring-sessions) if you're starting fresh. In the last few months, we have experienced very rapid growth in usage of our platform. As such, our team has been working hard to radically improve the performance and developer experience of our platform. A lot of these changes have happened in the background, but we are excited to finally share our new SDKs with you that complement our new infra. The new API features improved usability, enhanced stability, and better scalability. The SDKs built on top of it simplify the developer experience, making it easier than ever to build useful agents. # What's new? A lot of the changes are on the infra side, but from the SDK point of view, here is what you can expect: * Faster and more reliable tool execution * A simpler but more opinionated SDK * Much more intuitive and consistent naming conventions * A vastly improved TypeScript SDK that is meaningfully more type-safe and has full feature parity with the Python SDK There aren't too many new flashy features here (yet) mainly because we wanted to get the bones right — but we feel we have a solid foundation to ship incredible new experiences on top very quickly. # State of the new SDK and what is happening with the old SDKs? Currently, the new SDKs are in a preview release. These new SDKs come almost fully formed, we do not expect many breaking changes to them but are releasing them in a preview state to get feedback and make necessary changes before locking them in. As we lock the new SDKs in place, we will deprecate support for the old SDKs. They will continue to work for the foreseeable future but are no longer actively maintained. We will continue to push security updates and fix any critical bugs but will not support any new functionality in them. We urge you to upgrade to the new SDKs as soon as possible. # Nomenclature We have updated several key terms in the SDK and API to improve clarity and consistency. The following table summarizes these changes: | Previous Term | Current Term | Definition | | ------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | | Actions | Tools | Individual operations or capabilities that can be performed by an LLM agent | | Apps | Toolkits | A collection of tools grouped under a single application | | Integration | Auth Config | Configuration containing developer credentials and application-level settings such as scopes and API endpoints. Scoped to a toolkit. | | Connection | Connected accounts | User-linked accounts associated with a toolkit | | Entity ID | userID | The identifier of the user performing the action (UUID or email) | | Trigger | Trigger | An event that can be subscribed to | | Toolsets | Providers | LLM or agent framework that can be used with Composio to create agents | # Switch to nano IDs from UUIDs We have transitioned from UUIDs to nano IDs throughout the platform for the following reasons: * **Improved readability**: UUIDs are lengthy and difficult to read * **Better usability**: Easier to copy with a single double-click * **Better organization**: Nano IDs allow us to distinguish between different resource types through prefixes | Feature | Nano ID Prefix | Example | | ----------------- | -------------- | ----------------- | | Connected Account | `ca_` | `ca_8x9w2l3k5m` | | Auth Config | `ac_` | `ac_1234567890` | | Trigger | `ti_` | `ti_So9EQf8XnAcy` | > Nano IDs are short, unique, and prefixed to indicate the resource type. # SDK Changes Upgrade to the latest SDK version using the appropriate package manager: **Python:** ```bash pip install -U composio ``` **TypeScript:** Both SDKs now implement proper namespacing for each concept. ## UserID scoping The concept of `entity_id` has been expanded and renamed to `user_id`. All operations are now scoped to a userID, including: * Fetching tools * Initiating connections * Executing tools * Managing triggers This change provides explicit specification of the user for whom the action is being performed. When a user may have multiple accounts (such as work and personal Gmail connections), you can use the more specific connected account ID. ## Replacing ToolSets with Providers We have deprecated "toolsets" in favor of "providers". This change allows Composio to provide deeper standardization for tool implementation across different frameworks. Previously, you needed to import and use a framework-specific `ComposioToolSet` class: **Python (previous):** ```python from composio_openai import ComposioToolSet, Action, App from openai import OpenAI toolset = ComposioToolSet() ``` **TypeScript (previous):** ```typescript import { OpenAIToolSet } from 'composio-core'; const toolset = new OpenAIToolSet(); ``` The SDK structure is now framework-agnostic and includes the OpenAI provider out of the box: **Python (current):** ```python from composio import Composio # from composio_langchain import LangchainProvider composio = Composio() # composio = Composio(provider=LangchainProvider()) tools = composio.tools.get( user_id="0001", tools=["LINEAR_CREATE_LINEAR_ISSUE", "GITHUB_CREATE_COMMIT"] ) # tools returned is formatted for the provider. by default, OpenAI. ``` **TypeScript (current):** ```typescript import { Composio } from '@composio/core'; // import { VercelProvider } from '@composio/vercel'; const composio = new Composio({ // provider: new VercelProvider(), }); // Can specify other providers too, like OpenAI, Anthropic, Vercel AI SDK. const tools = await composio.tools.get('user@example.com', { tools: ['LINEAR_CREATE_LINEAR_ISSUE', 'GITHUB_CREATE_COMMIT'], }); // tools returned is formatted for the provider. by default, OpenAI. ``` You can now use the same tools across any framework with our unified interface, or create custom toolsets for frameworks we don't yet support. Read more about [providers in our documentation](/docs/providers/openai) and explore the [complete list of available providers](/docs/providers/openai). ## Fetching and filtering tools Previously, you could filter tools by: * Apps * Action names (tool names) * Tags You could also specify an `important` flag to retrieve the most important tools: **Python (previous):** ```python from composio_openai import ComposioToolSet, Action, App from openai import OpenAI toolset = ComposioToolSet() client = OpenAI() tools = toolset.get_tools( actions=[Action.GITHUB_GET_THE_AUTHENTICATED_USER], check_connected_accounts=True ) tools = toolset.get_tools(apps=[App.GITHUB, App.LINEAR, App.SLACK], check_connected_accounts=True) ``` **TypeScript (previous):** ```typescript import { OpenAIToolSet } from 'composio-core'; const toolset = new OpenAIToolSet(); const tools_1 = await toolset.getTools({ apps: ['GITHUB'] }); const tools_2 = await toolset.getTools({ actions: ['GITHUB_GET_THE_AUTHENTICATED_USER', 'LINEAR_CREATE_LINEAR_ISSUE'], }); ``` You can now filter tools by: * Toolkits * Tool slugs * Limit parameter * Search query The `important` flag has been removed. Instead, tools are returned in order of importance by default: > Since `user_id` is now explicitly required, the `check_connected_accounts` flag is no longer necessary. **Python (current):** ```python from composio import Composio composio = Composio() user_id = "user@acme.org" tools_1 = composio.tools.get(user_id=user_id, toolkits=["GITHUB", "LINEAR"]) tools_2 = composio.tools.get(user_id=user_id, toolkits=["SLACK"], limit=5) # Default limit=20 tools_3 = composio.tools.get( user_id=user_id, tools=["GITHUB_CREATE_AN_ISSUE", "GITHUB_CREATE_AN_ISSUE_COMMENT", "GITHUB_CREATE_A_COMMIT"], ) tools_4 = composio.tools.get(user_id="john", search="hackernews posts") ``` **TypeScript (current):** ```typescript import { Composio } from '@composio/core'; const userId = 'user@acme.org'; const composio = new Composio(); const tools_1 = await composio.tools.get(userId, { toolkits: ['GITHUB', 'LINEAR'], }); const tools_2 = await composio.tools.get(userId, { toolkits: ['GITHUB'], limit: 5, // Default limit=20 }); const tools_3 = await composio.tools.get(userId, { tools: ['GITHUB_CREATE_AN_ISSUE', 'GITHUB_CREATE_AN_ISSUE_COMMENT', 'GITHUB_CREATE_A_COMMIT'], }); const tools_4 = await composio.tools.get(userId, { search: 'hackernews posts', }); ``` ## Fetching raw tool data To examine the raw schema definition of a tool for understanding input/output parameters or building custom logic around tool definitions, use the following methods: **Python (current):** ```python from composio import Composio composio = Composio() tool = composio.tools.get_raw_composio_tool_by_slug("HACKERNEWS_GET_LATEST_POSTS") print(tool.model_dump_json()) ``` **TypeScript (current):** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const tool = await composio.tools.getRawComposioToolBySlug('GITHUB_GET_OCTOCAT'); console.log(JSON.stringify(tool, null, 2)); ``` ## Executing tools Tool execution remains largely unchanged, with `user_id` now explicitly required. For agentic frameworks, the tool object returned from `tools.get` is now the respective framework's native tool object. Tool call execution is handled by the agentic framework itself. > For non-agentic frameworks, Composio provides a helper function to execute tool calls. **Python v3:** ```python from composio import Composio from openai import OpenAI openai_client = OpenAI() composio = Composio() tools = composio.tools.get(user_id="user@acme.com", tools=["GITHUB_GET_THE_ZEN_OF_GITHUB"]) response = openai_client.chat.completions.create( model="gpt-4.1", messages=[{"role": "user", "content": "gimme some zen."}], tools=tools, ) result = composio.provider.handle_tool_calls(user_id="user@acme.com", response=response) print(result) ``` **TypeScript v3:** ```typescript import { Composio } from '@composio/core'; import { AnthropicProvider } from '@composio/anthropic'; import Anthropic from '@anthropic-ai/sdk'; const anthropic = new Anthropic(); const composio = new Composio({ provider: new AnthropicProvider(), }); const userId = 'user@example.com'; const tools = await composio.tools.get(userId, { toolkits: ['GMAIL'], }); const msg = await anthropic.messages.create({ model: 'claude-3-7-sonnet-latest', tools: tools, messages: [ { role: 'user', content: "Say hi to 'soham@composio.dev'", }, ], max_tokens: 1024, }); const result = await composio.provider.handleToolCalls(userId, msg); console.log('Tool results:', result); ``` For more information on executing tools for different frameworks, see [Replacing ToolSets with Providers](#replacing-toolsets-with-providers). ## Tool Modifiers (formerly Tool Processors) Tool processors have been renamed to *tool modifiers* and now provide an improved developer experience. The implementation is now available in TypeScript too! (previously Python-only). ```python title="Python (previous)" from composio_openai import ComposioToolSet, Action toolset = ComposioToolSet() def my_schema_processor(schema: dict) -> dict: ... def my_preprocessor(inputs: dict) -> dict: ... def my_postprocessor(result: dict) -> dict: ... # Get tools with the modified schema processed_tools = toolset.get_tools( actions=[Action.GMAIL_SEND_EMAIL], processors={ # Applied BEFORE the LLM sees the schema "schema": {Action.SOME_ACTION: my_schema_processor}, # Applied BEFORE the tool executes "pre": {Action.SOME_ACTION: my_preprocessor}, # Applied AFTER the tool executes, BEFORE the result is returned "post": {Action.SOME_ACTION: my_postprocessor}, }, ) ``` | Previous | Current | | ------------------ | ------------------------ | | `pre` processor | `beforeExecute` modifier | | `post` processor | `afterExecute` modifier | | `schema` processor | `schema` modifier | The modifiers now leverage language-specific features to provide a more natural developer experience. While tool processors could previously be applied during SDK initialization, tool fetching, and tool execution, we have restructured them as follows: * **Chat Completion providers**: Modifiers are specified and applied during tool execution * **Agentic frameworks**: Modifiers are specified and applied during tool fetching ### Schema Modifiers The following example demonstrates schema modifier usage, applicable across all providers: **Python (current):** ```python from composio import Composio, schema_modifier from composio.types import Tool user_id = "your@email.com" @schema_modifier(tools=["HACKERNEWS_GET_LATEST_POSTS"]) def modify_schema( tool: str, toolkit: str, schema: Tool, ) -> Tool: _ = schema.input_parameters["properties"].pop("page", None) schema.input_parameters["required"] = ["size"] return schema tools = composio.tools.get( user_id=user_id, tools=["HACKERNEWS_GET_LATEST_POSTS", "HACKERNEWS_GET_USER"], modifiers=[ modify_schema, ] ) ``` **TypeScript (current):** ```typescript // @noErrors import { Composio } from '@composio/core'; import { OpenAI } from 'openai'; const userId = 'your@email.com'; const composio = new Composio(); // Schema modifier to delete the `page` argument from the `HACKERNEWS_GET_LATEST_POSTS` tool const tools = await composio.tools.get( userId, { tools: ['HACKERNEWS_GET_LATEST_POSTS', 'HACKERNEWS_GET_USER'], }, { modifySchema: ({ toolSlug, toolkitSlug, schema }) => { if (toolSlug === 'HACKERNEWS_GET_LATEST_POSTS') { const { inputParameters } = schema; if (inputParameters?.properties) { delete inputParameters.properties['page']; } inputParameters.required = ['size']; } return schema; }, } ); console.log(JSON.stringify(tools, null, 2)); ``` ### Before Modifiers The following example shows creating and using a before modifier for a Chat Completion provider. For agentic frameworks, view the [complete before modifier documentation](/docs/tools-direct/modify-tool-behavior/before-execution-modifiers): **Python (current):** ```python @before_execute(tools=["HACKERNEWS_GET_LATEST_POSTS"]) def before_execute_modifier( tool: str, toolkit: str, params: ToolExecuteParams, ) -> ToolExecuteParams: params["arguments"]["size"] = 1 return params # Get tools tools = composio.tools.get(user_id=user_id, slug="HACKERNEWS_GET_LATEST_POSTS") ``` **TypeScript (current):** ```typescript // @noErrors const result_1 = await composio.tools.execute( 'HACKERNEWS_GET_LATEST_POSTS', { userId, arguments: JSON.parse(toolArgs), }, { beforeExecute: ({ toolSlug, toolkitSlug, params }) => { if (toolSlug === 'HACKERNEWS_GET_LATEST_POSTS') { params.arguments.size = 1; } console.log(params); return params; }, } ); ``` ### After Modifiers The following example shows creating and using an after modifier for a Chat Completion provider. For agentic frameworks, view the [complete after modifier documentation](/docs/tools-direct/modify-tool-behavior/after-execution-modifiers): **Python (current):** ```python @after_execute(tools=["HACKERNEWS_GET_USER"]) def after_execute_modifier( tool: str, toolkit: str, response: ToolExecutionResponse, ) -> ToolExecutionResponse: return { **response, "data": { "karma": response["data"]["karma"], }, } tools = composio.tools.get(user_id=user_id, slug="HACKERNEWS_GET_USER") ``` **TypeScript (current):** ```typescript // @noErrors const result_2 = await composio.tools.execute( 'HACKERNEWS_GET_USER', { userId, arguments: JSON.parse(toolArgs), }, { afterExecute: ({ toolSlug, toolkitSlug, result }) => { if (toolSlug === 'HACKERNEWS_GET_USER') { const { data } = result; const { karma } = data.response_data as { karma: number }; return { ...result, data: { karma }, }; } return result; }, } ); ``` ## Custom Tools Custom tools are now session-scoped. Define local tools with the experimental custom-tools API and attach them when creating or reusing a session. **Python:** ```python from composio import Composio from pydantic import BaseModel, Field composio = Composio() class GetIssueInfoInput(BaseModel): issue_number: int = Field(..., description="The issue number") @composio.experimental.tool(extends_toolkit="github") def get_issue_info(input: GetIssueInfoInput, ctx) -> dict: """Get information about a GitHub issue.""" result = ctx.proxy_execute( toolkit="github", endpoint=f"/repos/composiohq/composio/issues/{input.issue_number}", method="GET", ) return {"data": result.data} session = composio.create( user_id="default", experimental={"custom_tools": [get_issue_info]}, ) ``` **TypeScript:** ```typescript import { Composio, experimental_createTool } from "@composio/core"; import { z } from "zod/v3"; const composio = new Composio(); const getIssueInfo = experimental_createTool("GET_ISSUE_INFO", { name: "Get issue info", description: "Get information about a GitHub issue.", extendsToolkit: "github", inputParams: z.object({ issueNumber: z.number().describe("The issue number"), }), execute: async (input, ctx) => { const result = await ctx.proxyExecute({ toolkit: "github", endpoint: `/repos/composiohq/composio/issues/${input.issueNumber}`, method: "GET", }); return { data: result.data }; }, }); const session = await composio.create("default", { experimental: { customTools: [getIssueInfo] }, }); ``` For more information, see [Custom Tools and Toolkits](/docs/extending-sessions/custom-tools-and-toolkits). ## Auth configs (formerly integrations) Integrations are now called *auth configs*. While the terminology has changed, the underlying concept remains the same. Auth configs store the configuration required for authentication with a given toolkit, including OAuth developer credentials, configurable base URLs, and scopes. Auth configs now use nano IDs instead of UUIDs: | Previous (UUID) Example | Current (Nano ID) Example | | :------------------------------------- | :------------------------ | | `b7a9c1e2-3f4d-4a6b-8c2e-1d2f3a4b5c6d` | `ac_8x9w2l3k5m` | We recommend storing auth config nano IDs in your database for connecting users to the appropriate auth configuration. For most use cases, you will create auth configs through the dashboard, and this process remains unchanged. Read more about [creating auth configs](/docs/tools-direct/authenticating-tools#creating-an-auth-config) and [customizing auth configs](/docs/auth-configuration/custom-auth-configs). Creating auth configs programmatically in the previous SDK: **Python (previous):** ```python from composio_openai import App, ComposioToolSet toolset = ComposioToolSet() integration = toolset.create_integration( app=App.GITHUB, auth_mode="OAUTH2", use_composio_oauth_app=True, # For use_composio_oauth_app=False, you can provide your own OAuth app credentials here # auth_config={ # "client_id": "123456", # "client_secret": "123456" # } ) print(integration.id) ``` **TypeScript (previous):** ```typescript // @noErrors import { OpenAIToolSet } from "composio-core"; const composioToolset = new OpenAIToolSet(); const integration = await composioToolset.integrations.create({ name: "gmail_integration", appUniqueKey: "gmail", forceNewIntegration: true, useComposioAuth: false, // For useComposioAuth: false, you can provide your own OAuth app credentials here // authScheme: "OAUTH2", // authConfig: { // clientId: "123456", // clientSecret: "123456" // } }) console.log(integration.id) ``` Creating auth configs programmatically in the current SDK: **Python (current):** ```python from composio import Composio composio = Composio() # Use composio managed auth auth_config = composio.auth_configs.create( toolkit="notion", options={ "type": "use_composio_managed_auth", # "type": "use_custom_auth", # "auth_scheme": "OAUTH2", # "credentials": { # "client_id": "1234567890", # "client_secret": "1234567890", # "oauth_redirect_uri": "https://backend.composio.dev/api/v3/toolkits/auth/callback", # }, }, ) print(auth_config) ``` **TypeScript (current):** ```typescript // @noErrors import { Composio } from '@composio/core'; const composio = new Composio(); const authConfig = await composio.authConfigs.create('LINEAR', { name: 'Linear', type: 'use_composio_managed_auth', // type: "use_custom_auth", // credentials: { // client_id: "1234567890", // client_secret: "1234567890", // oauth_redirect_uri: "https://backend.composio.dev/api/v3/toolkits/auth/callback", // }, }); console.log(authConfig); ``` For using custom authentication credentials, refer to the [Programmatic Auth Configs](/docs/programmatic-auth-configs) documentation. > The callback URL for creating custom OAuth configs is now `https://backend.composio.dev/api/v3/toolkits/auth/callback`. The previous URL was `https://backend.composio.dev/api/v1/auth-apps/add`. ## Connected accounts / User IDs The primary change in connected accounts and user IDs is that user IDs are now a more prominent concept compared to entities in previous versions. We have simplified the process of connecting a user to a toolkit. Instead of multiple methods and parameters for initiating a connection, both the SDK and API now require only a `user_id` and `auth_config_id` to initiate a connection. This approach is more explicit and works well with the ability for developers to have multiple auth configs for a given toolkit. Connected accounts now use nano IDs instead of UUIDs: | Previous (UUID) Example | Current (Nano ID) Example | | :------------------------------------- | :------------------------ | | `b7a9c1e2-3f4d-4a6b-8c2e-1d2f3a4b5c6d` | `ca_8x9w2l3k5m` | Previously, you might have initiated a connection like this: **Python (previous):** ```python from composio_openai import ComposioToolSet toolset = ComposioToolSet() user_id = "your_user_unique_id" google_integration_id = "0000-0000" entity = toolset.get_entity(id=user_id) try: print(f"Initiating OAuth connection for entity {entity.id}...") connection_request = toolset.initiate_connection( integration_id=google_integration_id, entity_id=user_id, # Optionally add: redirect_url="https://yourapp.com/final-destination" # if you want user sent somewhere specific *after* Composio finishes. ) # Check if a redirect URL was provided (expected for OAuth) if connection_request.redirectUrl: print(f"Received redirect URL: {connection_request.redirectUrl}") else: print("Error: Expected a redirectUrl for OAuth flow but didn't receive one.") except Exception as e: print(f"Error initiating connection: {e}") ``` **TypeScript (previous):** ```typescript // @noErrors import { OpenAIToolSet } from "composio-core"; const toolset = new OpenAIToolSet(); const userId = "your_user_unique_id"; const googleIntegrationId = "0000-0000"; console.log(`Initiating OAuth connection for entity ${userId}...`); const connectionRequest = await toolset.connectedAccounts.initiate({ integrationId: googleIntegrationId, entityId: userId, // Optionally add: redirectUri: "https://yourapp.com/final-destination" // if you want user sent somewhere specific *after* Composio finishes. }); // Check if a redirect URL was provided (expected for OAuth) if (connectionRequest?.redirectUrl) { console.log(`Received redirect URL: ${connectionRequest.redirectUrl}`); // Proceed to Step 2: Redirect the user // Return or pass connectionRequest to the next stage } else { console.error("Error: Expected a redirectUrl for OAuth flow but didn't receive one."); } ``` The current process for initiating a connection is as follows: **Python (current):** ```python from composio import Composio linear_auth_config_id = "ac_1234" user_id = "user@email.com" composio = Composio() # Create a new connected account connection_request = composio.connected_accounts.initiate( user_id=user_id, auth_config_id=linear_auth_config_id, ) print(connection_request.redirect_url) # Wait for the connection to be established connected_account = connection_request.wait_for_connection() print(connected_account) ``` **TypeScript (current):** ```typescript // @noErrors import { Composio } from "@composio/core"; const composio = new Composio(); const linearAuthConfigId = "ac_1234"; const userId = "user@email.com"; // Initiate the OAuth connection request const connRequest = await composio.connectedAccounts.initiate(userId, linearAuthConfigId); const { redirectUrl, id } = connRequest; console.log(redirectUrl); // Wait for the connection to be established await connRequest.waitForConnection(); // If you only have the connection request ID, you can also wait using: await composio.connectedAccounts.waitForConnection(id); ``` ## Triggers Composio continues to support listening to application events using triggers through WebSockets and webhooks. ### Creating triggers The process for creating triggers and specifying their configuration has been redesigned for improved clarity and intuitiveness. Some triggers require configuration, such as repository names for GitHub triggers or channel names for Slack triggers. The process usually follows the pattern of fetching the trigger type and then creating the trigger with the appropriate configuration. **Python (current):** ```python from composio import Composio composio = Composio() user_id = "user@example.com" trigger_config = composio.triggers.get_type("GITHUB_COMMIT_EVENT") print(trigger_config.config) ## Trigger Config # { # "properties": { # "owner": { # "description": "Owner of the repository", # "title": "Owner", # "type": "string" # }, # "repo": { # "description": "Repository name", # "title": "Repo", # "type": "string" # } # }, # "required": ["owner", "repo"], # "title": "WebhookConfigSchema", # "type": "object" trigger = composio.triggers.create( slug="GITHUB_COMMIT_EVENT", user_id=user_id, trigger_config={"repo": "composiohq", "owner": "composio"}, ) print(trigger) # Managing triggers composio.triggers.enable(id="ti_abcd123") ``` **TypeScript (current):** ```typescript // @noErrors import { Composio } from '@composio/core'; const composio = new Composio(); const userId = 'user@acme.com'; // Fetch the trigger details const triggerType = await composio.triggers.getType('GITHUB_COMMIT_EVENT'); console.log(JSON.stringify(triggerType.config, null, 2)); /*--- Trigger config --- { "properties": { "owner": { "description": "Owner of the repository", "title": "Owner", "type": "string" }, "repo": { "description": "Repository name", "title": "Repo", "type": "string" } }, "required": ["owner", "repo"], "title": "WebhookConfigSchema", "type": "object" } */ const createResponse = await composio.triggers.create(userId, 'GITHUB_COMMIT_EVENT', { triggerConfig: { owner: 'composiohq', repo: 'composio', }, }); console.log(createResponse); ``` ### Enabling/Disabling triggers You can enable or disable triggers through either the SDK or the dashboard. The dashboard process remains unchanged. Managing triggers with the SDK: **Python:** ```python # Disable a trigger instance disabled_instance = composio.triggers.disable(trigger_id="ti_abcd123") print(disabled_instance) ``` **TypeScript:** ```typescript // @noErrors await composio.triggers.disable("ti_abcd123"); ``` If needed, the trigger can be enabled again. **Python:** ```python # Enable a trigger instance enabled_instance = composio.triggers.enable(trigger_id="ti_abcd123") print(enabled_instance) ``` **TypeScript:** ```typescript // @noErrors await composio.triggers.enable("ti_abcd123"); ``` ### Listening to triggers We recommend listening to triggers through webhooks. The following are example routes for Next.js and FastAPI. For development, you can also [listen to triggers through the SDK](/docs/setting-up-triggers/subscribing-to-events#receive-events-locally). **FastAPI:** ```python title="app/route.py" from fastapi import FastAPI, Request, HTTPException from typing import Dict, Any import uvicorn import json import hmac import hashlib import base64 import os def verify_webhook_signature(request: Request, body: bytes) -> bool: """Verify Composio webhook signature""" webhook_signature = request.headers.get("webhook-signature") webhook_id = request.headers.get("webhook-id") webhook_timestamp = request.headers.get("webhook-timestamp") webhook_secret = os.getenv("COMPOSIO_WEBHOOK_SECRET") if not all([webhook_signature, webhook_id, webhook_timestamp, webhook_secret]): raise HTTPException(status_code=400, detail="Missing required webhook headers or secret") if not webhook_signature.startswith("v1,"): raise HTTPException(status_code=401, detail="Invalid signature format") received = webhook_signature[3:] signing_string = f"{webhook_id}.{webhook_timestamp}.{body.decode()}" expected = base64.b64encode( hmac.new(webhook_secret.encode(), signing_string.encode(), hashlib.sha256).digest() ).decode() if not hmac.compare_digest(received, expected): raise HTTPException(status_code=401, detail="Invalid webhook signature") return True @app.post("/webhook") async def webhook_handler(request: Request): payload = await request.json() trigger_type = payload.get("type") event_data = payload.get("data", {}) if trigger_type == "github_star_added_event": repo_name = event_data.get("repository_name") starred_by = event_data.get("starred_by") print(f"Repository {repo_name} starred by {starred_by}") # Add your business logic here return {"status": "success", "message": "Webhook processed"} ``` **Next.js:** ```typescript title="app/api/webhook/route.ts" // @noErrors import type { NextApiRequest, NextApiResponse } from 'next'; import { TriggerEvent } from '@composio/core'; import crypto from 'crypto'; type GitHubStarEventData = { repository_name: string; repository_url: string; starred_by: string; starred_at: string; }; function verifyWebhookSignature( req: NextApiRequest, body: string ): boolean { const signature = req.headers['webhook-signature'] as string | undefined; const msgId = req.headers['webhook-id'] as string | undefined; const timestamp = req.headers['webhook-timestamp'] as string | undefined; const secret = process.env.COMPOSIO_WEBHOOK_SECRET; if (!signature || !msgId || !timestamp || !secret) { throw new Error('Missing required webhook headers or secret'); } if (!signature.startsWith('v1,')) { throw new Error('Invalid signature format'); } const received = signature.slice(3); const signingString = `${msgId}.${timestamp}.${body}`; const expected = crypto .createHmac('sha256', secret) .update(signingString) .digest('base64'); return crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected)); } export default async function webhookHandler(req: NextApiRequest, res: NextApiResponse) { const payload = req.body; if (payload.type === 'github_star_added_event') { const event: TriggerEvent = { type: payload.type, timestamp: payload.timestamp, data: payload.data }; console.log(`Repository ${event.data.repository_name} starred by ${event.data.starred_by}`); // Add your business logic here } res.status(200).json({ status: 'success', message: 'Webhook processed' }); } ``` # Coming Soon ## Local tools Previously, the Python SDK included *[local tools](https://github.com/ComposioHQ/composio/tree/master/python/composio/tools/local)*. These were tools defined within the SDK and consisted of local shell and code-related tools such as "clipboard", "sqltool", and "shelltool". This feature is currently in development for both Python and TypeScript SDKs, with newly created tools built for improved agent accuracy. This feature is currently in development for both Python and TypeScript SDKs. # API Endpoints The following table lists important API endpoints that have changed. You can use this reference to quickly find the new v3 API endpoint for migration: > This list is not exhaustive. Please refer to the [API Reference](/reference) for the complete list of endpoints. ## Toolkits (formerly Apps) | Previous Endpoint | Current Endpoint | | :--------------------------------- | :-------------------------------- | | `GET /api/v1/apps` | `GET /api/v3/toolkits` | | `GET /api/v1/apps/list/categories` | `GET /api/v3/toolkits/categories` | | `GET /api/v1/apps/{appName}` | `GET /api/v3/toolkits/{slug}` | ## Tools (formerly Actions) | Previous Endpoint | Current Endpoint | | :--------------------------------------------------- | :--------------------------------------------- | | `GET /api/v2/actions` | `GET /api/v3/tools` | | `GET /api/v2/actions/list/enums` | `GET /api/v3/tools/enum` | | `GET /api/v2/actions/{actionId}` | `GET /api/v3/tools/{tool_slug}` | | `POST /api/v2/actions/{actionId}/execute` | `POST /api/v3/tools/execute/{tool_slug}` | | `POST /api/v2/actions/{actionId}/execute/get.inputs` | `POST /api/v3/tools/execute/{tool_slug}/input` | | `POST /api/v2/actions/proxy` | `POST /api/v3/tools/execute/proxy` | ## Auth Configs (formerly Integrations/Connectors) | Previous Endpoint | Current Endpoint | | :-------------------------------------------- | :------------------------------------- | | `GET /api/v1/integrations` | `GET /api/v3/auth_configs` | | `POST /api/v1/integrations` | `POST /api/v3/auth_configs` | | `GET /api/v1/integrations/{integrationId}` | `GET /api/v3/auth_configs/{nanoid}` | | `PATCH /api/v1/integrations/{integrationId}` | `PATCH /api/v3/auth_configs/{nanoid}` | | `DELETE /api/v1/integrations/{integrationId}` | `DELETE /api/v3/auth_configs/{nanoid}` | | `POST /api/v2/integrations/create` | `POST /api/v3/auth_configs` | ## Connected Accounts (formerly Connections) | Previous Endpoint | Current Endpoint | | :--------------------------------------------------------------- | :------------------------------------------------- | | `GET /api/v1/connectedAccounts` | `GET /api/v3/connected_accounts` | | `POST /api/v1/connectedAccounts` | `POST /api/v3/connected_accounts` | | `POST /api/v2/connectedAccounts/initiateConnection` | `POST /api/v3/connected_accounts` | | `GET /api/v1/connectedAccounts/{connectedAccountId}` | `GET /api/v3/connected_accounts/{nanoid}` | | `DELETE /api/v1/connectedAccounts/{connectedAccountId}` | `DELETE /api/v3/connected_accounts/{nanoid}` | | `POST /api/v1/connectedAccounts/{connectedAccountId}/disable` | `PATCH /api/v3/connected_accounts/{nanoId}/status` | | `POST /api/v1/connectedAccounts/{connectedAccountId}/enable` | `PATCH /api/v3/connected_accounts/{nanoId}/status` | | `POST /api/v1/connectedAccounts/{connectedAccountId}/reinitiate` | `POST /api/v3/connected_accounts/{nanoid}/refresh` | ## Triggers | Previous Endpoint | Current Endpoint | | :---------------------------------------------------------------- | :---------------------------------------------------- | | `GET /api/v1/triggers` | `GET /api/v3/triggers_types` | | `GET /api/v1/triggers/list/enums` | `GET /api/v3/triggers_types/list/enum` | | `GET /api/v2/triggers/{triggerName}` | `GET /api/v3/triggers_types/{slug}` | | `GET /api/v1/triggers/active_triggers` | `GET /api/v3/trigger_instances/active` | | `POST /api/v1/triggers/enable/{connectedAccountId}/{triggerName}` | `POST /api/v3/trigger_instances/{slug}/upsert` | | `DELETE /api/v1/triggers/instance/{triggerInstanceId}` | `DELETE /api/v3/trigger_instances/manage/{triggerId}` | | `PATCH /api/v1/triggers/instance/{triggerId}/status` | `PATCH /api/v3/trigger_instances/manage/{triggerId}` | --- # Claude Code Plugin (/docs/claude-code-plugin) The **Composio plugin for Claude Code** lets Claude act on 1,000+ apps — send the Slack message, open the Linear issue, check your calendar, draft the email. Your agent decides what to do; Composio handles the rest: OAuth, permissions, and finding the right tool for each task. No API keys, no config files. > Using Cursor, Codex, Claude Desktop, or another MCP client? See [Composio Connect](/docs/composio-connect). # Install #### Add the Composio marketplace In Claude Code, run: ```bash /plugin marketplace add ComposioHQ/composio-plugin-cc ``` #### Install the plugin ```bash /plugin install composio@composio ``` Restart Claude Code (or run `/reload-plugins`) when prompted. #### Ask Claude to do something Try: *"Star `composiohq/composio` on GitHub."* The first time, Claude installs the Composio CLI if it's missing, signs you in with `composio login`, and gives you an OAuth link for GitHub. Approve it in your browser and Claude runs the action. > Prefer to set things up ahead of time? Run `curl -fsSL https://composio.dev/install | bash`, then `composio login`. # What you can do Naming the app in your prompt keeps tool search scoped and the run reliable. **Act on any connected app:** ```text What's on my Google Calendar for tomorrow? Add an event for lunch at 12PM. ``` **Run cross-app workflows** — reads feed the write: ```text Take the latest merged PR in acme/app, open a Linear issue summarizing it, and post the issue link to #eng in Slack. ``` **Fan out reads, then summarize:** ```text In parallel, fetch my last 10 Gmail emails, my open Linear issues, and today's Google Calendar events. Redact personal info, then give me a concise summary. ``` # Connecting apps Apps connect on demand — the first task that needs one hands you an OAuth link. To connect an app ahead of time: ```bash /composio-connect linear ``` Works for any of the 1,000+ supported apps — `slack`, `github`, `gmail`, `notion`, `linear`, `hubspot`, and more. # Team setup To pre-install the plugin for everyone on your team, add this to your project's `.claude/settings.json`: ```json title=".claude/settings.json" { "extraKnownMarketplaces": { "composio": { "source": { "source": "github", "repo": "ComposioHQ/composio-plugin-cc" } } }, "enabledPlugins": { "composio@composio": true } } ``` Anyone who clones the repo and opens it in Claude Code will be prompted to enable the plugin. See the Claude Code [plugin scopes](https://docs.claude.com/en/docs/claude-code/plugins-reference#plugin-installation-scopes) reference for `user` vs `project` vs `local` scope behavior. # Updating To pull the latest plugin release: ```bash /plugin marketplace update composio /reload-plugins ``` New capabilities usually ship in the Composio CLI itself — `composio upgrade` picks them up without touching the plugin. # Source code The plugin is open source: [ComposioHQ/composio-plugin-cc](https://github.com/ComposioHQ/composio-plugin-cc). Issues and PRs welcome. # Troubleshooting For plugin issues — install errors, marketplace not updating — see the [Claude Code plugin troubleshooting docs](https://docs.claude.com/en/docs/claude-code/plugins-reference#common-issues). For auth or connection issues, see the [CLI reference](/docs/cli) or run `composio --help`. --- # Composio Connect (/docs/composio-connect) Give any AI agent the ability to act across 1000+ apps. Bring your agent to where you work. # How Composio Connect works Composio Connect is an MCP server at `https://connect.composio.dev/mcp` that gives your AI agent access to 1000+ apps — Gmail, Notion, Slack, GitHub, Linear, HubSpot, Strava, and more — through a single connection. Rather than exposing every app tool directly, Composio exposes **7 meta-tools** that let the agent discover what's available, authorize apps on demand, and execute tools across apps in parallel. The first time your agent needs an app, Composio generates an OAuth link you approve in your browser; after that the connection persists across sessions. See [Available MCP tools](#available-mcp-tools) for the full list. To get started, pick your client below. ## Claude Code #### Get your API key Open the [Composio dashboard](https://dashboard.composio.dev) and click **AI Clients** in the sidebar. Select your client and copy your API key. #### Run the setup command Run this in your terminal to add the Composio MCP server to Claude Code. ```bash claude mcp add --scope user --transport http composio https://connect.composio.dev/mcp --header "x-consumer-api-key: YOUR_API_KEY" ``` #### Verify the connection Type `/mcp` in Claude Code to open the MCP server manager and confirm Composio appears and is enabled. ## Codex #### Get your API key Open the [Composio dashboard](https://dashboard.composio.dev) and click **AI Clients** in the sidebar. Select your client and copy your API key. #### Open your Codex config Open `~/.codex/config.toml` in your editor. Create the file if it doesn't exist. #### Add the Composio server ```toml title="~/.codex/config.toml" [mcp_servers.composio] url = "https://connect.composio.dev/mcp" http_headers = { "x-consumer-api-key" = "YOUR_API_KEY" } ``` #### Verify the connection Run `codex mcp list` to confirm Composio appears as a registered MCP server. ## OpenClaw #### Get your API key Open the [Composio dashboard](https://dashboard.composio.dev) and click **AI Clients** in the sidebar. Select your client and copy your API key. #### Add the MCP server via prompt Copy this prompt and paste it into OpenClaw: ``` Add a new MCP server called "composio" with transport type HTTP. Use the URL https://connect.composio.dev/mcp and add the header "x-consumer-api-key: YOUR_API_KEY". ``` ## Claude Desktop #### Open Connectors settings In Claude Desktop, go to **Settings**, then click **Connectors**. #### Add the Composio MCP server Click **Add custom connector** and paste the Composio MCP server URL. ``` https://connect.composio.dev/mcp ``` #### Authorize in your browser A browser window will open automatically. Sign in to authorize Claude Desktop to access your Composio account. ## ChatGPT #### Enable Developer Mode In ChatGPT, go to **Settings > Apps > Advanced settings** and toggle on **Developer Mode**. Requires ChatGPT Plus, Pro, Business, Enterprise, or Edu. #### Add the MCP server Click **Create app** and paste the Composio MCP server URL. ``` https://connect.composio.dev/mcp ``` #### Authorize in your browser A browser window will open automatically. Sign in to authorize ChatGPT to access your Composio account. ## Cursor #### Get your API key Open the [Composio dashboard](https://dashboard.composio.dev) and click **AI Clients** in the sidebar. Select your client and copy your API key. #### Add the Composio server Open `.cursor/mcp.json` in your project root (or `~/.cursor/mcp.json` for global config) and add: ```json title=".cursor/mcp.json" { "mcpServers": { "composio": { "url": "https://connect.composio.dev/mcp", "headers": { "x-consumer-api-key": "YOUR_API_KEY" } } } } ``` #### Restart Cursor Restart Cursor. Composio tools are now available in Composer and Agent mode. ## Notion #### Get your API key Open the [Composio dashboard](https://dashboard.composio.dev) and click **AI Clients** in the sidebar. Select your client and copy your API key. #### Create a custom agent In Notion, open the AI agent builder and click **Create Blank** to start a new agent. #### Add a connection Click **Add Connection**, then select **Custom MCP**. #### Configure the server Enter the MCP server URL: ``` https://connect.composio.dev/mcp ``` Select **API Key** as the authentication type. Set the key to `x-consumer-api-key` and paste your API key as the value. ## VS Code #### Get your API key Open the [Composio dashboard](https://dashboard.composio.dev) and click **AI Clients** in the sidebar. Select your client and copy your API key. #### Add the Composio server Open or create `.vscode/mcp.json` in your project root and add the following. Requires VS Code 1.99+ with GitHub Copilot. ```json title=".vscode/mcp.json" { "servers": { "composio": { "type": "http", "url": "https://connect.composio.dev/mcp", "headers": { "x-consumer-api-key": "YOUR_API_KEY" } } } } ``` Note: VS Code uses `"servers"` instead of `"mcpServers"`. #### Open a Copilot chat Open a GitHub Copilot chat. Composio tools are now available. ## Windsurf #### Get your API key Open the [Composio dashboard](https://dashboard.composio.dev) and click **AI Clients** in the sidebar. Select your client and copy your API key. #### Open your MCP config Open `~/.codeium/windsurf/mcp_config.json`, or go to **Windsurf > Settings > MCP Configuration**. #### Add the Composio server ```json title="mcp_config.json" { "mcpServers": { "composio": { "serverUrl": "https://connect.composio.dev/mcp", "headers": { "x-consumer-api-key": "YOUR_API_KEY" } } } } ``` Note: Windsurf uses `"serverUrl"` instead of `"url"`. #### Restart Windsurf Restart Windsurf. Composio tools are now available in Cascade. ## Cline #### Get your API key Open the [Composio dashboard](https://dashboard.composio.dev) and click **AI Clients** in the sidebar. Select your client and copy your API key. #### Open MCP settings In VS Code with Cline installed, click the **MCP Servers** icon in the Cline navigation bar. Click the **Configure** tab, then click **Configure MCP Servers** to open the JSON config file. #### Add the Composio server ```json title="cline_mcp_settings.json" { "mcpServers": { "composio": { "url": "https://connect.composio.dev/mcp", "headers": { "x-consumer-api-key": "YOUR_API_KEY" }, "disabled": false } } } ``` #### Save Save the configuration. Composio tools are now available in Cline. ## Agent Builder #### Get your API key Open the [Composio dashboard](https://dashboard.composio.dev) and click **AI Clients** in the sidebar. Select your client and copy your API key. #### Create a new agent Open the [OpenAI Agent Builder](https://platform.openai.com/agent-builder) and create a new agent. #### Add MCP Server Click **MCP** in the sidebar, then click **+ Server**. Paste the URL: ``` https://connect.composio.dev/mcp ``` Select **Custom headers**. Add a header with name `x-consumer-api-key` and paste your API key as the value. ## n8n #### Get your API key Open the [Composio dashboard](https://dashboard.composio.dev) and click **AI Clients** in the sidebar. Select your client and copy your API key. #### Add an MCP Client node In your n8n workflow, add an **MCP Client** node or MCP Client Tool sub-node for AI agent chains. #### Configure the connection Set the connection type to **HTTP Streamable** and paste the server URL: ``` https://connect.composio.dev/mcp ``` Select **Header Auth** and create a new credential. Set the header name to `x-consumer-api-key` and paste your API key as the value. ## MCP URL #### Get your API key Open the [Composio dashboard](https://dashboard.composio.dev) and click **AI Clients** in the sidebar. Select your client and copy your API key. #### Add to your client config Use this config with any MCP-compatible client: ```json { "mcpServers": { "composio": { "url": "https://connect.composio.dev/mcp", "headers": { "x-consumer-api-key": "YOUR_API_KEY" } } } } ``` # Connect your apps Your agent will prompt you to connect apps when needed. If you want to connect apps ahead of time, open the [Composio dashboard](https://dashboard.composio.dev) and click **Connect Apps** in the sidebar. # Available MCP tools Composio Connect exposes 7 meta-tools that orchestrate access to all supported apps. Your agent uses these to discover, connect, and execute upstream tools — you don't need to call them directly. * **`COMPOSIO_SEARCH_TOOLS`** — Search the Composio catalog and return relevant tools for a user request, along with a suggested execution plan. * **`COMPOSIO_GET_TOOL_SCHEMAS`** — Fetch full input schemas for tool slugs returned by search. * **`COMPOSIO_MULTI_EXECUTE_TOOL`** — Execute one or more discovered tools in parallel across connected apps (up to 50 per call). * **`COMPOSIO_MANAGE_CONNECTIONS`** — Create, list, rename, or remove OAuth connections to upstream apps. * **`COMPOSIO_WAIT_FOR_CONNECTIONS`** — Wait for a user to complete an OAuth flow before the agent continues. * **`COMPOSIO_REMOTE_WORKBENCH`** — Run Python in a remote sandbox for bulk operations or processing large tool responses. * **`COMPOSIO_REMOTE_BASH_TOOL`** — Run bash in a remote sandbox for file processing and large data handling. # Troubleshooting ## Tools aren't appearing in my agent 1. Confirm the MCP server is connected. In Claude Desktop, go to **Settings > Connectors** and check that Composio shows a `CUSTOM` badge. In Claude Code, run `/mcp` and confirm Composio is enabled. 2. Clear the connector cache. In Claude Desktop: click the **⋮** next to Composio and select **Clear cache**. 3. If the issue persists, disconnect and re-add the connector: * **Claude Desktop** — click **⋮ > Disconnect**, then **Remove**. Re-add via **Add custom connector**. * **Claude Code** — re-run the setup command from the Claude Code section above. ## The OAuth link expired or didn't open OAuth links are short-lived. If the browser window doesn't open or the link has expired, ask your agent to retry the action — Composio will generate a fresh link. You can also open the [Composio dashboard](https://dashboard.composio.dev), click **Connect Apps**, and start the flow manually. ## An app action is failing with an auth error 1. Open the [Composio dashboard](https://dashboard.composio.dev) and click **Connect Apps**. 2. Find the app in question. If the connection is unhealthy, click **Disconnect** and reconnect. 3. Retry the action in your agent. ## I want to remove or reconnect an app Open [dashboard.composio.dev](https://dashboard.composio.dev) and click **Connect Apps**. From there you can disconnect, delete, or re-authorize any app. ## I still need help Reach out at [tech@composio.dev](mailto:tech@composio.dev) or join the [Composio Discord](https://discord.com/invite/cNruWaAhQk). --- # Single Toolkit MCP (/docs/single-toolkit-mcp) > For most use cases, use a regular [session](/docs/configuring-sessions) instead. Sessions provide dynamic tool access and a much better MCP experience with context management handled by us. # Install the SDK **Python:** **TypeScript:** # Create an MCP server ### Initialize Composio **Python:** ```python from composio import Composio composio = Composio(api_key="YOUR_API_KEY") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); ``` ### Create server configuration > **Before you begin:** [Create an auth configuration](/docs/auth-configuration/custom-auth-configs) for your toolkit. **Python:** ```python server = composio.mcp.create( name="my-gmail-server", toolkits=[{ "toolkit": "gmail", "auth_config": "ac_xyz123" }], allowed_tools=["GMAIL_FETCH_EMAILS", "GMAIL_SEND_EMAIL"] ) print(f"Server created: {server.id}") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); const server = await composio.mcp.create("my-gmail-server", { toolkits: [ { authConfigId: "ac_xyz123", toolkit: "gmail" } ], allowedTools: ["GMAIL_FETCH_EMAILS", "GMAIL_SEND_EMAIL"] }); console.log(`Server created: ${server.id}`); ``` > You can also create and manage MCP configs from the [Composio dashboard](https://dashboard.composio.dev/~/org/connect/clients). ### Generate user URLs > Users must authenticate with the toolkits configured in your MCP server first. See [authentication](/docs/authentication) for details. **Python:** ```python instance = composio.mcp.generate(user_id="user-123", mcp_config_id=server.id) print(f"MCP Server URL: {instance['url']}") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); const server = { id: 'my-gmail-server' }; const instance = await composio.mcp.generate("user-123", server.id); console.log("MCP Server URL:", instance.url); ``` ### Use with AI providers > Pass an `x-api-key` header when connecting to Composio MCP. This is required when `require_mcp_api_key` is enabled (default for newly created organizations). **OpenAI (Python):** ```python from openai import OpenAI client = OpenAI(api_key="your-openai-api-key") mcp_server_url = "https://backend.composio.dev/v3/mcp/YOUR_SERVER_ID?user_id=YOUR_USER_ID" mcp_headers = {"x-api-key": "YOUR_COMPOSIO_API_KEY"} response = client.responses.create( model="gpt-5", tools=[{ "type": "mcp", "server_label": "composio-server", "server_url": mcp_server_url, "headers": mcp_headers, "require_approval": "never", }], input="What are my latest emails?", ) print(response.output_text) ``` **Anthropic (Python):** ```python from anthropic import Anthropic client = Anthropic(api_key="your-anthropic-api-key") mcp_server_url = "https://backend.composio.dev/v3/mcp/YOUR_SERVER_ID?user_id=YOUR_USER_ID" mcp_headers = {"x-api-key": "YOUR_COMPOSIO_API_KEY"} response = client.beta.messages.create( model="claude-sonnet-4-6", max_tokens=1000, messages=[{"role": "user", "content": "What are my latest emails?"}], mcp_servers=[{ "type": "url", "url": mcp_server_url, "name": "composio-mcp-server", "headers": mcp_headers, }], betas=["mcp-client-2025-04-04"] ) print(response.content) ``` **Mastra (TypeScript):** ```typescript import { MCPClient } from "@mastra/mcp"; import { openai } from "@ai-sdk/openai"; import { Agent } from "@mastra/core/agent"; const MCP_URL = "https://backend.composio.dev/v3/mcp/YOUR_SERVER_ID?user_id=YOUR_USER_ID"; const MCP_HEADERS = { "x-api-key": "YOUR_COMPOSIO_API_KEY" }; const client = new MCPClient({ id: "mcp-client", servers: { composio: { url: new URL(MCP_URL), headers: MCP_HEADERS }, } }); const agent = new Agent({ id: "assistant", name: "Assistant", instructions: "You are a helpful assistant that can read and manage emails.", model: openai("gpt-5.4"), tools: await client.getTools() }); const res = await agent.generate("What are my latest emails?"); console.log(res.text); ``` # Server management ## List servers **Python:** ```python servers = composio.mcp.list() print(f"Found {len(servers['items'])} servers") # Filter by toolkit gmail_servers = composio.mcp.list(toolkits="gmail", limit=20) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const servers = await composio.mcp.list({ toolkits: [], authConfigs: [], limit: 10, page: 1 }); console.log(`Found ${servers.items.length} servers`); // Filter by toolkit const gmailServers = await composio.mcp.list({ toolkits: ["gmail"], authConfigs: [], limit: 20, page: 1 }); ``` ## Get server details **Python:** ```python server = composio.mcp.get("mcp_server_id") print(f"Server: {server.name}") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const server = await composio.mcp.get("mcp_server_id"); console.log(`Server: ${server.name}`); ``` ## Update a server **Python:** ```python updated = composio.mcp.update( server_id="mcp_server_id", name="updated-name", allowed_tools=["GMAIL_FETCH_EMAILS", "GMAIL_SEARCH_EMAILS"] ) ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const updated = await composio.mcp.update("mcp_server_id", { name: "updated-name", allowedTools: ["GMAIL_FETCH_EMAILS", "GMAIL_SEARCH_EMAILS"] }); ``` ## Delete a server **Python:** ```python result = composio.mcp.delete("mcp_server_id") if result['deleted']: print("Server deleted") ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const result = await composio.mcp.delete("mcp_server_id"); if (result.deleted) { console.log("Server deleted"); } ``` # Next - [Providers](/docs/providers): Use with Anthropic, OpenAI, and other frameworks --- # Examples --- # Build a Slack bot that can do work with you and your team (/examples/general-agent-with-pi) The agent is the easy part. [Pi](https://github.com/earendil-works/pi/tree/main/packages/coding-agent) does the reasoning; Composio gives it 1000+ apps to act on. In three lines you have an agent that can open a PR, check a calendar, or search a Notion workspace for one user. The work is everything around it: putting that agent in Slack, where a whole team talks to it, and making it act as *each* person while posting as one bot. That's a handful of Composio pieces: 1. **Triggers** deliver every Slack message to your server as a webhook. 2. **Sessions** give each user their own scoped toolset, so the agent acts as *them*. 3. **A shared connection** lets the bot speak as the workspace bot, with one install for everyone. 4. **Redirected auth links** keep OAuth out of the channel: when an app isn't connected, the bot DMs the user a link and resumes on approval. 5. **The proxy** reaches the Slack Web API endpoints the toolkit doesn't wrap as tools. Below you build the whole thing from scratch: a basic agent first, then a piece at a time up to the full server, then a browse of the real source. You bring a Composio API key and an agent runtime. Composio brings the workspace. # Setup You need a [Composio API key](https://dashboard.composio.dev), a publicly reachable URL for your server, and [Bun](https://bun.sh). **No public URL? Use a Cloudflare tunnel** Composio posts webhooks to your server, so it needs a public URL. In local development, run a [Cloudflare tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) to expose your local port: ```bash cloudflared tunnel --url http://localhost:3000 ``` Use the `https://…trycloudflare.com` URL it prints as your `APP_URL`. ```bash bun add @composio/core @composio/experimental @earendil-works/pi-coding-agent ``` # Install the bot A Slack bot needs a Slack app to authenticate as and a stream of events. Composio gives you both, so you never register a webhook with Slack or hold a bot token. The `slackbot` toolkit ships with Composio-managed OAuth, and you install it as one **[shared connection](/docs/shared-connections)** for the whole workspace. This is `install.ts`, run once, built up three steps at a time: **`install.ts` — complete file** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); // The scopes the bot needs. The slackbot toolkit ships Composio-managed OAuth, // so you never register your own Slack app. const authConfig = await composio.authConfigs.create('slackbot', { type: 'use_composio_managed_auth', name: 'workspace-bot', credentials: { scopes: ['app_mentions:read', 'channels:history', 'chat:write', 'reactions:write', 'users:read'], user_scopes: ['search:read'], }, }); // One connection for the whole workspace: authorize it as SHARED. const setup = await composio.create('setup:workspace-bot', { toolkits: ['slackbot'], authConfigs: { slackbot: authConfig.id }, manageConnections: true, }); const request = await setup.authorize('slackbot', { callbackUrl: `${process.env.APP_URL}/setup/callback`, experimental: { accountType: 'SHARED' }, }); console.log('Approve the install:', request.redirectUrl); // On the OAuth callback: open the ACL, subscribe your webhook, create triggers. // Persist connectedAccountId as SLACK_CONNECTION_ID for the bot server. export async function onSetupCallback(connectedAccountId: string) { await composio.connectedAccounts.updateAcl(connectedAccountId, { allowAllUsers: true }); await composio.triggers.setWebhookSubscription({ webhookUrl: `${process.env.APP_URL}/webhooks/composio` }); await composio.triggers.create('setup:workspace-bot', 'SLACKBOT_CHANNEL_MESSAGE_RECEIVED', { triggerConfig: { is_bot_message: false } }); await composio.triggers.create('setup:workspace-bot', 'SLACKBOT_DIRECT_MESSAGE_RECEIVED', { triggerConfig: {} }); } ``` A webhook subscription is the *pipe*; each trigger is a *tap*. Together they stream channel messages and DMs to your server. The connected account id that comes back from the OAuth callback is the `SLACK_CONNECTION_ID` the server pins into every session. # Build the bot `bot.ts` starts as a bare three-line agent and grows into the server, one Composio concept at a time. Each diff below is exactly what that concept adds. ## Start with a basic agent The whole idea, before any Slack: create a session for a user, hand the Pi provider the session so it can search and execute, and run a prompt. This already acts across every app that user has connected. **`bot.ts` — step 1: A basic agent** ```typescript import { Composio } from '@composio/core'; import { PiProvider } from '@composio/experimental'; import { createAgentSession, SessionManager } from '@earendil-works/pi-coding-agent'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); const piProvider = new PiProvider(); // Run the Pi agent over a session's tools and return its final text. async function runPi(tools: unknown, prompt: string) { const { session: pi } = await createAgentSession({ sessionManager: SessionManager.inMemory(process.cwd()), customTools: tools, tools: ['composio_search_tools', 'composio_manage_connections', 'composio_execute_tool'], }); let reply = ''; pi.subscribe((e) => { if (e.type === 'message_update' && e.assistantMessageEvent.type === 'text_delta') { reply += e.assistantMessageEvent.delta; } }); await pi.prompt(prompt); pi.dispose(); return reply; } // The smallest agent: one session, its tools, one prompt. export async function runAgent(userId: string, prompt: string) { const session = await composio.create(userId); const tools = piProvider.createSessionTools({ sessionId: session.sessionId, search: (params) => session.search(params), execute: (slug, args, options) => session.execute(slug, args, options), }); return runPi(tools, prompt); } ``` ## Put it in a Slack thread Turn the one-shot agent into a handler. Each Slack thread gets its own [session](/docs/configuring-sessions), reused so the agent keeps context, and the reply goes back with the `SLACKBOT_SEND_MESSAGE` tool. The session is keyed to the Slack user, so when Alice asks for a GitHub issue it opens as *Alice*, against her GitHub connection. **`bot.ts` — step 2: Wire it to Slack threads** ```typescript import { Composio } from '@composio/core'; import type { IncomingTriggerPayload } from '@composio/core'; import { PiProvider } from '@composio/experimental'; import { createAgentSession, SessionManager } from '@earendil-works/pi-coding-agent'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); const piProvider = new PiProvider(); const callbackUrl = `${process.env.APP_URL}/connections/callback`; // One session per Slack thread, reused so the agent keeps context, with a short // transcript for memory across turns. const threads = new Map(); const threadKey = (event: IncomingTriggerPayload) => `${event.payload?.channel}:${event.payload?.thread_ts ?? event.payload?.ts}`; async function sessionForThread(event: IncomingTriggerPayload) { const key = threadKey(event); const existing = threads.get(key); if (existing) return { session: await composio.use(existing.sessionId), memory: existing }; const session = await composio.create(event.userId, { manageConnections: { enable: true, callbackUrl, waitForConnections: true }, }); const memory = { sessionId: session.sessionId, history: [] as { role: string; content: string }[] }; threads.set(key, memory); return { session, memory }; } function toolsForSession(session) { return piProvider.createSessionTools({ sessionId: session.sessionId, callbackUrl, search: (params) => session.search(params), execute: (slug, args, options) => session.execute(slug, args, options), connections: { getToolkitStates: (toolkits) => session.toolkits({ toolkits }), authorizeToolkit: async (toolkit) => { const request = await session.authorize(toolkit, { callbackUrl }); return { status: 'needs_connection', redirectUrl: request.redirectUrl }; }, isConnected: (state) => state.connection?.isActive ?? false, }, }); } // Run the Pi agent over a session's tools and return its final text. async function runPi(tools: unknown, prompt: string) { const { session: pi } = await createAgentSession({ sessionManager: SessionManager.inMemory(process.cwd()), customTools: tools, tools: ['composio_search_tools', 'composio_manage_connections', 'composio_execute_tool'], }); let reply = ''; pi.subscribe((e) => { if (e.type === 'message_update' && e.assistantMessageEvent.type === 'text_delta') { reply += e.assistantMessageEvent.delta; } }); await pi.prompt(prompt); pi.dispose(); return reply; } // Reply to one Slack message as the user who sent it. async function handleSlackMessage(event: IncomingTriggerPayload) { const { session, memory } = await sessionForThread(event); const prompt = [...memory.history.map((m) => `${m.role}: ${m.content}`), `user: ${event.payload?.text}`].join('\n'); const reply = await runPi(toolsForSession(session), prompt); await session.execute('SLACKBOT_SEND_MESSAGE', { channel: event.payload?.channel, thread_ts: event.payload?.thread_ts, text: reply, }); memory.history.push({ role: 'user', content: event.payload?.text ?? '' }, { role: 'assistant', content: reply }); } ``` ## Share one workspace connection By default a connected account is **PRIVATE**: only its creator can use it. The install authorized the Slack connection as **SHARED**, so you pin it into every session. Now Alice's session has *her* GitHub connection but *the workspace's* Slack connection. It posts as the bot, and acts everywhere else as Alice. **`bot.ts` — step 3: Share one workspace connection** ```typescript import { Composio } from '@composio/core'; import type { IncomingTriggerPayload } from '@composio/core'; import { PiProvider } from '@composio/experimental'; import { createAgentSession, SessionManager } from '@earendil-works/pi-coding-agent'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); const piProvider = new PiProvider(); const callbackUrl = `${process.env.APP_URL}/connections/callback`; // One Slack connection, shared by the whole workspace. The bot posts as this // identity while acting in every other app as the individual user. const SHARED_SLACK_CONNECTION_ID = process.env.SLACK_CONNECTION_ID; // One session per Slack thread, reused so the agent keeps context, with a short // transcript for memory across turns. const threads = new Map(); const threadKey = (event: IncomingTriggerPayload) => `${event.payload?.channel}:${event.payload?.thread_ts ?? event.payload?.ts}`; async function sessionForThread(event: IncomingTriggerPayload) { const key = threadKey(event); const existing = threads.get(key); if (existing) return { session: await composio.use(existing.sessionId), memory: existing }; const session = await composio.create(event.userId, { // Pin the shared Slack connection; the session still resolves every other // toolkit against this user's own connections. connectedAccounts: { slackbot: [SHARED_SLACK_CONNECTION_ID] }, manageConnections: { enable: true, callbackUrl, waitForConnections: true }, }); const memory = { sessionId: session.sessionId, history: [] as { role: string; content: string }[] }; threads.set(key, memory); return { session, memory }; } function toolsForSession(session) { return piProvider.createSessionTools({ sessionId: session.sessionId, callbackUrl, search: (params) => session.search(params), execute: (slug, args, options) => session.execute(slug, args, options), connections: { getToolkitStates: (toolkits) => session.toolkits({ toolkits }), authorizeToolkit: async (toolkit) => { const request = await session.authorize(toolkit, { callbackUrl }); return { status: 'needs_connection', redirectUrl: request.redirectUrl }; }, isConnected: (state) => state.connection?.isActive ?? false, }, }); } // Run the Pi agent over a session's tools and return its final text. async function runPi(tools: unknown, prompt: string) { const { session: pi } = await createAgentSession({ sessionManager: SessionManager.inMemory(process.cwd()), customTools: tools, tools: ['composio_search_tools', 'composio_manage_connections', 'composio_execute_tool'], }); let reply = ''; pi.subscribe((e) => { if (e.type === 'message_update' && e.assistantMessageEvent.type === 'text_delta') { reply += e.assistantMessageEvent.delta; } }); await pi.prompt(prompt); pi.dispose(); return reply; } // Reply to one Slack message as the user who sent it. async function handleSlackMessage(event: IncomingTriggerPayload) { const { session, memory } = await sessionForThread(event); const prompt = [...memory.history.map((m) => `${m.role}: ${m.content}`), `user: ${event.payload?.text}`].join('\n'); const reply = await runPi(toolsForSession(session), prompt); await session.execute('SLACKBOT_SEND_MESSAGE', { channel: event.payload?.channel, thread_ts: event.payload?.thread_ts, text: reply, }); memory.history.push({ role: 'user', content: event.payload?.text ?? '' }, { role: 'assistant', content: reply }); } ``` ## Reach the gaps with the proxy Most Slack actions are `SLACKBOT_*` tools. The few that aren't, like the typing indicator and opening a DM channel, drop down to `session.proxyExecute`, which calls the Slack Web API with the pinned connection's auth so you never touch a token. **`bot.ts` — step 4: Reach the gaps with the proxy** ```typescript import { Composio } from '@composio/core'; import type { IncomingTriggerPayload } from '@composio/core'; import { PiProvider } from '@composio/experimental'; import { createAgentSession, SessionManager } from '@earendil-works/pi-coding-agent'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); const piProvider = new PiProvider(); const callbackUrl = `${process.env.APP_URL}/connections/callback`; // One Slack connection, shared by the whole workspace. The bot posts as this // identity while acting in every other app as the individual user. const SHARED_SLACK_CONNECTION_ID = process.env.SLACK_CONNECTION_ID; // One session per Slack thread, reused so the agent keeps context, with a short // transcript for memory across turns. const threads = new Map(); const threadKey = (event: IncomingTriggerPayload) => `${event.payload?.channel}:${event.payload?.thread_ts ?? event.payload?.ts}`; async function sessionForThread(event: IncomingTriggerPayload) { const key = threadKey(event); const existing = threads.get(key); if (existing) return { session: await composio.use(existing.sessionId), memory: existing }; const session = await composio.create(event.userId, { // Pin the shared Slack connection; the session still resolves every other // toolkit against this user's own connections. connectedAccounts: { slackbot: [SHARED_SLACK_CONNECTION_ID] }, manageConnections: { enable: true, callbackUrl, waitForConnections: true }, }); const memory = { sessionId: session.sessionId, history: [] as { role: string; content: string }[] }; threads.set(key, memory); return { session, memory }; } // Anything the toolkit doesn't wrap as a tool, reach via the proxy: it calls the // Slack Web API with the pinned connection's auth, so you never touch a token. async function setStatus(session, event: IncomingTriggerPayload, status: string) { await session .proxyExecute({ toolkit: 'slackbot', endpoint: 'https://slack.com/api/assistant.threads.setStatus', method: 'POST', body: { channel_id: event.payload?.channel, thread_ts: event.payload?.thread_ts, status }, }) .catch(() => {}); } async function openDm(session, userId: string): Promise { const res = await session.proxyExecute({ toolkit: 'slackbot', endpoint: 'https://slack.com/api/conversations.open', method: 'POST', body: { users: userId }, }); return res.data?.channel?.id; } function toolsForSession(session) { return piProvider.createSessionTools({ sessionId: session.sessionId, callbackUrl, search: (params) => session.search(params), execute: (slug, args, options) => session.execute(slug, args, options), connections: { getToolkitStates: (toolkits) => session.toolkits({ toolkits }), authorizeToolkit: async (toolkit) => { const request = await session.authorize(toolkit, { callbackUrl }); return { status: 'needs_connection', redirectUrl: request.redirectUrl }; }, isConnected: (state) => state.connection?.isActive ?? false, }, }); } // Run the Pi agent over a session's tools and return its final text. async function runPi(tools: unknown, prompt: string) { const { session: pi } = await createAgentSession({ sessionManager: SessionManager.inMemory(process.cwd()), customTools: tools, tools: ['composio_search_tools', 'composio_manage_connections', 'composio_execute_tool'], }); let reply = ''; pi.subscribe((e) => { if (e.type === 'message_update' && e.assistantMessageEvent.type === 'text_delta') { reply += e.assistantMessageEvent.delta; } }); await pi.prompt(prompt); pi.dispose(); return reply; } // Reply to one Slack message as the user who sent it. async function handleSlackMessage(event: IncomingTriggerPayload) { const { session, memory } = await sessionForThread(event); await setStatus(session, event, 'Working on it…'); const prompt = [...memory.history.map((m) => `${m.role}: ${m.content}`), `user: ${event.payload?.text}`].join('\n'); const reply = await runPi(toolsForSession(session), prompt); await session.execute('SLACKBOT_SEND_MESSAGE', { channel: event.payload?.channel, thread_ts: event.payload?.thread_ts, text: reply, }); memory.history.push({ role: 'user', content: event.payload?.text ?? '' }, { role: 'assistant', content: reply }); } ``` ## Redirect auth links The payoff. When the agent reaches for an app the user hasn't connected, the tool result carries a one-time Composio connect URL. You never want it in the channel or in the model's context. The bot extracts it, **redacts** it from the tool output, DMs it to the user privately, and the run resumes the moment they approve, because the session was created with `waitForConnections`. **`bot.ts` — step 5: Redirect auth links** ```typescript import { Composio } from '@composio/core'; import type { IncomingTriggerPayload } from '@composio/core'; import { PiProvider } from '@composio/experimental'; import { createAgentSession, SessionManager } from '@earendil-works/pi-coding-agent'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); const piProvider = new PiProvider(); const callbackUrl = `${process.env.APP_URL}/connections/callback`; // One Slack connection, shared by the whole workspace. The bot posts as this // identity while acting in every other app as the individual user. const SHARED_SLACK_CONNECTION_ID = process.env.SLACK_CONNECTION_ID; // One session per Slack thread, reused so the agent keeps context, with a short // transcript for memory across turns. const threads = new Map(); const threadKey = (event: IncomingTriggerPayload) => `${event.payload?.channel}:${event.payload?.thread_ts ?? event.payload?.ts}`; async function sessionForThread(event: IncomingTriggerPayload) { const key = threadKey(event); const existing = threads.get(key); if (existing) return { session: await composio.use(existing.sessionId), memory: existing }; const session = await composio.create(event.userId, { // Pin the shared Slack connection; the session still resolves every other // toolkit against this user's own connections. connectedAccounts: { slackbot: [SHARED_SLACK_CONNECTION_ID] }, manageConnections: { enable: true, callbackUrl, waitForConnections: true }, }); const memory = { sessionId: session.sessionId, history: [] as { role: string; content: string }[] }; threads.set(key, memory); return { session, memory }; } // Anything the toolkit doesn't wrap as a tool, reach via the proxy: it calls the // Slack Web API with the pinned connection's auth, so you never touch a token. async function setStatus(session, event: IncomingTriggerPayload, status: string) { await session .proxyExecute({ toolkit: 'slackbot', endpoint: 'https://slack.com/api/assistant.threads.setStatus', method: 'POST', body: { channel_id: event.payload?.channel, thread_ts: event.payload?.thread_ts, status }, }) .catch(() => {}); } async function openDm(session, userId: string): Promise { const res = await session.proxyExecute({ toolkit: 'slackbot', endpoint: 'https://slack.com/api/conversations.open', method: 'POST', body: { users: userId }, }); return res.data?.channel?.id; } // Redirect auth links. When a tool hits an app the user hasn't connected, the // result carries a one-time Composio connect URL. Never let the model or the // channel see it: redact it from the tool output and DM the user privately. The // session's manageConnections + waitForConnections resumes the run on approval. const CONNECT_LINK = /https:\/\/(?:connect\.composio\.dev|[^\s"']*composio[^\s"']*\/link)\/[^\s"')]+/gi; function redactLinks(value: T): T { if (typeof value === 'string') return value.replace(CONNECT_LINK, '[connection link sent via DM]') as T; if (Array.isArray(value)) return value.map(redactLinks) as T; if (value && typeof value === 'object') { return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redactLinks(v)])) as T; } return value; } async function handleAuthLinks(session, event: IncomingTriggerPayload, value: T): Promise { const links = [...new Set([...JSON.stringify(value ?? '').matchAll(CONNECT_LINK)].map((m) => m[0]))]; if (links.length > 0) { const dm = await openDm(session, event.userId); for (const url of links) { await session.execute('SLACKBOT_SEND_MESSAGE', { channel: dm, text: `*Connection needed.* Approve access and I'll continue automatically:\n<${url}|Connect>`, }); } } return redactLinks(value); // hand the model a result with the raw URL stripped } function toolsForSession(session, event: IncomingTriggerPayload) { return piProvider.createSessionTools({ sessionId: session.sessionId, callbackUrl, search: (params) => session.search(params), // Every tool result passes through handleAuthLinks: connect URLs get DM'd to // the user and redacted before the model ever sees them. execute: async (slug, args, options) => handleAuthLinks(session, event, await session.execute(slug, args, options)), connections: { getToolkitStates: (toolkits) => session.toolkits({ toolkits }), authorizeToolkit: async (toolkit) => { const request = await session.authorize(toolkit, { callbackUrl }); await handleAuthLinks(session, event, request.redirectUrl); return { status: 'needs_connection', redirectUrl: request.redirectUrl }; }, isConnected: (state) => state.connection?.isActive ?? false, }, }); } // Run the Pi agent over a session's tools and return its final text. async function runPi(tools: unknown, prompt: string) { const { session: pi } = await createAgentSession({ sessionManager: SessionManager.inMemory(process.cwd()), customTools: tools, tools: ['composio_search_tools', 'composio_manage_connections', 'composio_execute_tool'], }); let reply = ''; pi.subscribe((e) => { if (e.type === 'message_update' && e.assistantMessageEvent.type === 'text_delta') { reply += e.assistantMessageEvent.delta; } }); await pi.prompt(prompt); pi.dispose(); return reply; } // Reply to one Slack message as the user who sent it. async function handleSlackMessage(event: IncomingTriggerPayload) { const { session, memory } = await sessionForThread(event); await setStatus(session, event, 'Working on it…'); const prompt = [...memory.history.map((m) => `${m.role}: ${m.content}`), `user: ${event.payload?.text}`].join('\n'); const reply = await runPi(toolsForSession(session, event), prompt); await session.execute('SLACKBOT_SEND_MESSAGE', { channel: event.payload?.channel, thread_ts: event.payload?.thread_ts, text: reply, }); memory.history.push({ role: 'user', content: event.payload?.text ?? '' }, { role: 'assistant', content: reply }); } ``` ## Serve the webhook Verify each trigger's signature with `composio.triggers.verifyWebhook`, then hand the payload to `handleSlackMessage` off the response path so a slow handler doesn't get retried. That's the whole server. **`bot.ts` — step 6: Serve the webhook** ```typescript import { Composio } from '@composio/core'; import type { IncomingTriggerPayload } from '@composio/core'; import { PiProvider } from '@composio/experimental'; import { createAgentSession, SessionManager } from '@earendil-works/pi-coding-agent'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); const piProvider = new PiProvider(); const callbackUrl = `${process.env.APP_URL}/connections/callback`; // One Slack connection, shared by the whole workspace. The bot posts as this // identity while acting in every other app as the individual user. const SHARED_SLACK_CONNECTION_ID = process.env.SLACK_CONNECTION_ID; // One session per Slack thread, reused so the agent keeps context, with a short // transcript for memory across turns. const threads = new Map(); const threadKey = (event: IncomingTriggerPayload) => `${event.payload?.channel}:${event.payload?.thread_ts ?? event.payload?.ts}`; async function sessionForThread(event: IncomingTriggerPayload) { const key = threadKey(event); const existing = threads.get(key); if (existing) return { session: await composio.use(existing.sessionId), memory: existing }; const session = await composio.create(event.userId, { // Pin the shared Slack connection; the session still resolves every other // toolkit against this user's own connections. connectedAccounts: { slackbot: [SHARED_SLACK_CONNECTION_ID] }, manageConnections: { enable: true, callbackUrl, waitForConnections: true }, }); const memory = { sessionId: session.sessionId, history: [] as { role: string; content: string }[] }; threads.set(key, memory); return { session, memory }; } // Anything the toolkit doesn't wrap as a tool, reach via the proxy: it calls the // Slack Web API with the pinned connection's auth, so you never touch a token. async function setStatus(session, event: IncomingTriggerPayload, status: string) { await session .proxyExecute({ toolkit: 'slackbot', endpoint: 'https://slack.com/api/assistant.threads.setStatus', method: 'POST', body: { channel_id: event.payload?.channel, thread_ts: event.payload?.thread_ts, status }, }) .catch(() => {}); } async function openDm(session, userId: string): Promise { const res = await session.proxyExecute({ toolkit: 'slackbot', endpoint: 'https://slack.com/api/conversations.open', method: 'POST', body: { users: userId }, }); return res.data?.channel?.id; } // Redirect auth links. When a tool hits an app the user hasn't connected, the // result carries a one-time Composio connect URL. Never let the model or the // channel see it: redact it from the tool output and DM the user privately. The // session's manageConnections + waitForConnections resumes the run on approval. const CONNECT_LINK = /https:\/\/(?:connect\.composio\.dev|[^\s"']*composio[^\s"']*\/link)\/[^\s"')]+/gi; function redactLinks(value: T): T { if (typeof value === 'string') return value.replace(CONNECT_LINK, '[connection link sent via DM]') as T; if (Array.isArray(value)) return value.map(redactLinks) as T; if (value && typeof value === 'object') { return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redactLinks(v)])) as T; } return value; } async function handleAuthLinks(session, event: IncomingTriggerPayload, value: T): Promise { const links = [...new Set([...JSON.stringify(value ?? '').matchAll(CONNECT_LINK)].map((m) => m[0]))]; if (links.length > 0) { const dm = await openDm(session, event.userId); for (const url of links) { await session.execute('SLACKBOT_SEND_MESSAGE', { channel: dm, text: `*Connection needed.* Approve access and I'll continue automatically:\n<${url}|Connect>`, }); } } return redactLinks(value); // hand the model a result with the raw URL stripped } function toolsForSession(session, event: IncomingTriggerPayload) { return piProvider.createSessionTools({ sessionId: session.sessionId, callbackUrl, search: (params) => session.search(params), // Every tool result passes through handleAuthLinks: connect URLs get DM'd to // the user and redacted before the model ever sees them. execute: async (slug, args, options) => handleAuthLinks(session, event, await session.execute(slug, args, options)), connections: { getToolkitStates: (toolkits) => session.toolkits({ toolkits }), authorizeToolkit: async (toolkit) => { const request = await session.authorize(toolkit, { callbackUrl }); await handleAuthLinks(session, event, request.redirectUrl); return { status: 'needs_connection', redirectUrl: request.redirectUrl }; }, isConnected: (state) => state.connection?.isActive ?? false, }, }); } // Run the Pi agent over a session's tools and return its final text. async function runPi(tools: unknown, prompt: string) { const { session: pi } = await createAgentSession({ sessionManager: SessionManager.inMemory(process.cwd()), customTools: tools, tools: ['composio_search_tools', 'composio_manage_connections', 'composio_execute_tool'], }); let reply = ''; pi.subscribe((e) => { if (e.type === 'message_update' && e.assistantMessageEvent.type === 'text_delta') { reply += e.assistantMessageEvent.delta; } }); await pi.prompt(prompt); pi.dispose(); return reply; } // Reply to one Slack message as the user who sent it. async function handleSlackMessage(event: IncomingTriggerPayload) { const { session, memory } = await sessionForThread(event); await setStatus(session, event, 'Working on it…'); const prompt = [...memory.history.map((m) => `${m.role}: ${m.content}`), `user: ${event.payload?.text}`].join('\n'); const reply = await runPi(toolsForSession(session, event), prompt); await session.execute('SLACKBOT_SEND_MESSAGE', { channel: event.payload?.channel, thread_ts: event.payload?.thread_ts, text: reply, }); memory.history.push({ role: 'user', content: event.payload?.text ?? '' }, { role: 'assistant', content: reply }); } Bun.serve({ port: 3000, async fetch(req) { const url = new URL(req.url); if (req.method === 'POST' && url.pathname === '/webhooks/composio') { const { payload } = await composio.triggers.verifyWebhook({ payload: await req.text(), secret: process.env.COMPOSIO_WEBHOOK_SECRET, id: req.headers.get('webhook-id'), timestamp: req.headers.get('webhook-timestamp'), signature: req.headers.get('webhook-signature'), }); void handleSlackMessage(payload); return Response.json({ ok: true }); } return new Response('Not found', { status: 404 }); }, }); ``` # The whole project The two files above are the spine. The real project rounds them out with grouped auth-link DMs, per-user routing, message chunking, reaction acks, and durable storage. Here's a slice of the actual source, with the Composio touch-points highlighted. Browse the tree, read the files: > The complete project is on GitHub: [composio-slack-bot](https://github.com/ComposioHQ/composio-slack-bot). The complete project lives on GitHub: [composio-slack-bot](https://github.com/ComposioHQ/composio-slack-bot). # Run it Run `bun install.ts` once to set up the bot, start the server with `bun bot.ts`, then `@mention` the bot in any channel. It opens a session as you, finds the tool it needs, runs it against your connections, and replies in thread as the workspace bot, usually within a few seconds. Ask it to do something in an app you haven't connected yet and it DMs you a link first, then continues once you approve. - [Configuring sessions](/docs/configuring-sessions): Everything a session can scope: toolkits, tools, connections, and limits - [Shared connections](/docs/shared-connections): SHARED vs PRIVATE accounts and the per-user ACL --- # iMessage custom toolkit with eve (/examples/imessage-agent) Most Composio toolkits call a remote API. Some capabilities only exist on one machine, and iMessage is the classic case: there's no iMessage cloud API, so the agent has to run on your Mac and drive Messages.app directly. This example builds exactly that: a terminal agent that **texts on your behalf from your own Mac** and reaches the rest of your apps (Gmail, Calendar, GitHub, Slack) through the Composio catalog in the same breath. ``` you › read my last email and text Shams a summary you › text mom i'm running 10 min late you › what did Lena and I last text about? ``` The pattern is the takeaway, not iMessage specifically. It comes together from a handful of Composio pieces: 1. **A custom toolkit** wraps local iMessage (send, contacts, history, memory) as Composio tools. 2. **In-process execution** runs each tool right on your Mac through `session.execute`, with no remote API. 3. **One session** puts those local tools on the same surface as the 1000+ app catalog. 4. **The eve provider** turns `session.tools()` into [eve](https://github.com/vercel/eve)-native tools, so `eve dev` can call them. 5. **Triggers** let an outside event wake the agent and reach your phone, with in-chat auth through `COMPOSIO_MANAGE_CONNECTIONS` for any app you haven't connected. Below you build the integration core: the custom toolkit first, then the wiring that puts it on a session, then triggers, then a browse of the relevant source. You bring a Composio API key and a Mac. Composio brings the catalog. # Setup You need a [Composio API key](https://dashboard.composio.dev) and macOS (the iMessage tools drive Messages.app, Contacts.app, and the local `chat.db`). **Install** ```bash npm install @composio/core @composio/experimental eve ``` **Configure** ```txt title=".env.local" COMPOSIO_API_KEY=xxxxxxxxx ``` **Grant macOS permissions** (prompted on first use): Automation for Messages, Contacts access, and Full Disk Access to read `chat.db`. # The custom toolkit A custom toolkit is a named group of custom tools. Each tool declares an input schema and an `execute` that runs locally. Here is `SEND`, which shells out to AppleScript to drive Messages.app: **`imessage/send-message.ts` — complete file** ```typescript import { experimental_createTool } from '@composio/core'; import { z } from 'zod/v3'; import { runAppleScript, SEND_SCRIPT } from './applescript'; export const sendMessage = experimental_createTool('SEND', { name: 'Send iMessage', description: 'Send an iMessage from your Mac to a phone number or iMessage email.', preload: true, inputParams: z.object({ to: z.string().describe('Phone number or iMessage email.'), text: z.string().describe('Message body to send.'), }), execute: async ({ to, text }) => { await runAppleScript(SEND_SCRIPT, [to, text]); return { sent: true, to }; }, }); ``` Group your tools into a toolkit. The full project also ships `FIND_CONTACT` (fuzzy contact lookup over Contacts.app), `READ_MESSAGES` (recent messages from `chat.db`), and `RECALL`/`REMEMBER` (per-contact memory), each built the same way: ```ts title="imessage/index.ts" // @noErrors import { experimental_createToolkit } from '@composio/core'; import { sendMessage } from './send-message'; import { findContact } from './find-contact'; import { readMessages } from './read-messages'; import { recallContact, rememberContact } from './memory-tools'; export function createImessageToolkit() { return experimental_createToolkit('IMESSAGE', { name: 'iMessage', description: "Send and read iMessages, look up contacts, and remember people, locally on the user's Mac.", tools: [sendMessage, findContact, readMessages, recallContact, rememberContact], }); } ``` The toolkit depends only on `@composio/core`, so it drops into any Composio agent. Custom tools execute in-process through `session.execute`, which is why they aren't on the MCP URL yet. # Wire it up Set the [eve provider](/docs/providers/eve) on the client and register the toolkit on the session. `composio.ts` grows in three steps, one Composio concept each: ## Create the client with the eve provider The provider is what makes `session.tools()` return eve-native tools instead of raw Composio tools. Its approval policy pauses every iMessage send before the local AppleScript runs. **`composio.ts` — step 1: Client with the eve provider** ```typescript import { Composio } from '@composio/core'; import { EveProvider, requireApprovalForTools } from '@composio/experimental/eve'; export const composio = new Composio({ provider: new EveProvider({ needsApproval: requireApprovalForTools('LOCAL_IMESSAGE_SEND'), }), }); ``` ## Scope a session to the user `sessions.create` gives this user their own toolset, already wired to the full Composio catalog. **`composio.ts` — step 2: A session for the user** ```typescript import { Composio } from '@composio/core'; import { EveProvider, requireApprovalForTools } from '@composio/experimental/eve'; export const composio = new Composio({ provider: new EveProvider({ needsApproval: requireApprovalForTools('LOCAL_IMESSAGE_SEND'), }), }); export const session = composio.sessions.create('user_123'); ``` ## Register the local toolkit Pass the custom toolkit through `experimental.customToolkits`, and the local iMessage tools join the catalog on the same session. **`composio.ts` — step 3: Register the local toolkit** ```typescript import { Composio } from '@composio/core'; import { EveProvider, requireApprovalForTools } from '@composio/experimental/eve'; import { createImessageToolkit } from './imessage'; export const composio = new Composio({ provider: new EveProvider({ needsApproval: requireApprovalForTools('LOCAL_IMESSAGE_SEND'), }), }); export const session = composio.sessions.create('user_123', { experimental: { customToolkits: [createImessageToolkit()] }, }); ``` Now hand that session to eve. eve discovers tools from files, so `defineComposioTools(session)` returns the resolver that exposes `session.tools()`. One line: ```ts title="agent/tools/composio.ts" // @noErrors import { defineComposioTools } from '@composio/experimental/eve'; import { session } from '../../composio'; export default defineComposioTools(session); ``` ```ts title="agent/agent.ts" // @noErrors import { defineAgent } from 'eve'; export default defineAgent({ model: 'google/gemini-2.5-flash', }); ``` That's the whole integration. Run `eve dev` and talk to it. The agent can text a contact, read a thread, and act across every connected app, with auth handled in chat through `COMPOSIO_MANAGE_CONNECTIONS`. # Extend it: triggers The agent can text, so anything that can wake the agent can reach your phone. Composio **triggers** turn an external event into an agent run: subscribe to an app event, point Composio's webhook at your app, and act on each event with the same iMessage tools. For example, surface a Linear assignment and ask the user whether to inspect it. The first turn contains no issue title or body, so third-party text does not cross into the agent prompt before the user opts in: ```ts title="agent/channels/triggers.ts" // @noErrors import { defineChannel, POST } from 'eve/channels'; import { composio } from '../../composio'; export default defineChannel({ routes: [ POST('/webhook', async (req, { send }) => { const { payload: event } = await composio.triggers.parse(req, { verifySecret: process.env.COMPOSIO_WEBHOOK_SECRET, }); if (event.userId !== 'user_123' || event.triggerSlug !== 'LINEAR_ISSUE_ASSIGNED') { return new Response(null, { status: 202 }); } await send( `A verified Linear assignment event arrived. Ask whether I want to inspect it. ` + `Do not call tools in this turn. Event reference: ${event.uuid}.`, { auth: { authenticator: 'composio-webhook', principalType: 'service', principalId: event.userId, attributes: { triggerSlug: event.triggerSlug }, }, continuationToken: event.uuid, } ); return new Response(null, { status: 202 }); }), ], }); ``` Point Composio at your webhook and create the trigger, once each. Reuse the same `composio` client from `composio.ts`: ```ts title="agent/setup-triggers.ts" // @noErrors import { composio } from '../composio'; // Register the webhook URL once per project; store the returned // secret as COMPOSIO_WEBHOOK_SECRET. const subscription = await composio.triggers.setWebhookSubscription({ webhookUrl: `${process.env.APP_URL}/webhook`, }); // Use the exact slug from the Composio triggers catalog. const trigger = await composio.triggers.create('user_123', 'LINEAR_ISSUE_ASSIGNED'); console.log(`Trigger created: ${trigger.triggerId}`); ``` Run it once with `npx tsx agent/setup-triggers.ts`, and the webhook handler above takes over from there. Swap the trigger and the prompt to create another reflex. Keep the first turn content-free, then fetch third-party content only after the user asks to continue. # More reflexes The prompt is where each reflex earns its keep, but the webhook should not turn untrusted content into instructions. Here is the same two-step gate for Gmail: ```ts title="agent/channels/triggers.ts" // @noErrors import { defineChannel, POST } from 'eve/channels'; import { composio } from '../../composio'; export default defineChannel({ routes: [ POST('/webhook', async (req, { send }) => { const { payload: event } = await composio.triggers.parse(req, { verifySecret: process.env.COMPOSIO_WEBHOOK_SECRET, }); if (event.userId !== 'user_123' || event.triggerSlug !== 'GMAIL_NEW_GMAIL_MESSAGE') { return new Response(null, { status: 202 }); } await send( `A verified Gmail event arrived. Ask whether I want to inspect the email. ` + `Do not call tools in this turn. Event reference: ${event.uuid}.`, { auth: { authenticator: 'composio-webhook', principalType: 'service', principalId: event.userId, attributes: { triggerSlug: event.triggerSlug }, }, continuationToken: event.uuid, } ); return new Response(null, { status: 202 }); }), ], }); ``` If the user continues, the agent can fetch the email in a separate turn and require approval before any side effect. A few more patterns worth wiring up with the same gate: * **Stand-up nudge.** A calendar event ten minutes out texts you the agenda and the meeting link, pulled straight from the invite. * **Review request.** A new GitHub review request texts you the PR title and a one-line read of the diff, so you can reply "approve" from your phone. * **Money in.** A successful Stripe payment texts you the amount and the customer, no dashboard required. * **Cover for me.** A Slack mention while you're away texts a teammate and asks them to take a look. Use the exact slug from the Composio triggers catalog for each. Signature verification proves the event came through Composio; it does not make an email subject, issue title, or other third-party text trustworthy. Reject unexpected users and trigger slugs, keep external content out of the initial prompt, and require approval before side effects such as sending a message. # Browse the project The key files, in one place. The custom toolkit and the session wiring carry the integration, the eve provider is imported from `@composio/experimental/eve`, and the rest is local macOS glue. > The iMessage code browser is an implementation slice, not a standalone fixture. The complete runnable project will be published in the Composio examples repo. The complete, runnable project will be published in the Composio examples repo. # Run it The browser above is an implementation slice, not a standalone fixture: it omits the project's package manifest and supporting `handles`, `chat-db`, and `memory` modules. The complete runnable project will be published in the Composio examples repo. Until then, use the provider walkthrough on this page in an existing eve app and treat the iMessage source as a reference for the local toolkit. The first send or contact lookup prompts for macOS permission, and the first time the agent needs an app you haven't connected, it returns an auth link in chat through `COMPOSIO_MANAGE_CONNECTIONS`. - [eve provider](/docs/providers/eve): EveProvider, the defineComposioTools resolver, and the (ctx, next) hooks. - [Custom tools and toolkits](/docs/extending-sessions/custom-tools-and-toolkits): Build and register your own in-process Composio tools. --- # Examples (/examples) End-to-end builds that wire Composio into working agents. Each one is a complete project you can read top to bottom and run. - [General agent with Pi](/examples/general-agent-with-pi): Build a Pi + Composio agent and drop it into Slack: triggers, per-user sessions, a shared connection, redirected auth links, and the proxy. - [Daily standup bot](/examples/standup-slackbot): A Slack bot that drafts each teammate's standup from their own connected tools: your own Slack app, tool-router sessions, manual tool execution, the proxy, and per-member auth links. - [Local sandbox PR reviewer](/examples/local-sandbox-pr-reviewer): Run a PR reviewer in your own sandbox while it calls GitHub tools through a Composio session. - [iMessage custom toolkit with eve](/examples/imessage-agent): An agent that texts on your behalf from your own Mac: a custom toolkit wraps local iMessage in-process, and the eve provider puts it on the same session as the whole Composio catalog. --- # Review pull requests in a sandbox you own (/examples/local-sandbox-pr-reviewer) Composio usually runs your tools for you. A **local sandbox** is for the times you need to run them yourself: your filesystem, your shell, your security boundary. You still get [managed auth](/docs/authentication) and 1000+ apps; you just keep the code execution. This example builds a GitHub PR reviewer that does exactly that: it clones a pull request into a sandbox *you* own, runs the repo's real checks there, and posts one grounded comment. The sandbox here is E2B, but E2B is just the sample. The same pattern works with your own VM, container, Kubernetes job, or internal sandbox service. It comes down to a handful of Composio pieces: 1. **A local sandbox session** is a [Composio session](/docs/how-composio-works) with [code execution turned off](/docs/configuring-sessions#disabling-the-sandbox). Composio still does [discovery](/docs/how-composio-works#meta-tools) and auth; it just won't run code for you. 2. **The helper contract** is what comes back: a Python helper exposing the same [`run_composio_tool`, `invoke_llm`, and `web_search`](/docs/sandbox/remote) tools Composio's managed sandbox runs for you, plus the `env` it needs. You inject it into your sandbox and the agent calls it. 3. **Your sandbox is the boundary.** Tool *execution* happens in a box you control. E2B is the replaceable sample runner; the contract it honors is the real interface. > **The sandbox holds your project API key**: The `env` that `experimental_createLocalWorkbenchSession` returns includes your **project** `COMPOSIO_API_KEY`, and you inject that `env` into the sandbox. Anything running there can read it, including the untrusted PR code you clone and build. Treat the sandbox as your trust boundary: run it on infrastructure you control, give the reviewer a key scoped to only what it needs, and rotate the key if a run could have leaked it. Below you build the host orchestration from scratch: a bare client first, then a piece at a time up to the full run loop, then a browse of the real source. You bring a Composio API key and a place to run code. Composio brings the tools. # Setup You need a [Composio API key](https://dashboard.composio.dev), an OpenAI API key for the reviewer agent, a GitHub connection for your `COMPOSIO_USER_ID`, and [Bun](https://bun.sh). **No sandbox provider? Use the E2B sample runner** The host writes the Composio helper into a sandbox and runs the agent there, so it needs *somewhere* to run code. This example ships an [E2B](https://e2b.dev) runner in `src/sandbox/e2b.ts` so you can run it today with just an `E2B_API_KEY`. E2B is a hosted sandbox provider: that key provisions an isolated microVM to run the agent in, so you don't have to stand up a VM or container yourself. It's still real infrastructure, just E2B's to manage rather than yours. E2B is deliberately isolated to that one file. To run on your own VM, container, or CI worker, replace `createE2bSandbox` with anything that honors the same contract: create a directory, write `helperSource` into it, pass `env` to the process, stream stdout and stderr back, and tear down on your schedule. ```bash bun add @composio/core @composio/experimental e2b @openai/agents ``` Connect GitHub once for the user id you'll review as, then keep that same id for the review run: ```bash bun run connect ``` # Build the host `src/runner.ts` is the host: it owns orchestration, never tool execution. It starts as a bare Composio client and grows into the full run loop, one concept at a time. Each diff below is exactly what that concept adds. ## Create the Composio client The whole thing acts as one stable user, against the connections they own. Start there. **`src/runner.ts` — step 1: Create the Composio client** ```typescript import { Composio } from '@composio/core'; import { experimental_createLocalWorkbenchSession } from '@composio/experimental/workbench'; import { createE2bSandbox } from './sandbox/e2b'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); const userId = process.env.COMPOSIO_USER_ID ?? 'local-pr-reviewer-user'; ``` ## Check the GitHub connection A local sandbox still leans on Composio for auth and [tool discovery](/docs/how-composio-works#meta-tools); only code execution moves to your side. So before booting any infrastructure, confirm this user actually has [GitHub connected](/docs/authentication), and hand them a connect link if not. **`src/runner.ts` — step 2: Check the GitHub connection** ```typescript import { Composio } from '@composio/core'; import { experimental_createLocalWorkbenchSession } from '@composio/experimental/workbench'; import { createE2bSandbox } from './sandbox/e2b'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); const userId = process.env.COMPOSIO_USER_ID ?? 'local-pr-reviewer-user'; // Composio runs tools as a user. Before anything else, make sure this user has // an active GitHub connection. There's no point booting a sandbox without one. async function requireGithubConnection() { const list = await composio.connectedAccounts.list({ userIds: [userId], toolkitSlugs: ['github'], statuses: ['ACTIVE'], }); if (list.items?.[0]) return; const request = await composio.toolkits.authorize(userId, 'github'); throw new Error(`Connect GitHub first: ${request.redirectUrl}`); } ``` ## Create the local sandbox session The core of the integration. You create a [Composio session](/docs/configuring-sessions#creating-a-session) yourself with code execution off (`workbench.enable: false`, so Composio will not run code for you), then hand that session to `experimental_createLocalWorkbenchSession`. The helper validates the session is local (it errors if the session has the remote workbench enabled, because the managed workbench and a local sandbox can't both run for one session) and returns the pieces you run yourself: a `helperSource` (a Python helper with `run_composio_tool`, `invoke_llm`, and `web_search`) and the `env` that helper needs to reach Composio from inside your box. **`src/runner.ts` — step 3: Create the local sandbox session** ```typescript import { Composio } from '@composio/core'; import { experimental_createLocalWorkbenchSession } from '@composio/experimental/workbench'; import { createE2bSandbox } from './sandbox/e2b'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); const userId = process.env.COMPOSIO_USER_ID ?? 'local-pr-reviewer-user'; // Composio runs tools as a user. Before anything else, make sure this user has // an active GitHub connection. There's no point booting a sandbox without one. async function requireGithubConnection() { const list = await composio.connectedAccounts.list({ userIds: [userId], toolkitSlugs: ['github'], statuses: ['ACTIVE'], }); if (list.items?.[0]) return; const request = await composio.toolkits.authorize(userId, 'github'); throw new Error(`Connect GitHub first: ${request.redirectUrl}`); } // The local sandbox session. Create a normal Composio session yourself with // `workbench.enable: false` (Composio won't run code for you), then hand that // session to the helper, which validates it's local and gives you the pieces to // run code yourself, wherever you choose. async function createWorkbench() { const session = await composio.create(userId, { toolkits: ['github'], workbench: { enable: false }, }); return experimental_createLocalWorkbenchSession(composio, session); // returns { helperSource, env }: // helperSource: a Python helper exposing run_composio_tool / invoke_llm / web_search // env: the variables that helper needs to reach Composio from inside your box } ``` ## Start your sandbox, inject the helper Boot a box you control, write `helperSource` into it as `composio_helper.py`, and pass `env` to the process. That helper is the *only* Composio-specific thing your sandbox has to carry. E2B is the sample runner; swap it for anything that honors the same contract. **`src/runner.ts` — step 4: Start your sandbox, inject the helper** ```typescript import { Composio } from '@composio/core'; import { experimental_createLocalWorkbenchSession } from '@composio/experimental/workbench'; import { createE2bSandbox } from './sandbox/e2b'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); const userId = process.env.COMPOSIO_USER_ID ?? 'local-pr-reviewer-user'; // Composio runs tools as a user. Before anything else, make sure this user has // an active GitHub connection. There's no point booting a sandbox without one. async function requireGithubConnection() { const list = await composio.connectedAccounts.list({ userIds: [userId], toolkitSlugs: ['github'], statuses: ['ACTIVE'], }); if (list.items?.[0]) return; const request = await composio.toolkits.authorize(userId, 'github'); throw new Error(`Connect GitHub first: ${request.redirectUrl}`); } // The local sandbox session. Create a normal Composio session yourself with // `workbench.enable: false` (Composio won't run code for you), then hand that // session to the helper, which validates it's local and gives you the pieces to // run code yourself, wherever you choose. async function createWorkbench() { const session = await composio.create(userId, { toolkits: ['github'], workbench: { enable: false }, }); return experimental_createLocalWorkbenchSession(composio, session); // returns { helperSource, env }: // helperSource: a Python helper exposing run_composio_tool / invoke_llm / web_search // env: the variables that helper needs to reach Composio from inside your box } export async function runReview(repo: string, pr: number) { await requireGithubConnection(); const workbench = await createWorkbench(); // Start a sandbox you own, inject the helper, and pass the env. E2B is just // the sample runner; swap createE2bSandbox for any box that honors the same // contract: write a file, set env, run a command, stream output, tear down. const sandbox = await createE2bSandbox({ apiKey: process.env.E2B_API_KEY, helperSource: workbench.helperSource, // written as composio_helper.py env: workbench.env, }); } ``` ## Run the reviewer and stream output Run the agent inside the sandbox and stream its output back. Whenever the agent calls `run_composio_tool`, the helper routes that GitHub action back through Composio under this user's connection. Tool *execution* happens in your box; discovery and auth stay managed. **`src/runner.ts` — step 5: Run the reviewer and stream output** ```typescript import { Composio } from '@composio/core'; import { experimental_createLocalWorkbenchSession } from '@composio/experimental/workbench'; import { createE2bSandbox } from './sandbox/e2b'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); const userId = process.env.COMPOSIO_USER_ID ?? 'local-pr-reviewer-user'; // Composio runs tools as a user. Before anything else, make sure this user has // an active GitHub connection. There's no point booting a sandbox without one. async function requireGithubConnection() { const list = await composio.connectedAccounts.list({ userIds: [userId], toolkitSlugs: ['github'], statuses: ['ACTIVE'], }); if (list.items?.[0]) return; const request = await composio.toolkits.authorize(userId, 'github'); throw new Error(`Connect GitHub first: ${request.redirectUrl}`); } // The local sandbox session. Create a normal Composio session yourself with // `workbench.enable: false` (Composio won't run code for you), then hand that // session to the helper, which validates it's local and gives you the pieces to // run code yourself, wherever you choose. async function createWorkbench() { const session = await composio.create(userId, { toolkits: ['github'], workbench: { enable: false }, }); return experimental_createLocalWorkbenchSession(composio, session); // returns { helperSource, env }: // helperSource: a Python helper exposing run_composio_tool / invoke_llm / web_search // env: the variables that helper needs to reach Composio from inside your box } export async function runReview(repo: string, pr: number) { await requireGithubConnection(); const workbench = await createWorkbench(); // Start a sandbox you own, inject the helper, and pass the env. E2B is just // the sample runner; swap createE2bSandbox for any box that honors the same // contract: write a file, set env, run a command, stream output, tear down. const sandbox = await createE2bSandbox({ apiKey: process.env.E2B_API_KEY, helperSource: workbench.helperSource, // written as composio_helper.py env: workbench.env, }); // Run the reviewer agent inside the sandbox and stream its output back. The // agent calls run_composio_tool from composio_helper.py, which routes GitHub // actions back through Composio under this user's connection. const task = `Review PR #${pr} on ${repo}. Run the repo's real checks in this sandbox.`; try { await sandbox.run('npx --yes tsx agent.ts', { env: { ...workbench.env, TASK: task, OPENAI_API_KEY: process.env.OPENAI_API_KEY }, onStdout: (chunk) => process.stdout.write(chunk), onStderr: (chunk) => process.stderr.write(chunk), }); } finally { await sandbox.teardown(); } } ``` # The whole project The file above is the spine. The real project rounds it out with a CLI, a smoke/dry-run path, the E2B runner behind the sandbox contract, the reviewer agent and its review policy, and the `composio_helper.py` the helper source compiles to. Here's a slice of the actual source, with the Composio touch-points highlighted. Browse the tree, read the files: > The complete project is on GitHub: [composio-slack-bot](https://github.com/ComposioHQ/composio-slack-bot). The complete project lives on GitHub: [local-pr-reviewer](https://github.com/ComposioHQ/local-pr-reviewer). # Run it Dry-run first to validate your input with no credentials, network calls, or sandbox startup, then run it for real: ```bash bun run review -- --repo ComposioHQ/composio --pr 123 --dry-run bun run review -- --repo ComposioHQ/composio --pr 123 ``` The host opens a local sandbox session, boots the sandbox, and runs the repo's real checks inside it, then posts one grounded comment, or nothing if it can't build the PR. --- # Daily standup bot (/examples/standup-slackbot) Standup is a crucial part of running an effective engineering team, and also oh so tedious: every morning, everyone digs back through what they did and writes it up. It's worse for teams spread across timezones, where there's no shared standup to anchor the day, so it's easy to just forget. But the work you did *is* there: the PRs in GitHub, the docs in Notion, the decisions in Slack threads. If it's all recorded somewhere, an agent should be able to at least draft it. [Composio sessions](/docs/configuring-sessions) make this *incredibly easy for agents*: a session hands the agent everything it needs, [search](/docs/how-composio-works#meta-tools) to find the right tool, [parallel execute](/docs/how-composio-works#meta-tools) to run many at once, a [sandbox](/docs/sandbox/remote) and [volumes](/docs/sandbox/remote#files-and-mounts). It uses Composio to extract, parse, and cross-reference data across all those sources and write clean summaries of the real work your team shipped. All you have to do is create a session for your teammate and let it cook. ![The daily standup reminder in Slack](/images/standup-slackbot/slack-reminder.png) *The daily reminder, with Draft and Connect more tools buttons* ![A generated standup draft in Slack](/images/standup-slackbot/slack-draft.png) *The draft the agent writes, delivered to a teammate in Slack* So we built a Slack bot that does exactly that. Once a day, at a set time in each teammate's own timezone, it reminds them to post in the daily standup thread in a central channel. With one button click, they can run a subagent that uses their Composio connections to generate a clean, consolidated draft to review and post. We'll build it step by step. > **Is this the right example for you?**: This is a deliberately advanced, opinionated build. It's a strong reference for five things: * **Background-agent sessions**: the draft agent runs on a schedule, not in a conversation. It works from the tools a member already connected and never pauses to ask for auth. * **Manual execution for deterministic workflows**: outside the draft, the bot doesn't let an agent decide. It runs a fixed flow, calling tools directly with [manual execution](/docs/tools-direct/executing-tools), so a button always triggers the same exact steps. * **Manual, pre-connected auth**: members connect their tools ahead of time using [manual connections](/docs/manually-authenticating), and the agent just uses whatever is there. * **White-labelling** (advanced): your own Slack app and bot identity, via [white-labelling](/docs/white-labeling-authentication). This is *not* the easy path. We'd recommend Composio's managed apps, which require no additional configuration. Only do this if you specifically want your own branding. * **The proxy** (advanced): using [`proxyExecute`](/docs/extending-sessions/proxy-execute) to call Slack API endpoints Composio doesn't wrap as tools. It is **not** an example of [in-chat or dynamic auth](/docs/authentication) (asking a user to connect a tool mid-run), and it's more setup than many bots need. If you'd rather have a Slack bot with zero setup (Composio's managed app) or in-chat auth, start with the [general Slack bot](/examples/general-agent-with-pi) instead. The Slack bot itself follows a deterministic flow: the same menu every day. When a member taps a button, it launches a subagent with a Composio session to produce the draft. Here's the shape of it: # Setup You need a [Composio API key](https://dashboard.composio.dev/~/project/settings/api-keys), a Slack workspace you can install an app into, and Node with [tsx](https://nodejs.org). The finished bot deploys to [Vercel](https://vercel.com) as two serverless functions, a cron and an interactivity handler, so there's no long-running server. # Make your custom Slack bot This bot doesn't post as "Composio". It posts as *my* app, with its own name, icon, and (frankly ridiculous) face: ![The Daily Standup Bot avatar](/images/standup-slackbot/bot-avatar.png) *Create the app from scratch and name it* **Add the Bot Token Scopes.** Under **OAuth & Permissions**, add: `chat:write`, `im:write`, `channels:history`, `channels:read`, `users:read`, `users:read.email`, `team:read`. Then turn on **Interactivity** and point its Request URL at your deployment's `/api/interactivity`. ![Adding bot token scopes under OAuth & Permissions](/images/standup-slackbot/bot-scopes.png) *Add the bot token scopes* **Grab the app credentials.** On **Basic Information**, copy the **Client ID** and **Client Secret**. Composio drives the OAuth as your app with these. ![The app's Client ID and Secret under Basic Information](/images/standup-slackbot/app-credentials.png) *Copy the Client ID and Secret* # Auth the bot The Slack app exists; now connect it through Composio so your code can act as it. You create one `slackbot` auth config from your credentials, then a setup script does the OAuth once with Composio's [manual authentication](/docs/manually-authenticating) flow. > **`slackbot` vs `slack`**: Composio has two Slack toolkits, and this bot uses both: * **`slackbot`** authenticates a Slack *app* and acts as the **bot** (a bot token). It posts the reminders and drafts as "Daily Standup Bot," and it's the one you white-label here. * **`slack`** authenticates an individual **user** and acts as *them* (a user token). Each teammate connects this so the bot can post their standup under their own name and read their activity for context. Rule of thumb: posting *as the bot* uses `slackbot`; doing something *as a person* uses `slack`. **Create an auth config and pick the `Slackbot` toolkit.** In the [Composio dashboard](https://dashboard.composio.dev/~/project/auth-configs), click **Create Auth Config** and search `slackbot`. Choose **Slackbot**, *not* `Slack`: `Slackbot` posts as the bot identity, while `Slack` acts as an individual user. ![Choosing the Slackbot toolkit, not Slack](/images/standup-slackbot/auth-config-slackbot.png) *Pick Slackbot, not Slack* **Use your own credentials.** Pick **OAuth 2.0**, then **Your Own Credentials**, and paste the Client ID and Secret from before. Add `team:read` to the user token scopes. This is the white-label step: your app, your name, your face. ![Selecting Your Own Credentials and entering the Client ID and Secret](/images/standup-slackbot/auth-config-credentials.png) *Use your own credentials* **Save the auth config id.** Once created, copy its `ac_...` id into `COMPOSIO_SLACKBOT_AUTH_CONFIG_ID`. This is the one auth config your app uses to take actions on behalf of your bot. ![The created slackbot auth config with its ac_ id](/images/standup-slackbot/auth-config-created.png) *The created auth config and its ac_ id* **Run the setup script to connect the bot.** For this bot, we first need to connect the bot itself to Composio, which only needs to be done once. The script creates an OAuth link for you to connect your *Slack bot* to Composio, which lets you use Composio to send messages on behalf of your bot. **`scripts/setup.ts` — complete file** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); const AUTH_CONFIG = process.env.COMPOSIO_SLACKBOT_AUTH_CONFIG_ID!; // Connect the bot's own Slack app once, so it can post and DM as the bot. async function main() { const session = await composio.create('default', { authConfigs: { slackbot: AUTH_CONFIG }, }); const toolkits = await session.toolkits({ toolkits: ['slackbot'] }); const active = toolkits.items.find((t) => t.slug === 'slackbot')?.connection?.isActive; if (active) { console.log('Bot already connected.'); return; } // Not connected: print the Connect Link, then wait for the user to finish. const connectionRequest = await session.authorize('slackbot'); console.log('Authorize the bot:', connectionRequest.redirectUrl); const account = await connectionRequest.waitForConnection(); console.log('Bot connected:', account.id); } main(); ``` The first run prints a link and waits: ```text ╭─────────────────────────────────────────────────────────╮ │ Daily Standup Bot: One-Time Setup │ ╰─────────────────────────────────────────────────────────╯ ✅ Auth config has the required user scopes. · Bot is not connected yet. Generating an authorization link… Open this URL in your browser to authorize the bot: https://backend.composio.dev/s/AbC123xy Waiting for you to complete the OAuth flow (Ctrl+C to abort)… ✅ Bot connected to Slack. ────────────────────────────────────────────────────────────────────── 🎉 Setup complete. Invite the bot to your standup channel and point your Slack app's Interactivity Request URL at https:///api/interactivity ────────────────────────────────────────────────────────────────────── ``` Open that link to approve the bot, and the connection goes live: ![Approving the bot's OAuth connection](/images/standup-slackbot/oauth-approve.png) *Approve the bot in Slack* ![Composio successfully connected to Slackbot](/images/standup-slackbot/connected.png) *Connected* The script is idempotent and repeatable. Forgot a scope, or hit an issue? No stress, just re-run it with `--reconnect`. # Talk to Slack To send and update messages in our deterministic bot workflow, we use Composio's `SLACKBOT_SEND_MESSAGE` and `SLACKBOT_UPDATES_A_MESSAGE` tools via [manual tool execution](/docs/tools-direct/executing-tools). `SLACKBOT_SEND_MESSAGE` takes Block Kit `blocks`, so a message with interactive buttons can go through a tool too. **`api/_utils/slack.ts` — step 1: Send and update with named tools** ```typescript import { composio } from './composio'; const BOT_USER = 'default'; // Sending a message is a named tool, even when it carries interactive buttons: // SLACKBOT_SEND_MESSAGE takes markdown_text for prose, or Block Kit `blocks`. export async function postMessage(channel: string, text: string, blocks?: unknown[]) { const res = await composio.tools.execute('SLACKBOT_SEND_MESSAGE', { userId: BOT_USER, arguments: blocks ? { channel, blocks } : { channel, markdown_text: text }, }); return res.data as { ts?: string }; } // Updating the draft after an edit is a tool too: SLACKBOT_UPDATES_A_MESSAGE. export async function updateMessage(channel: string, ts: string, blocks: unknown[]) { await composio.tools.execute('SLACKBOT_UPDATES_A_MESSAGE', { userId: BOT_USER, arguments: { channel, ts, blocks }, }); } ``` When a Slack action has no tool, like opening a modal (`views.open`), it drops to [`proxyExecute`](/docs/extending-sessions/proxy-execute): the escape hatch for anything the named tools don't cover, hitting any Slack Web API endpoint as a connected account with no token in your code. **`api/_utils/slack.ts` — step 2: The proxy for the one thing without a tool** ```typescript import { composio } from './composio'; const BOT_USER = 'default'; // Sending a message is a named tool, even when it carries interactive buttons: // SLACKBOT_SEND_MESSAGE takes markdown_text for prose, or Block Kit `blocks`. export async function postMessage(channel: string, text: string, blocks?: unknown[]) { const res = await composio.tools.execute('SLACKBOT_SEND_MESSAGE', { userId: BOT_USER, arguments: blocks ? { channel, blocks } : { channel, markdown_text: text }, }); return res.data as { ts?: string }; } // Updating the draft after an edit is a tool too: SLACKBOT_UPDATES_A_MESSAGE. export async function updateMessage(channel: string, ts: string, blocks: unknown[]) { await composio.tools.execute('SLACKBOT_UPDATES_A_MESSAGE', { userId: BOT_USER, arguments: { channel, ts, blocks }, }); } // Opening a modal (views.open) has no Composio tool, so it drops to the proxy. // proxyExecute hits the raw endpoint as the bot's connected account, the escape // hatch for anything the named tools don't cover. export async function openModal(triggerId: string, view: unknown) { const { items } = await composio.connectedAccounts.list({ userIds: [BOT_USER], toolkitSlugs: ['slackbot'], statuses: ['ACTIVE'], }); await composio.tools.proxyExecute({ endpoint: '/views.open', method: 'POST', body: { trigger_id: triggerId, view }, connectedAccountId: items[0]?.id, }); } ``` # Make the buttons work Our StandUp bot gives the user two options every morning: **Draft** or **Connect more tools**. Each message uses [Block Kit](https://api.slack.com/block-kit) to create those buttons. For each button we define an `action_id` that lets us recognise which button was clicked. ```ts declare const memberEmail: string, dmChannel: string, dmTs: string; // the reminder's Draft button const draftButton = { type: 'button', style: 'primary', text: { type: 'plain_text', text: '📝 Draft' }, action_id: 'draft', value: JSON.stringify({ memberEmail, dmChannel, dmTs }), }; ``` ![The daily standup reminder in Slack](/images/standup-slackbot/slack-reminder.png) *The daily reminder, with Draft and Connect more tools buttons* When it's clicked, Slack POSTs to your `/api/interactivity` handler. Verify the request, ack within Slack's 3-second window, then route on the `action_id`: **`api/interactivity.ts` — complete file** ```typescript import { verifySlackSignature, readRawBody, updateMessage, postAsMember } from './_utils/slack'; import { generateDraft } from './_utils/agent'; import { draftMessage, connectMenu } from './_utils/blocks'; // Slack POSTs here every time someone clicks a button. Verify it really came // from Slack, then ack within 3 seconds (Slack retries if you're slow). export default async function handler(req: Request, res: Response) { const body = await readRawBody(req); if (!verifySlackSignature(body, req.headers)) return res.status(401).end(); const payload = JSON.parse(new URLSearchParams(body).get('payload') ?? '{}'); res.status(200).end(); // ack immediately await handleClick(payload); // then do the slow work } // Each button carried its context in `value`, so the handler knows exactly what // to do. No model decides anything here: the flow is fixed. async function handleClick(payload: any) { const action = payload.actions?.[0]; const ctx = JSON.parse(action?.value ?? '{}'); if (action?.action_id === 'draft') { const draft = await generateDraft(ctx.memberEmail); // launch the subagent await updateMessage(ctx.dmChannel, ctx.dmTs, draftMessage(draft, ctx)); } else if (action?.action_id === 'connect') { await updateMessage(ctx.dmChannel, ctx.dmTs, connectMenu(ctx)); } else if (action?.action_id === 'confirm') { await postAsMember(ctx.memberEmail, ctx.channel, ctx.draft, ctx.threadTs); } } ``` **Connect more tools** generates a per-member OAuth link for each toolkit the member hasn't connected, so they can add a source without leaving Slack: ![The connect-more-tools menu in Slack](/images/standup-slackbot/slack-connect.png) *Connect more tools, each button a per-member OAuth link* **Edit** opens a modal (`views.open` through the proxy), and **Confirm** posts the draft into the day's thread as the member. # Draft the standup Now this is the cool and magical part, and the easy part: all the background agent needs is a tool-router session and a prompt. When a member taps **Draft**, you spin up a session scoped to the toolkit catalogue and let the agent research and write. ## A session writes the draft A [tool-router session](/docs/configuring-sessions) gives the agent its tools. Pass the member's email and your full list of toolkits, hand the tools to the model, and let it investigate and write. You don't have to check which ones the member set up: the session only exposes tools for the accounts they've actually connected, and ignores the rest. **`api/_utils/agent.ts` — step 1: A tool-router session writes the draft** ```typescript import { Composio } from '@composio/core'; import { VercelProvider } from '@composio/vercel'; import { generateText, stepCountIs } from 'ai'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY, provider: new VercelProvider(), }); // The toolkits the bot can draft from. A member only has some of these connected, // and that's fine: the session just uses whatever they've actually authorized. const TOOLKITS = ['github', 'linear', 'notion', 'googlecalendar', 'slack']; // Spin up a tool-router session for one member and let the agent research and // write their standup. session.tools() returns Composio's research meta-tools // (search / execute / workbench), scoped to those toolkits. export async function generateDraft(memberEmail: string) { const session = await composio.create(memberEmail, { toolkits: TOOLKITS }); const tools = await session.tools(); const { text } = await generateText({ model: 'anthropic/claude-sonnet-4-5', system: "Write a concise daily standup from the member's recent activity.", prompt: 'Research and write the standup update.', tools, stopWhen: stepCountIs(40), }); return text.trim(); } ``` ## Use what's connected, nothing more The router can also *manage* connections, asking the user to authorize any toolkits they haven't connected yet. During a draft you don't want that: if the agent reaches for a tool the member hasn't connected, it should skip it, not prompt them to log in. `manageConnections: false` removes those meta-tools, so the agent drafts from exactly what's already connected. **`api/_utils/agent.ts` — step 2: Keep the agent from connecting mid-draft** ```typescript import { Composio } from '@composio/core'; import { VercelProvider } from '@composio/vercel'; import { generateText, stepCountIs } from 'ai'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY, provider: new VercelProvider(), }); // The toolkits the bot can draft from. A member only has some of these connected, // and that's fine: the session just uses whatever they've actually authorized. const TOOLKITS = ['github', 'linear', 'notion', 'googlecalendar', 'slack']; // Spin up a tool-router session for one member and let the agent research and // write their standup. session.tools() returns Composio's research meta-tools // (search / execute / workbench), scoped to those toolkits. export async function generateDraft(memberEmail: string) { // manageConnections:false strips the connection meta-tools. The agent drafts // from whatever the member already connected and never starts an OAuth flow // mid-draft: if a tool needs auth, it's simply not in the session. const session = await composio.create(memberEmail, { toolkits: TOOLKITS, manageConnections: false, }); const tools = await session.tools(); const { text } = await generateText({ model: 'anthropic/claude-sonnet-4-5', system: "Write a concise daily standup from the member's recent activity.", prompt: 'Research and write the standup update.', tools, stopWhen: stepCountIs(40), }); return text.trim(); } ``` The bot posts the result back as a draft the member can confirm or edit: ![A generated standup draft in Slack with Confirm and Edit buttons](/images/standup-slackbot/slack-draft.png) *The draft the agent writes, delivered to a teammate in Slack* # The whole project > The complete project is on GitHub: [composio-slack-bot](https://github.com/ComposioHQ/composio-slack-bot). # Run it Edit `standup.config.ts` with your team (each member's Slack email and timezone, plus your channel and GitHub org), set your four environment variables, run `npx tsx scripts/setup.ts` once to connect your bot, then `vercel deploy`. - [Configuring sessions](/docs/configuring-sessions): What a session can scope: toolkits, tools, connections, and connection management - [White-labeling authentication](/docs/white-labeling-authentication): Ship a bot under your own app's name, icon, and credentials - [Custom vs managed auth](/docs/custom-app-vs-managed-app): Bring-your-own Slack app versus a Composio-managed connection - [Triggers](/docs/triggers): Run agents in response to events: schedules, webhooks, and app activity --- # API Reference --- # Errors (/reference/errors) Composio uses conventional HTTP response codes to indicate the success or failure of an API request. In general: codes in the `2xx` range indicate success, codes in the `4xx` range indicate an error with the information provided, and codes in the `5xx` range indicate an error with Composio's servers. # The error object ```json { "error": { "message": "No connected account found for this user and toolkit", "status": 400, "request_id": "req_abc123def456", "suggested_fix": "Connect the user to the toolkit first" } } ``` ## Attributes | Attribute | Description | | --------------- | --------------------------------------------------------------------------- | | `message` | A human-readable message providing details about the error. | | `status` | The HTTP status code. | | `request_id` | A unique identifier for this request. Include this when contacting support. | | `suggested_fix` | When available, guidance on how to resolve the error. | # HTTP status codes | Code | Status | Description | | ------------------ | -------------------- | ------------------------------------------------------------------------------------------------ | | 200 | OK | Everything worked as expected. | | 400 | Bad Request | The request was unacceptable, often due to missing a required parameter. | | 401 | Unauthorized | No valid API key provided. | | 403 | Forbidden | The API key doesn't have permissions to perform the request. | | 404 | Not Found | The requested resource doesn't exist. | | 409 | Conflict | The request conflicts with another request (perhaps due to using the same idempotent key). | | 422 | Unprocessable Entity | The request was valid but cannot be processed. | | 429 | Too Many Requests | Too many requests hit the API too quickly. We recommend an exponential backoff of your requests. | | 500, 502, 503, 504 | Server Errors | Something went wrong on Composio's end. | # Error types ## Authentication errors Composio uses two types of API keys: * **Project API key** (`x-api-key`) — For project-level operations * **Organization API key** (`x-org-api-key`) — For organization-level access across projects | Error | Cause | | -------------------------- | --------------------------------------------------------------------------------------------------- | | Invalid API key | The API key is incorrect or revoked. Verify in [Settings](https://dashboard.composio.dev/settings). | | No authentication provided | The request is missing the `x-api-key` or `x-org-api-key` header. | | Invalid organization key | The organization API key is incorrect or revoked. Verify in Organization Settings. | | Insufficient permissions | The API key doesn't have access to this resource. | > See [Authenticating users](/docs/authentication) for more help. ## Tool errors Errors that occur when fetching or executing tools. | Error | Cause | | --------------------- | ------------------------------------------------------------------------------------------ | | Tool not found | The tool slug doesn't exist. Tool slugs are case-sensitive and use `SCREAMING_SNAKE_CASE`. | | No connected account | The user hasn't connected to this toolkit yet. | | Tool execution failed | The external service returned an error. Check tool parameters and user permissions. | > See [Tools and toolkits](/docs/how-composio-works) for more help. ## Connection errors Errors related to connected accounts. | Error | Cause | | --------------------------- | ---------------------------------------------------------------- | | Connected account not found | The `connectedAccountId` doesn't exist or was deleted. | | Auth refresh required | The OAuth token has expired. Prompt the user to re-authenticate. | | Connected account deleted | The connection was removed. Create a new connection. | ## Trigger errors Errors related to trigger subscriptions. | Error | Cause | | ------------------------ | -------------------------------------------------------------- | | Trigger not found | The trigger slug doesn't exist for this toolkit. | | Trigger instance deleted | The trigger subscription or its connected account was removed. | > See [Triggers](/docs/triggers) for more help. # Rate limiting When you hit rate limits, you'll receive a `429` status code. See [Rate Limits](/reference/rate-limits) for details on limits by plan and best practices for handling rate limit errors. # Getting help When contacting support, include the `request_id` from the error response. - [Discord](https://discord.com/channels/1170785031560646836/1268871288156323901): Community support - [Email](mailto:support@composio.dev): Contact support team - [GitHub](https://github.com/ComposioHQ/composio/issues/new?labels=bug): Report a bug --- # Glossary (/reference/glossary) ### Auth Config A blueprint that defines how authentication works for a toolkit: the auth method (`OAUTH2`, `API_KEY`, `BEARER_TOKEN`, or `BASIC`), scopes, and credentials. A [session](/docs/how-composio-works) creates one automatically when it needs one. To use your own OAuth credentials or non-default scopes, [create a custom one](/docs/auth-configuration/custom-auth-configs). ### Auth Scheme The authentication method an auth config uses, such as `OAUTH2`, `API_KEY`, `BEARER_TOKEN`, or `BASIC`. ### Callback URL The URL a user returns to after completing an OAuth flow through a Connect Link. You pass it as `callbackUrl` when you initiate authentication. ### Composio API Key A project-scoped secret that authenticates your SDK and API requests. Every resource you create with it is scoped to that project. ### Composio Managed Auth The default mode in which Composio supplies its own OAuth app credentials for each toolkit. It requires no setup. ### Connect Link A hosted page where a user authorizes access to a toolkit. It is returned as a `redirect_url` from `session.authorize()` or `connectedAccounts.link()`, and Composio manages the full OAuth flow. See [Authentication](/docs/authentication). ### Connected Account A stored set of credentials (OAuth tokens or API keys) linked to a userID, created when a user authenticates with a toolkit. Composio refreshes OAuth tokens automatically, and a user can have [multiple connected accounts](/docs/managing-multiple-connected-accounts) for the same toolkit. IDs are prefixed `ca_`. ### Connection Request The object returned when you initiate authentication. It contains the Connect Link URL and a `waitForConnection()` method that resolves once the user completes the flow. ### Custom Tool A tool you define yourself and use alongside Composio's built-in tools. Add local experimental custom tools and custom toolkits through [Custom tools and toolkits](/docs/extending-sessions/custom-tools-and-toolkits). ### In-Chat Authentication A flow in which the agent handles authentication itself by calling `COMPOSIO_MANAGE_CONNECTIONS` to generate a Connect Link and send it to the user in the conversation. See [In-chat authentication](/docs/authentication#in-chat-authentication). ### MCP (Model Context Protocol) An open protocol for connecting AI models to external tools. Create a session with `mcp: true` to expose `session.mcp.url` and `session.mcp.headers`, an MCP-compatible endpoint any MCP client can connect to. See [Using sessions via MCP](/docs/sessions-via-mcp). ### Manual Authentication Authenticating users from your own code with `session.authorize()` or `connectedAccounts.link()`, rather than letting the agent handle it through in-chat authentication. See [Manual authentication](/docs/manually-authenticating). ### Meta Tools A set of tools included in every session: `COMPOSIO_SEARCH_TOOLS`, `COMPOSIO_GET_TOOL_SCHEMAS`, `COMPOSIO_MANAGE_CONNECTIONS`, `COMPOSIO_MULTI_EXECUTE_TOOL`, `COMPOSIO_REMOTE_WORKBENCH`, and `COMPOSIO_REMOTE_BASH_TOOL`. They let the agent discover tools, manage auth, execute in parallel, and run code without loading hundreds of tool definitions upfront. See [Meta Tools Reference](/toolkits/meta-tools). ### Modifiers Middleware that transforms tool behavior. [Schema modifiers](/docs/tools-direct/modify-tool-behavior/schema-modifiers) change a tool's schema before the agent sees it, [before-execution modifiers](/docs/tools-direct/modify-tool-behavior/before-execution-modifiers) change arguments before a tool runs, and [after-execution modifiers](/docs/tools-direct/modify-tool-behavior/after-execution-modifiers) transform the result. In Python, [`@before_file_upload`](/docs/tools-direct/modify-tool-behavior/before-execution-modifiers#before-file-upload-python) intercepts local paths for `file_uploadable` parameters before read and upload. ### Native Tools Tools you access through provider packages with `session.tools()` and call directly, rather than over MCP. Both paths give the agent the same capabilities, but tools called directly integrate with your AI framework and support modifiers and custom tools. ### Organization The top-level Composio account entity. It contains team members and projects. ### Organization API Key A key (`x-org-api-key`) for organization-level operations such as creating and managing projects. It is distinct from the project-scoped Composio API Key. ### Project An isolated environment within an organization that scopes API keys, connected accounts, auth configs, and webhooks. Resources in one project are inaccessible from another. IDs are prefixed `proj_`. See [Projects](/reference/api-reference/projects). ### Proxy Execute Making authenticated HTTP requests through a toolkit's connected account without a predefined tool. Use it for API endpoints Composio has no built-in tool for. ### Provider An adapter package that transforms Composio tools into the format an AI framework expects (OpenAI, Anthropic, LangChain, Vercel AI SDK, and others). See [Providers](/docs/providers). ### Session An ephemeral, immutable configuration object returned by `composio.create(userId)`. It ties together a userID, the available toolkits, an auth config, and connected accounts, and it exposes `tools()`, `authorize()`, and `toolkits()` (plus `mcp.url` when created with `mcp: true`). See [What is a session?](/docs/how-composio-works). ### Session ID The unique identifier for a session. Meta tools use it internally to share context across calls within the same session. ### Tool An individual action an agent can execute. It has an input schema and an output schema, and is named `{TOOLKIT}_{ACTION}` (for example, `GITHUB_CREATE_ISSUE`). ### Tool Slug A tool's unique identifier, in the `{TOOLKIT}_{ACTION}` pattern (for example, `GITHUB_CREATE_ISSUE`). ### Toolkit A collection of related tools for a single external service. Users connect to a toolkit through authentication, and all of its tools execute with the user's credentials. ### Toolkit Slug The lowercase identifier for a toolkit (for example, `github`, `gmail`, or `slack`). Use it when configuring sessions, fetching tools, or creating triggers. ### Toolkit Versioning Pinning a toolkit to a specific version so your integration keeps a consistent set of tools even as Composio updates its definitions. See [Toolkit versioning](/docs/tools-direct/toolkit-versioning). ### Trigger A source that sends structured payloads to your application when something happens in a connected app. Triggers come in two kinds: realtime (the provider pushes events the moment they happen, for example Slack, GitHub, or Asana) and polling (Composio checks the provider on a schedule, for example Gmail). Either way, events arrive at your subscription or webhook URL. See [Triggers](/docs/triggers). ### Trigger Instance A specific, active trigger scoped to a user's connected account. ### Webhook Endpoint The ingress URL Composio issues per OAuth app for webhook triggers, plus the signing secret used to verify each inbound request. Composio configures it for you in most cases. When a trigger type's `requires_webhook_endpoint_setup` flag is `true`, you configure it yourself once per OAuth app through the [Webhook Endpoints API](/reference/api-reference/webhook-endpoints). See [Custom OAuth webhooks](/docs/setting-up-triggers/custom-oauth-webhooks). ### Webhook Subscription The URL Composio delivers signed events to in your application. There is one per project, configured through the dashboard or the [Webhook Subscriptions API](/reference/api-reference/webhook-subscriptions). See [Subscribing to events](/docs/setting-up-triggers/subscribing-to-events). ### userID An identifier from your application that Composio uses to scope connected accounts, tool executions, and authorizations. Connections are fully isolated between userIDs. See [What is a session?](/docs/how-composio-works). ### White-Labeling Customizing the auth experience so users see your brand during the OAuth flow. You provide your own OAuth credentials, redirect URIs, and branding. See [White-labeling authentication](/docs/white-labeling-authentication). ### Sandbox A persistent Python environment (previously called the workbench) exposed through the `COMPOSIO_REMOTE_WORKBENCH` meta tool. Its state persists across calls within a session, which makes it useful for bulk operations, data transformations, and processing large tool responses. Configure it with the `sandbox` session key; `workbench` still works as an alias. See [Sandbox](/docs/sandbox/remote). --- # Overview (/reference) Composio powers tool discovery, execution, authentication, and context management for your AI agents with 1000+ toolkits. This reference covers our REST APIs and SDKs. # Quick Reference * **Base URL**: * **[Authenticating to Composio](/reference/authenticating-to-composio)**: `x-api-key` (project) or `x-org-api-key` (organization) header * **[Rate Limits](/reference/rate-limits)**: 2K-10K requests per minute (plan-dependent) # REST API | API | Description | | ----------------------------------------------------------------- | ------------------------------------------------------------- | | [Tool Router](/reference/api-reference/tool-router) | Session-based API for AI agents to discover and execute tools | | [Tools](/reference/api-reference/tools) | List, search, and execute individual actions | | [Connected Accounts](/reference/api-reference/connected-accounts) | Manage user OAuth connections to apps | | [Auth Configs](/reference/api-reference/auth-configs) | Configure how users authenticate to toolkits | | [Triggers](/reference/api-reference/triggers) | Subscribe to webhooks from connected apps | | [Toolkits](/reference/api-reference/toolkits) | Browse available apps and their tools | # SDK Reference - [TypeScript SDK](/reference/sdk-reference/typescript): TypeScript SDK reference - [Python SDK](/reference/sdk-reference/python): Python SDK reference --- # Rate Limits (/reference/rate-limits) Composio enforces rate limits **per organization** over a fixed one-minute window. Every authenticated endpoint draws from the same budget — tool execution, connected accounts, triggers, and the rest — so the limit below is your organization's total across all API calls. # Rate limits by plan | Plan | Rate limit | Window | | ---------- | --------------- | -------- | | Starter | 2,000 requests | 1 minute | | Hobby | 2,000 requests | 1 minute | | Growth | 10,000 requests | 1 minute | | Enterprise | Custom | - | # Rate limit headers Every response includes headers so you can track usage without guessing: | Header | Description | | ------------------------- | ------------------------------------------------------- | | `X-RateLimit` | Total requests allowed in the current window | | `X-RateLimit-Remaining` | Requests remaining in the current window | | `X-RateLimit-Window-Size` | Window size (e.g., `60s` for 60 seconds) | | `Retry-After` | Seconds until the window resets (only on 429 responses) | # Rate limit response When you exceed the rate limit, you'll receive a `429 Too Many Requests` response: ```json { "message": "Rate limit exceeded. Limit: 10000 requests per 1 minutes" } ``` # Best practices 1. **Watch `X-RateLimit-Remaining`** — read it on each response to know how much headroom you have left in the window. 2. **Honor `Retry-After`** — on a `429`, wait the number of seconds it gives you before retrying instead of hammering the endpoint. 3. **Cache what doesn't change** — keep tool definitions and other static data client-side so you don't spend requests re-fetching them. # Need higher limits? If you hit these limits regularly, upgrade your plan or [talk to us](https://calendly.com/composiohq/enterprise) about custom limits for your use case. - [Errors](/reference/errors): Understanding API error responses - [Pricing](https://composio.dev/pricing): Compare plans and limits --- # Overview (/reference/authenticating-to-composio) Every Composio API request authenticates with an API key. Send the key in a request header and Composio resolves it to your project or organization. Composio has three kinds of API key. They share the same authentication flow — they differ in what they can reach. | Key | Header | Scope | | -------------------------------------------------------------------------- | --------------- | ------------------------------------------------- | | Project API key | `x-api-key` | Full access to a single project. | | Organization API key | `x-org-api-key` | Access across every project in your organization. | | Scoped project API key **· New** | `x-api-key` | A chosen subset of a single project's resources. | # Project API key A project API key authenticates to one project with full access. Use it for most application code. Get it from the dashboard: sign in to [composio.dev](https://composio.dev), open **Settings → Project Settings**, and copy the key from the **API Keys** section. Send it in the `x-api-key` header: ```bash curl https://backend.composio.dev/api/v3.1/tools \ -H "x-api-key: $COMPOSIO_API_KEY" ``` # Organization API key An organization API key authenticates across every project in your organization. Use it for organization-level endpoints. Get it from the dashboard: open **Organization Settings → General Settings** and copy a token under **Organization Access Tokens**. Send it in the `x-org-api-key` header: ```bash curl https://backend.composio.dev/api/v3.1/org/projects \ -H "x-org-api-key: $COMPOSIO_ORG_API_KEY" ``` # Scoped project API key A scoped project API key authenticates to a single project but reaches only the resources you grant it — for example, executing tools without managing connected accounts. It uses the same `x-api-key` header as a default project key. Scope a key to the least it needs, then send it like any project key: ```bash curl https://backend.composio.dev/api/v3.1/tools/execute/HACKERNEWS_GET_USER \ -H "x-api-key: $COMPOSIO_SCOPED_API_KEY" \ -H "Content-Type: application/json" \ -d '{"arguments": {"username": "pg"}}' ``` See [Scoped project API keys](/reference/authenticating-to-composio/project-api-key-permissions) for the permission areas, access levels, and the routes each one covers. - [Scoped project API keys](/reference/authenticating-to-composio/project-api-key-permissions): Permission areas, access levels, and covered routes - [Errors](/reference/errors): Understanding API error responses - [Rate Limits](/reference/rate-limits): API rate limits by plan --- # Scoped Project API Key (/reference/authenticating-to-composio/project-api-key-permissions) [Video: Scoped Project API Key walkthrough](https://youtube.com/watch?v=ySMu9lljkWg) A scoped project API key lets you choose which project resources the key can access. Reach for one when a key needs only a subset of your project, such as executing tools, reading logs, or managing connected accounts. > You pick a key's permissions when you create it, and they can't be changed afterward. To adjust them, create a new key and rotate your application to use it. > Default project API keys keep full project API key access. Scoped keys use the permission areas and access levels on this page. # Create a scoped API key Create a scoped key from the dashboard: Go to the [Composio Dashboard](https://dashboard.composio.dev). Select **Platform**. Select your project. Go to **Settings**. Open the **API Keys** tab. Click **Create API Key**, then choose the permission areas and access levels below. # Access levels | Access level | What it allows | | -------------- | ------------------------------------------------------------------- | | No access | The key cannot use routes in that permission area. | | Read only | The key can use read routes in that permission area. | | Write only | The key can use write routes in that permission area. | | Read and write | The key can use both read and write routes in that permission area. | Some read routes use `POST` because the request body carries filters or lookup input. The access level is based on what the route does, not only the HTTP method. When v3 and v3.1 expose the same route shape, this page lists one representative version instead of repeating both. Version-specific routes are listed separately. # Permission areas Jump to each permission area to see the routes it covers. | Permission area | Available levels | Routes | | ------------------ | ------------------------------------------------ | ---------------------------------- | | Auth configs | No access, Read only, Write only, Read and write | [View routes](#auth-configs) | | Connected accounts | No access, Read only, Write only, Read and write | [View routes](#connected-accounts) | | Tools | No access, Read only | [View routes](#tools) | | Tool execution | No access, Write only | [View routes](#tool-execution) | | Proxy execute | No access, Write only | [View routes](#proxy-execute) | | Toolkits | No access, Read only, Write only, Read and write | [View routes](#toolkits) | | Triggers | No access, Read only, Write only, Read and write | [View routes](#triggers) | | Webhooks | No access, Read only, Write only, Read and write | [View routes](#webhooks) | | Observability | No access, Read only | [View routes](#observability) | | Sessions | No access, Read only, Write only, Read and write | [View routes](#sessions) | # Auth configs View and modify auth configs. | Access | Method | Endpoint | | ------ | -------- | ---------------------------------------- | | Read | `GET` | `/api/v3/auth_configs` | | Read | `GET` | `/api/v3/auth_configs/{nanoid}` | | Write | `POST` | `/api/v3/auth_configs` | | Write | `PATCH` | `/api/v3/auth_configs/{nanoid}` | | Write | `DELETE` | `/api/v3/auth_configs/{nanoid}` | | Write | `PATCH` | `/api/v3/auth_configs/{nanoid}/{status}` | # Connected accounts View and manage connected accounts. | Access | Method | Endpoint | | ------ | -------- | ---------------------------------------------- | | Read | `GET` | `/api/v3/connected_accounts` | | Read | `GET` | `/api/v3/connected_accounts/{nanoid}` | | Write | `POST` | `/api/v3/connected_accounts` | | Write | `POST` | `/api/v3/connected_accounts/link` | | Write | `PATCH` | `/api/v3/connected_accounts/{nanoid}` | | Write | `PATCH` | `/api/v3/connected_accounts/{nanoid}/status` | | Write | `POST` | `/api/v3/connected_accounts/{nanoid}/refresh` | | Write | `DELETE` | `/api/v3/connected_accounts/{nanoid}` | | Write | `POST` | `/api/v3.1/connected_accounts/{nanoid}/revoke` | # Tools View tool definitions, inputs, scopes, and versions. | Access | Method | Endpoint | | ------ | ------ | ---------------------------------------------- | | Read | `GET` | `/api/v3.1/tools` | | Read | `GET` | `/api/v3.1/tools/enum` | | Read | `GET` | `/api/v3.1/tools/{tool_slug}` | | Read | `GET` | `/api/v3/tools/{tool_slug}/get_latest_version` | | Read | `GET` | `/api/v3.1/tools/scopes/required` | | Read | `GET` | `/api/v3.1/tools/get_scopes_required` | | Read | `POST` | `/api/v3.1/tools/execute/{tool_slug}/input` | # Tool execution Execute predefined Composio tools. | Access | Method | Endpoint | | ------ | ------ | ------------------------------------- | | Write | `POST` | `/api/v3.1/tools/execute/{tool_slug}` | | Write | `POST` | `/api/v3/files/upload/request` | | Write | `POST` | `/api/v3/files/upload/response` | | Write | `GET` | `/api/v3/files/list` | # Proxy execute Execute raw proxy requests against connected accounts. Proxy execute is separate from tool execution. Grant it only when your application needs to call a connected account API through the raw proxy path. | Access | Method | Endpoint | | ------ | ------ | -------------------------------------------------------- | | Write | `POST` | `/api/v3.1/tools/execute/proxy` | | Write | `POST` | `/api/v3/tool_router/session/{session_id}/proxy_execute` | # Toolkits View and install toolkits. | Access | Method | Endpoint | | ------ | ------ | ----------------------------- | | Read | `GET` | `/api/v3/toolkits` | | Read | `GET` | `/api/v3/toolkits/{slug}` | | Read | `GET` | `/api/v3/toolkits/categories` | | Read | `GET` | `/api/v3/toolkits/changelog` | | Write | `POST` | `/api/v3/toolkits/multi` | # Triggers View trigger types, manage trigger instances, and subscribe to trigger events. The realtime routes are called by the SDK (`triggers.subscribe()`) and the CLI to receive trigger events. | Access | Method | Endpoint | | ------ | -------- | ---------------------------------------------- | | Read | `GET` | `/api/v3/triggers_types` | | Read | `GET` | `/api/v3/triggers_types/{slug}` | | Read | `GET` | `/api/v3/triggers_types/list/enum` | | Read | `GET` | `/api/v3/trigger_instances/active` | | Read | `GET` | `/api/v3/cli/realtime/credentials` | | Read | `POST` | `/api/v3/cli/realtime/auth` | | Read | `GET` | `/api/v3/internal/sdk/realtime/credentials` | | Read | `POST` | `/api/v3/internal/sdk/realtime/auth` | | Write | `POST` | `/api/v3/trigger_instances/{slug}/upsert` | | Write | `PATCH` | `/api/v3/trigger_instances/manage/{triggerId}` | | Write | `DELETE` | `/api/v3/trigger_instances/manage/{triggerId}` | # Webhooks View and manage webhook endpoints and subscriptions. | Access | Method | Endpoint | | ------ | -------- | -------------------------------------------------- | | Read | `GET` | `/api/v3/webhook_endpoints` | | Read | `GET` | `/api/v3/webhook_endpoints/{nano_id}` | | Read | `GET` | `/api/v3/webhook_endpoints/schema` | | Read | `GET` | `/api/v3/webhook_subscriptions` | | Read | `GET` | `/api/v3/webhook_subscriptions/{id}` | | Read | `GET` | `/api/v3/webhook_subscriptions/event_types` | | Write | `POST` | `/api/v3/webhook_endpoints` | | Write | `POST` | `/api/v3/webhook_endpoints/{nano_id}` | | Write | `PATCH` | `/api/v3/webhook_endpoints/{nano_id}` | | Write | `DELETE` | `/api/v3/webhook_endpoints/{nano_id}` | | Write | `POST` | `/api/v3/webhook_subscriptions` | | Write | `PATCH` | `/api/v3/webhook_subscriptions/{id}` | | Write | `DELETE` | `/api/v3/webhook_subscriptions/{id}` | | Write | `POST` | `/api/v3/webhook_subscriptions/{id}/rotate_secret` | # Observability View execution logs and project usage summaries. | Access | Method | Endpoint | | ------ | ------ | --------------------------------------- | | Read | `POST` | `/api/v3.1/logs/tool_execution` | | Read | `GET` | `/api/v3.1/logs/tool_execution/{id}` | | Read | `POST` | `/api/v3.1/project/usage/{entity_type}` | | Read | `POST` | `/api/v3.1/project/usage/summary` | # Sessions Create and operate sessions and MCP servers. This permission area covers MCP server management, the MCP runtime transport, and the tool router MCP transport. | Access | Method | Endpoint | | ------ | -------- | ------------------------------------------------------------------------- | | Read | `GET` | `/api/v3/mcp/servers` | | Read | `GET` | `/api/v3/mcp/{id}` | | Read | `GET` | `/api/v3/mcp/app/{app_key}` | | Read | `GET` | `/api/v3/mcp/servers/{server_id}/instances` | | Read | `GET` | `/tool_router/{session_id}/mcp` | | Read | `GET` | `/api/v3.1/tool_router/session/{session_id}` | | Read | `GET` | `/api/v3/tool_router/session/{session_id}/toolkits` | | Read | `GET` | `/api/v3.1/tool_router/session/{session_id}/tools` | | Read | `GET` | `/api/v3/tool_router/session/{session_id}/mounts/{mount_id}/items` | | Read | `GET` | `/api/v3.1/tool_router/session/{session_id}/config_history` | | Write | `POST` | `/api/v3/mcp/servers` | | Write | `POST` | `/api/v3/mcp/servers/generate` | | Write | `POST` | `/api/v3/mcp/servers/custom` | | Write | `PATCH` | `/api/v3/mcp/{id}` | | Write | `DELETE` | `/api/v3/mcp/{id}` | | Write | `POST` | `/api/v3/mcp/servers/{server_id}/instances` | | Write | `DELETE` | `/api/v3/mcp/servers/{server_id}/instances/{instance_id}` | | Write | `POST` | `/api/v3/mcp/{server_id}/{transport}` | | Write | `DELETE` | `/api/v3/mcp/{server_id}/{transport}` | | Write | `POST` | `/tool_router/{session_id}/mcp` | | Write | `DELETE` | `/tool_router/{session_id}/mcp` | | Write | `POST` | `/api/v3.1/tool_router/session` | | Write | `POST` | `/api/v3.1/tool_router/session/{session_id}/execute` | | Write | `POST` | `/api/v3.1/tool_router/session/{session_id}/execute_meta` | | Write | `POST` | `/api/v3/tool_router/session/{session_id}/link` | | Write | `POST` | `/api/v3.1/tool_router/session/{session_id}/search` | | Write | `PATCH` | `/api/v3.1/tool_router/session/{session_id}` | | Write | `POST` | `/api/v3/tool_router/session/{session_id}/mounts/{mount_id}/upload_url` | | Write | `POST` | `/api/v3/tool_router/session/{session_id}/mounts/{mount_id}/download_url` | | Write | `POST` | `/api/v3/tool_router/session/{session_id}/mounts/{mount_id}/delete` | | Write | `POST` | `/api/v3.1/tool_router/session/{session_id}/attach` | # What to read next - [Authenticating to Composio](/reference/authenticating-to-composio): Authenticate API requests with project and organization API keys - [Projects](/reference/api-reference/projects): Understand projects, project API keys, and project-scoped resources - [Proxy execute](/reference/api-reference/tools): Call connected account APIs through the raw proxy path - [Observability](/reference/api-reference/logs): Inspect tool execution logs and usage summaries --- # Authentication (/reference/v3/authentication) Every Composio API request authenticates with an API key. Send the key in a request header and Composio resolves it to your project or organization. # Project API key A project API key authenticates to one project with full access. Send it in the `x-api-key` header. Get it from the dashboard: sign in to [composio.dev](https://composio.dev), open **Settings → Project Settings**, and copy the key from the **API Keys** section. # Organization API key An organization API key authenticates across every project in your organization. Send it in the `x-org-api-key` header. Get it from the dashboard: open **Organization Settings → General Settings** and copy a token under **Organization Access Tokens**. # Using the API key Include your API key in the request header: ```bash curl https://backend.composio.dev/api/v3/tools \ -H "x-api-key: $COMPOSIO_API_KEY" ``` For organization-level endpoints: ```bash curl https://backend.composio.dev/api/v3/org/projects \ -H "x-org-api-key: $COMPOSIO_ORG_API_KEY" ``` - [Errors](/reference/v3/errors): Understanding API error responses - [Rate Limits](/reference/v3/rate-limits): API rate limits by plan --- # Errors (/reference/v3/errors) Composio uses conventional HTTP response codes to indicate the success or failure of an API request. In general: codes in the `2xx` range indicate success, codes in the `4xx` range indicate an error with the information provided, and codes in the `5xx` range indicate an error with Composio's servers. # The error object ```json { "error": { "message": "No connected account found for this user and toolkit", "status": 400, "request_id": "req_abc123def456", "suggested_fix": "Connect the user to the toolkit first" } } ``` ## Attributes | Attribute | Description | | --------------- | --------------------------------------------------------------------------- | | `message` | A human-readable message providing details about the error. | | `status` | The HTTP status code. | | `request_id` | A unique identifier for this request. Include this when contacting support. | | `suggested_fix` | When available, guidance on how to resolve the error. | # HTTP status codes | Code | Status | Description | | ------------------ | -------------------- | ------------------------------------------------------------------------------------------------ | | 200 | OK | Everything worked as expected. | | 400 | Bad Request | The request was unacceptable, often due to missing a required parameter. | | 401 | Unauthorized | No valid API key provided. | | 403 | Forbidden | The API key doesn't have permissions to perform the request. | | 404 | Not Found | The requested resource doesn't exist. | | 409 | Conflict | The request conflicts with another request (perhaps due to using the same idempotent key). | | 422 | Unprocessable Entity | The request was valid but cannot be processed. | | 429 | Too Many Requests | Too many requests hit the API too quickly. We recommend an exponential backoff of your requests. | | 500, 502, 503, 504 | Server Errors | Something went wrong on Composio's end. | # Error types ## Authentication errors Composio uses two types of API keys: * **Project API key** (`x-api-key`) — For project-level operations * **Organization API key** (`x-org-api-key`) — For organization-level access across projects | Error | Cause | | -------------------------- | --------------------------------------------------------------------------------------------------- | | Invalid API key | The API key is incorrect or revoked. Verify in [Settings](https://dashboard.composio.dev/settings). | | No authentication provided | The request is missing the `x-api-key` or `x-org-api-key` header. | | Invalid organization key | The organization API key is incorrect or revoked. Verify in Organization Settings. | | Insufficient permissions | The API key doesn't have access to this resource. | > See [Authenticating users](/docs/authentication) for more help. ## Tool errors Errors that occur when fetching or executing tools. | Error | Cause | | --------------------- | ------------------------------------------------------------------------------------------ | | Tool not found | The tool slug doesn't exist. Tool slugs are case-sensitive and use `SCREAMING_SNAKE_CASE`. | | No connected account | The user hasn't connected to this toolkit yet. | | Tool execution failed | The external service returned an error. Check tool parameters and user permissions. | > See [Tools and toolkits](/docs/how-composio-works) for more help. ## Connection errors Errors related to connected accounts. | Error | Cause | | --------------------------- | ---------------------------------------------------------------- | | Connected account not found | The `connectedAccountId` doesn't exist or was deleted. | | Auth refresh required | The OAuth token has expired. Prompt the user to re-authenticate. | | Connected account deleted | The connection was removed. Create a new connection. | ## Trigger errors Errors related to trigger subscriptions. | Error | Cause | | ------------------------ | -------------------------------------------------------------- | | Trigger not found | The trigger slug doesn't exist for this toolkit. | | Trigger instance deleted | The trigger subscription or its connected account was removed. | > See [Triggers](/docs/triggers) for more help. # Rate limiting When you hit rate limits, you'll receive a `429` status code. See [Rate Limits](/reference/v3/rate-limits) for details on limits by plan and best practices for handling rate limit errors. # Getting help When contacting support, include the `request_id` from the error response. - [Discord](https://discord.com/channels/1170785031560646836/1268871288156323901): Community support - [Email](mailto:support@composio.dev): Contact support team - [GitHub](https://github.com/ComposioHQ/composio/issues/new?labels=bug): Report a bug --- # Overview (/reference/v3) Composio powers tool discovery, execution, authentication, and context management for your AI agents with 1000+ toolkits. This reference covers our REST APIs and SDKs. # Quick Reference * **Base URL**: * **[Authentication](/reference/v3/authentication)**: `x-api-key` (project) or `x-org-api-key` (organization) header * **[Rate Limits](/reference/v3/rate-limits)**: 2K-10K requests per minute (plan-dependent) # REST API | API | Description | | -------------------------------------------------------------------- | ------------------------------------------------------------- | | [Tool Router](/reference/v3/api-reference/tool-router) | Session-based API for AI agents to discover and execute tools | | [Tools](/reference/v3/api-reference/tools) | List, search, and execute individual actions | | [Connected Accounts](/reference/v3/api-reference/connected-accounts) | Manage user OAuth connections to apps | | [Auth Configs](/reference/v3/api-reference/auth-configs) | Configure how users authenticate to toolkits | | [Triggers](/reference/v3/api-reference/triggers) | Subscribe to webhooks from connected apps | | [Toolkits](/reference/v3/api-reference/toolkits) | Browse available apps and their tools | --- # Rate Limits (/reference/v3/rate-limits) Composio enforces rate limits **per organization** over a fixed one-minute window. Every authenticated endpoint draws from the same budget — tool execution, connected accounts, triggers, and the rest — so the limit below is your organization's total across all API calls. # Rate limits by plan | Plan | Rate limit | Window | | ---------- | --------------- | -------- | | Starter | 2,000 requests | 1 minute | | Hobby | 2,000 requests | 1 minute | | Growth | 10,000 requests | 1 minute | | Enterprise | Custom | - | # Rate limit headers Every response includes headers so you can track usage without guessing: | Header | Description | | ------------------------- | ------------------------------------------------------- | | `X-RateLimit` | Total requests allowed in the current window | | `X-RateLimit-Remaining` | Requests remaining in the current window | | `X-RateLimit-Window-Size` | Window size (e.g., `60s` for 60 seconds) | | `Retry-After` | Seconds until the window resets (only on 429 responses) | # Rate limit response When you exceed the rate limit, you'll receive a `429 Too Many Requests` response: ```json { "message": "Rate limit exceeded. Limit: 10000 requests per 1 minutes" } ``` # Best practices 1. **Watch `X-RateLimit-Remaining`** — read it on each response to know how much headroom you have left in the window. 2. **Honor `Retry-After`** — on a `429`, wait the number of seconds it gives you before retrying instead of hammering the endpoint. 3. **Cache what doesn't change** — keep tool definitions and other static data client-side so you don't spend requests re-fetching them. # Need higher limits? If you hit these limits regularly, upgrade your plan or [talk to us](https://calendly.com/composiohq/enterprise) about custom limits for your use case. - [Errors](/reference/v3/errors): Understanding API error responses - [Pricing](https://composio.dev/pricing): Compare plans and limits --- # API Keys (/reference/api-reference/api-keys) {/* Auto-generated from OpenAPI spec. Do not edit directly. */} API key management # Endpoints --- # Auth Configs (/reference/api-reference/auth-configs) {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/auth-configs.mdx, not this file. */} An auth config is a blueprint that defines how a toolkit authenticates across all your users. It specifies the authentication method, the scopes your tools can request, and which credentials Composio uses to run the OAuth or token flow. A single auth config applies to every user who connects that toolkit. When a user authenticates against it, Composio creates a [connected account](/reference/api-reference/connected-accounts) that stores their tokens and links them to your user ID. Each auth config defines: * **Auth scheme**: OAuth2, API key, Bearer token, or Basic Auth * **Scopes**: what your tools are allowed to do on the user's behalf * **Credentials**: Composio's managed app, or your own OAuth client and secrets Reach for a custom auth config when you need your own branding on consent screens, custom scopes, a dedicated rate-limit quota, or a custom toolkit instance. See [managed vs custom auth](/docs/custom-app-vs-managed-app) for the decision and [how Composio handles authentication](/docs/authentication) for the full picture. # Auth schemes The `auth_scheme` on an auth config determines how users authenticate to the toolkit. Composio supports four. The schemes available for a given toolkit come from the toolkit itself. | Scheme | What it is | When it's used | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `OAUTH2` | OAuth 2.0 authorization-code flow. The user authorizes through a hosted consent screen, and Composio stores and automatically refreshes the access and refresh tokens. | Most apps with user accounts (Gmail, GitHub, Slack, Notion, and so on). Uses Composio's managed OAuth app by default; bring your own for custom branding or scopes. | | `API_KEY` | A static API key the user provides. There's no OAuth flow: the key is stored on the connected account and sent on each request. | Services that authenticate with a key, such as SendGrid, Tavily, or PostHog. | | `BEARER_TOKEN` | A bearer access token you already hold (for example, from your own OAuth flow). Composio sends it as `Authorization: Bearer ` and does not refresh it, so you keep it current. | Bringing an existing OAuth or server-to-server token into Composio, or apps that issue long-lived tokens. | | `BASIC` | HTTP Basic authentication with a username and password. | Services that use Basic Auth. | Most OAuth toolkits work out of the box with Composio managed auth. For the others you supply the credential fields. To choose or customize the scheme, see [managed vs custom auth](/docs/custom-app-vs-managed-app). These endpoints use your project API key in the `x-api-key` header. Each auth config is addressed by its `nanoid`, and you can enable or disable one without deleting it. # Endpoints --- # Connected Accounts (/reference/api-reference/connected-accounts) {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/connected-accounts.mdx, not this file. */} A connected account is a single user's authorized connection to a toolkit. It stores their credentials (OAuth tokens or API keys) and links them to your user ID, so your tools can act on that user's behalf. Composio creates a connected account when a user completes the flow defined by an [auth config](/reference/api-reference/auth-configs). From there you manage its full lifecycle: * **Create or link**: start a new connection, or generate an auth link session for the user to authorize. See [manually authenticating users](/docs/manually-authenticating). * **Refresh**: renew authentication for an account whose tokens have expired. * **Enable, disable, or update**: change an account's status or metadata without removing it. * **Revoke or delete**: revoke the grant at the provider, or remove the account from Composio. Each account is addressed by its `nanoid`. List endpoints accept filters so you can find accounts by user, toolkit, or auth config. # Link auth (Composio Connect Links) A Composio Connect Link is a hosted, secure sign-in page. You create one with the create auth link session endpoint, redirect the user to the returned URL, and Composio handles the rest: the user signs in, Composio creates the connected account, and Composio stores and refreshes its tokens. Credentials never pass through your app. This works for all Composio managed connections, with no OAuth credentials to set up. By default a connected account is `PRIVATE` and usable only by its owning user. Mark one `SHARED` to let other users reach it through a per-connection access control list. See [shared connections](/docs/shared-connections). These endpoints use your project API key in the `x-api-key` header. > Shared-connection ACL fields are experimental and nested under an `experimental` block on the wire. Pin a specific SDK version if you depend on the current shape. # Endpoints --- # Files (/reference/api-reference/files) {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/files.mdx, not this file. */} These endpoints handle files that tools read and write during execution. When a tool produces or consumes a file, Composio stores it in object storage and exchanges it through presigned URLs rather than streaming bytes through the API. You reach for these endpoints to: * **List files** that tools have generated, optionally filtered by app and action. * **Request an upload**: get a presigned S3 URL, `PUT` your file to it, then pass the returned reference into a tool's input. This keeps large payloads out of request bodies. Tools receive a file reference and resolve the underlying object on their side. > File uploads are a two-step flow. Call the upload-request endpoint to mint a presigned URL, then upload the file contents directly to that URL. The API never receives the raw bytes. If your agent works with files inside a session, prefer the session file mount, where the sandbox exposes uploaded files to running code. See the [remote sandbox](/docs/sandbox/remote) for the sandbox helpers (`upload_local_file`, `smart_file_extract`) that build on this storage. These endpoints use your project API key in the `x-api-key` header. # Endpoints --- # Logs (/reference/api-reference/logs) {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/logs.mdx, not this file. */} The Logs API returns **individual tool execution events**, one record per tool call. Use it to debug failures, inspect request/response payloads, and trace specific user activity. For aggregated counts (how many tool calls happened), use the [Usage API](/reference/api-reference/organization) instead. All endpoints in this section require a **project API key** (`x-api-key`) or a valid session cookie. # List logs ```bash curl -X POST https://backend.composio.dev/api/v3.1/logs/tool_execution \ -H "x-api-key: YOUR_PROJECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "limit": 20, "time_range": { "from": 1744848000000, "to": 1744934400000 }, "filters": [ { "field": "toolkit_slug", "operator": "==", "value": "gmail" }, { "field": "status", "operator": "==", "value": "failed" } ] }' ``` The response contains a page of log entries and a `next_cursor`: ```json { "logs": [ { "id": "log_-jRTWClpBoVo", "timestamp": "2026-04-17T10:25:00.000Z", "type": "tool.execution", "status": "failed", "level": "error", "message": "GMAIL_SEND_EMAIL failed: invalid recipient", "metadata": { /* tool, toolkit, user_id, connected_account_id, ... */ }, "metrics": { "duration_ms": 202 }, "parent": null } ], "next_cursor": "eyJwYWdlIjoyfQ==" } ``` Pass `next_cursor` back as `cursor` on the next request to paginate. When `next_cursor` is `null`, you've reached the end. ## Filter fields Pass one or more filters in the `filters` array. Filters are **AND**-combined. | Field | What it matches | | ---------------------- | ----------------------------------------------------------- | | `tool_slug` | The specific tool that was called (e.g. `GMAIL_SEND_EMAIL`) | | `toolkit_slug` | The toolkit (e.g. `gmail`, `slack`, `github`) | | `connected_account_id` | The connected account used for the call | | `auth_config_id` | The auth config (integration) behind the connected account | | `status` | `success` or `failed` | | `user_id` | Entity that initiated the call | | `session_id` | Tool router session, if routed through a session | | `sandbox_id` | Sandbox the call ran in, if applicable | | `request_id` | Request ID (useful for correlating with your own logs) | | `log_id` | Exact log ID (equivalent to the detail endpoint) | ## Operators | Operator | Meaning | | -------------- | ------------------------ | | `==` | Exact match | | `!=` | Not equal | | `contains` | Substring match | | `not_contains` | Substring does not match | ## Parameters | Field | Type | Default | Notes | | ----------------- | -------------- | ------- | ---------------------------------------------- | | `limit` | number | `20` | Max 100 | | `cursor` | string \| null | `null` | Opaque pagination token from previous response | | `filters` | array | `[]` | AND-combined | | `time_range.from` | number | — | Epoch milliseconds | | `time_range.to` | number | — | Epoch milliseconds | # Get a single log Fetch one log by ID to get the **full** payload, including request/response bodies, timing breakdowns, and source metadata: ```bash curl https://backend.composio.dev/api/v3.1/logs/tool_execution/log_-jRTWClpBoVo \ -H "x-api-key: YOUR_PROJECT_API_KEY" ``` The detail response includes everything from the list shape plus: * `timings`: `start_time` and `end_time` in epoch ms * `context`: `session_id`, `trace_id`, `request_id` * `source`: `host` (e.g. `mcp`, `sdk`, `api`), `framework`, `language` * `data`: the full request payload and response body This is the endpoint to call when you need to reconstruct *exactly* what happened, for example when debugging a 500 from a user report. # Recipes ## Find failed Gmail tool calls in the last hour ```bash NOW=$(date +%s)000 HOUR_AGO=$(( $(date +%s) - 3600 ))000 curl -X POST https://backend.composio.dev/api/v3.1/logs/tool_execution \ -H "x-api-key: YOUR_PROJECT_API_KEY" \ -H "Content-Type: application/json" \ -d "{ \"time_range\": { \"from\": ${HOUR_AGO}, \"to\": ${NOW} }, \"filters\": [ { \"field\": \"toolkit_slug\", \"operator\": \"==\", \"value\": \"gmail\" }, { \"field\": \"status\", \"operator\": \"==\", \"value\": \"failed\" } ] }" ``` ## Get failures for a specific user ```bash curl -X POST https://backend.composio.dev/api/v3.1/logs/tool_execution \ -H "x-api-key: YOUR_PROJECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "filters": [ { "field": "user_id", "operator": "==", "value": "user_abc123" }, { "field": "status", "operator": "==", "value": "failed" } ] }' ``` ## Fetch a single log's full request/response ```bash curl https://backend.composio.dev/api/v3.1/logs/tool_execution/log_-jRTWClpBoVo \ -H "x-api-key: YOUR_PROJECT_API_KEY" ``` # Endpoints --- # MCP (/reference/api-reference/mcp) {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/mcp.mdx, not this file. */} > This API is deprecated. Use a session's MCP endpoint instead. Create a session with `composio.create(userId, { mcp: true })`, then read the hosted URL off `session.mcp.url`. See [Using sessions via MCP](/docs/sessions-via-mcp) and [migrating MCP servers to sessions](/docs/migration-guide/mcp-servers-to-sessions). The MCP API is the standalone, hosted [Model Context Protocol](https://modelcontextprotocol.io) server-management surface. It let you stand up and manage a separate server config per toolkit, then mint a per-user MCP URL that any MCP-compatible client could connect to. These endpoints create, list, update, and delete MCP servers, including custom servers spanning multiple apps, generate per-user MCP URLs, and manage per-user server instances and their connected accounts. Sessions replace this. A single `composio.create(...)` gives you the same MCP URL pattern, keyed by `user_id`, while handling tool discovery, authentication, context, and versioning for you. Your existing tools, auth configs (`ac_…`), and connected accounts carry over with no re-authentication. To pin a session to a fixed tool list the way a server did, use the direct-tools preset described in [Configuring sessions](/docs/configuring-sessions). # Endpoints --- # Organization (/reference/api-reference/organization) {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/organization.mdx, not this file. */} The Usage API returns **aggregated counts** of tool calls and sessions. Use it to power billing dashboards, customer-facing analytics, or internal utilization reports. For individual events, use the [Logs API](/reference/api-reference/logs). There are two query shapes: * **Summary**: totals across one or more entity types in a time window. * **Breakdown**: one entity type, grouped by a dimension (tool, user, session, etc.). Each shape comes in an org-scoped and a project-scoped flavor. The org-scoped endpoints are documented below; the project-scoped usage endpoints (`POST /api/v3.1/project/usage/*`) also appear on the [Projects](/reference/api-reference/projects) reference page. # Authentication | Endpoint | Header | Scope | | -------------------------------------------- | ------------------------------ | ------------------------ | | `POST /api/v3.1/org/usage/summary` | `x-org-api-key` *(or org JWT)* | All projects in your org | | `POST /api/v3.1/org/usage/{entity_type}` | `x-org-api-key` *(or org JWT)* | All projects in your org | | `POST /api/v3.1/project/usage/summary` | `x-api-key` *(or cookie)* | Single project | | `POST /api/v3.1/project/usage/{entity_type}` | `x-api-key` *(or cookie)* | Single project | The org endpoints accept a `project_id` filter so you can slice by project without rotating keys. # Entity types | Entity type | What it counts | | ------------ | ------------------------------------------- | | `tool_calls` | Every tool execution (successful or failed) | | `sessions` | Sessions created | # Summary Totals across entity types for a time window. ```bash curl -X POST https://backend.composio.dev/api/v3.1/project/usage/summary \ -H "x-api-key: YOUR_PROJECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": 1744848000000, "to": 1744934400000, "entity_types": ["tool_calls", "sessions"] }' ``` Response: ```json { "entities": { "tool_calls": { "unit": "count", "total_quantity": "142", "event_count": 142 }, "sessions": { "unit": "count", "total_quantity": "8", "event_count": 8 } } } ``` ## Summary parameters | Field | Type | Default | Notes | | -------------------- | ------------------- | ----------- | -------------------------------------------------------------- | | `from` | number | 30 days ago | Epoch milliseconds | | `to` | number | now | Epoch milliseconds | | `entity_types` | string\[] | all | Subset of `tool_calls`, `sessions` | | `filters.user_id` | string \| string\[] | — | Filter events by initiating user | | `filters.session_id` | string \| string\[] | — | Filter events by session | | `filters.project_id` | string \| string\[] | — | Only meaningful on org endpoints; ignored on project endpoints | # Breakdown One entity type, grouped by a dimension. Useful for answering "top N" questions. ```bash curl -X POST https://backend.composio.dev/api/v3.1/project/usage/tool_calls \ -H "x-api-key: YOUR_PROJECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": 1744848000000, "to": 1744934400000, "group_by": "toolkit_slug", "order_by": "total_quantity", "order_direction": "desc", "limit": 10 }' ``` Response: ```json { "entity_type": "tool_calls", "unit": "count", "total_quantity": "142", "event_count": 142, "groups": [ { "key": "github", "total_quantity": "80", "event_count": 80 }, { "key": "slack", "total_quantity": "62", "event_count": 62 } ] } ``` ## Breakdown `group_by` options | Entity | Org scope | Project scope | Default | | ------------ | ------------------------------------------------------------------------------------------ | ----------------------- | ----------- | | `tool_calls` | `tool_slug`, `toolkit_slug`, `connected_account_id`, `user_id`, `session_id`, `project_id` | same minus `project_id` | `tool_slug` | | `sessions` | `user_id`, `project_id` | `user_id` | `user_id` | ## Breakdown parameters | Field | Type | Default | Notes | | ----------------- | ------------------- | ---------------- | --------------------------------------------- | | `from` | number | 30 days ago | Epoch ms | | `to` | number | now | Epoch ms | | `group_by` | string | see table | Dimension to group by | | `order_by` | string | `total_quantity` | One of `key`, `total_quantity`, `event_count` | | `order_direction` | `"asc"` \| `"desc"` | `"desc"` | | | `limit` | number | 50 | Max groups returned | | `filters` | object | — | See Filters below | # Filters Filters live in a `filters` object on the request body. Each filter value can be a **single string** or an **array of strings**: * Within a single field, values are OR-combined (`user_id: ["a", "b"]` matches events for user *a or b*). * Across fields, filters are AND-combined. ```json { "filters": { "user_id": ["user_123", "user_456"], "session_id": "sess_abc" } } ``` The `project_id` filter is only meaningful on the org-scoped endpoints. Project endpoints accept the field but ignore it (your key already pins the scope to a single project). # Time ranges * `from` and `to` are **epoch milliseconds**. * `from` defaults to 30 days before `to`. * `to` defaults to the current time. * Maximum range: **366 days**. Longer ranges return a 400. # Recipes ## Top 10 tools my org called last week ```bash WEEK_AGO=$(( $(date +%s) - 604800 ))000 NOW=$(date +%s)000 curl -X POST https://backend.composio.dev/api/v3.1/org/usage/tool_calls \ -H "x-org-api-key: YOUR_ORG_API_KEY" \ -H "Content-Type: application/json" \ -d "{ \"from\": ${WEEK_AGO}, \"to\": ${NOW}, \"group_by\": \"tool_slug\", \"limit\": 10 }" ``` ## Tool call count per user for my project this month ```bash MONTH_AGO=$(( $(date +%s) - 2592000 ))000 NOW=$(date +%s)000 curl -X POST https://backend.composio.dev/api/v3.1/project/usage/tool_calls \ -H "x-api-key: YOUR_PROJECT_API_KEY" \ -H "Content-Type: application/json" \ -d "{ \"from\": ${MONTH_AGO}, \"to\": ${NOW}, \"group_by\": \"user_id\", \"limit\": 50 }" ``` ## Which toolkits is a specific user using? ```bash curl -X POST https://backend.composio.dev/api/v3.1/project/usage/tool_calls \ -H "x-api-key: YOUR_PROJECT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "group_by": "toolkit_slug", "filters": { "user_id": "user_abc123" } }' ``` # Endpoints --- # Organization Management (/reference/api-reference/organization-management) {/* Auto-generated from OpenAPI spec. Do not edit directly. */} Organization Management API endpoints # Endpoints --- # Projects (/reference/api-reference/projects) {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/projects.mdx, not this file. */} Projects are Composio's multi-tenancy primitive. Every Composio account belongs to an **organization**. Inside an organization, **projects** are isolated environments that scope your API keys, connected accounts, auth configs, and webhook configurations. Resources in one project are not accessible from another. ```mermaid graph LR ORG["Organization (org_xxx)"] --- P1["Project: Production (proj_xxx)"] ORG --- P2["Project: Staging (proj_xxx)"] ORG --- TM["Team Members"] P1 --- A1["API Keys"] P1 --- A2["Connected Accounts"] P1 --- A3["Auth Configs"] P1 --- A4["Webhook Config"] P2 --- B1["..."] ``` Common reasons to use multiple projects: * **Separate environments**: keep production and staging isolated * **Separate products**: keep resources for different apps independent * **Client isolation**: give each client their own project with separate credentials and data # Managing projects Manage projects from the [dashboard](https://dashboard.composio.dev/~/org/) or via the API using an **organization API key** (`x-org-api-key`). > Project management endpoints use the `x-org-api-key` header, not the regular `x-api-key`. Find your org API key in the dashboard under **Settings > Organization**. There is no limit on the number of projects per organization. Project names must be unique within the organization. Create a project with `should_create_api_key: true` to get an API key back in the response: ```bash curl -X POST https://backend.composio.dev/api/v3.1/org/owner/project/new \ -H "x-org-api-key: YOUR_ORG_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "my-staging-project", "should_create_api_key": true }' ``` ```json { "id": "proj_abc123xyz456", "name": "my-staging-project", "api_key": "ak_abc123xyz456" } ``` The list endpoint supports pagination with `limit` and `cursor`; getting a project by ID returns the full project object including its API keys. # Project settings Each project has settings that control security, logging, and display behavior. The project detail endpoints return current configuration for inspection. Use **Settings > Project Settings** in the [dashboard](https://dashboard.composio.dev/~/project/settings/general) to update project settings. Notable security setting: `require_mcp_api_key`, when `true`, requires MCP server requests to include a valid `x-api-key` header. This defaults to `true` for organizations created on or after March 5, 2026. # Endpoints --- # Sessions (prev Tool Router) (/reference/api-reference/tool-router) {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/tool-router.mdx, not this file. */} These are Composio's session endpoints. A **session** is the runtime context your agent uses to work for one of your users: it scopes which user's connected accounts are in play, which tools are available, how authentication happens, and where execution state lives. Read [What is a session?](/docs/how-composio-works) for the full concept. > Sessions were formerly called the "tool router", which is why these endpoints live under `tool_router`. They are the same thing. In the SDK you do not call these endpoints directly. Use `composio.create(...)` to start a session and `composio.use(...)` to resume one, then call `session.tools()`, `session.execute(...)`, and `session.authorize(...)` on the returned object. Reach for the raw API when you need lower-level control: creating and patching a session config, attaching to an existing session, searching for tools, executing tools and meta tools, opening link sessions for auth, proxying authenticated requests, and reading or writing files in a session mount. See [Configuring sessions](/docs/configuring-sessions) for toolkits, auth configs, account selection, and presets. # Endpoints --- # Toolkits (/reference/api-reference/toolkits) {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/toolkits.mdx, not this file. */} A toolkit is a collection of related tools for a single app, like `gmail`, `github`, or `slack`. Each toolkit groups the actions for that service, its authentication requirements, and the trigger types it exposes. Reach for these endpoints when you want to: * List the toolkits in the catalog, sorted by popularity, to browse what is available before configuring a session. * Fetch a single toolkit by `slug` for its name, logo, categories, and metadata. * Fetch several toolkits at once with the multi endpoint. * List the available toolkit categories to filter the catalog by use case. * Read the toolkits changelog to track when tools or schemas change. These endpoints authenticate with your project API key in the `x-api-key` header. > Tools within a toolkit are versioned. When you execute a tool, resolve to a known version with `toolkit_versions=latest` or a pinned dated version. See the [toolkit versioning migration guide](/docs/migration-guide/toolkit-versioning). To browse toolkits visually, see the [toolkits catalog](/toolkits). For the concepts and SDK usage, see [Tools and toolkits](/docs/how-composio-works) and [Configuring sessions](/docs/configuring-sessions). # Endpoints --- # Tools (/reference/api-reference/tools) {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/tools.mdx, not this file. */} Tools are the individual executable actions inside a toolkit, like `GMAIL_SEND_EMAIL` or `GITHUB_CREATE_ISSUE`. Each tool has an input schema describing its parameters and an output schema describing what it returns. Tool slugs are SCREAMING\_SNAKE\_CASE and follow a `{TOOLKIT}_{ACTION}` pattern. Reach for these endpoints when you want to: * List or search the catalog of available tools, optionally scoped to one or more toolkits. * Fetch a single tool's input and output schema by `tool_slug` before constructing a call. * Execute a tool on behalf of a user's connected account, or generate the inputs for an execution from natural language. * Look up the OAuth scopes a set of tools requires, or send an authenticated proxy request to an app's underlying API. These endpoints authenticate with your project API key in the `x-api-key` header. > Manual tool execution requires an explicit toolkit version. Pass `toolkit_versions=latest` (or pin a dated version like `20251027_00`) so calls resolve to a known tool definition. See the [toolkit versioning migration guide](/docs/migration-guide/toolkit-versioning). For the concepts behind tools, schemas, and authentication, see [Tools and toolkits](/docs/how-composio-works). # Proxy execute Proxy execute sends an authenticated HTTP request through a toolkit's [connected account](/docs/auth-configuration/connected-accounts) without a predefined tool, and Composio injects the OAuth token, API key, or other credentials on the server side so your code never touches raw secrets. Reach for it when you need an endpoint that Composio's predefined tools do not cover, when you need a request shape (custom query parameters, field masks, or advanced filters) that a predefined tool cannot express, or when a terminal agent would otherwise hardcode a bearer token in a `curl` call. Call it with `composio.tools.proxyExecute()` in the TypeScript SDK, `composio.tools.proxy()` in the Python SDK, or `POST /api/v3.1/tools/execute/proxy` over HTTP. The `endpoint` can be an absolute URL (`https://api.example.com/v1/resource`) or a path relative to the toolkit's base URL (`/v1/resource`), `method` is the HTTP verb, `connectedAccountId` selects the account to authenticate as, `body` carries the JSON payload, and `parameters` adds extra headers and query parameters. The response forwards the upstream `status`, `headers`, and parsed `data` verbatim. ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const { status, data } = await composio.tools.proxyExecute({ endpoint: '/repos/composiohq/composio/issues/1', method: 'GET', connectedAccountId: 'ca_github_user_123', parameters: [{ name: 'Accept', value: 'application/vnd.github.v3+json', in: 'header' }], }); console.log(status, data); ``` > Proxy execute rejects cross-domain requests, so the `endpoint` must resolve to the same domain as the connected account's toolkit, and you should not set the `Authorization` header yourself because Composio injects the correct credential for the account's auth scheme. This is an intentional security boundary, not a quota, so it cannot be bypassed by reshaping the request. Proxy execute is a form of [direct tool execution](/docs/sessions-vs-direct-execution): it bypasses session state, tool schemas, and modifiers. If you are building an agent, prefer [sessions](/docs/configuring-sessions), and use the proxy only for the specific API call that is not available as a tool. The full request and response schema lives in the [`POST /api/v3.1/tools/execute/proxy`](/reference/api-reference/tools/postToolsExecuteProxy) reference. # Endpoints --- # Triggers (/reference/api-reference/triggers) {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/triggers.mdx, not this file. */} Triggers let you subscribe to events from a user's connected app, such as a new Gmail message, a GitHub commit, or a Slack message, and receive the event data as a structured payload at your webhook endpoint. There are two layers to understand: * A **trigger type** is a template that defines what event to listen for and what configuration it needs. For example, `GITHUB_COMMIT_EVENT` requires an `owner` and a `repo`. Each toolkit exposes its own trigger types. * A **trigger instance** is a trigger type scoped to a specific user and connected account. Creating one produces an instance with its own `ti_*` ID that you can enable, disable, or delete independently. Reach for these endpoints when you want to: * Discover the trigger types a toolkit offers, or fetch one type by `slug` to inspect its config and payload schema. * Create or update a trigger instance for a connected account with the upsert endpoint. * List active trigger instances, or enable, disable, and delete an instance by `triggerId`. These endpoints authenticate with your project API key in the `x-api-key` header. > Creating a trigger instance only registers it. To actually receive events, set a webhook URL for your project once, then route incoming events on `metadata.trigger_slug`. See [Subscribing to events](/docs/setting-up-triggers/subscribing-to-events). For the full concept overview, see [Triggers](/docs/triggers). # Endpoints --- # Webhook Endpoints (/reference/api-reference/webhook-endpoints) {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/webhook-endpoints.mdx, not this file. */} Webhook endpoints are per-OAuth-app webhook ingress configurations. They define the inbound URL a provider posts events to, along with the signing secret Composio stores and uses to verify those incoming payloads. Reach for these endpoints when an OAuth app you have configured needs to deliver provider-side events into Composio. You create an endpoint, configure or update it by its `nano_id`, and store the signing secret Composio uses to authenticate inbound requests. Each endpoint is addressed by its `nano_id`. The `POST` to `/webhook_endpoints/{nano_id}` replaces the full configuration, while `PATCH` updates it in place. This is distinct from [webhook subscriptions](/reference/api-reference/webhook-subscriptions), which control where Composio delivers outbound trigger events. To verify the signature on payloads Composio sends you, see [Verifying signatures](/docs/setting-up-triggers/subscribing-to-events#verifying-signatures). To set up the trigger events those payloads carry, see [Triggers](/docs/triggers). # Endpoints --- # Webhook Subscriptions (/reference/api-reference/webhook-subscriptions) {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/webhook-subscriptions.mdx, not this file. */} Webhook subscriptions are outbound delivery configurations. They define the URL Composio posts [trigger](/docs/triggers) events to, along with the signing secret and the set of event types you want to receive. Reach for these endpoints to register where Composio should send events, to filter delivery to specific event types, and to manage the signing secret used to verify those deliveries. List the available event types with the `/webhook_subscriptions/event_types` endpoint, then create a subscription scoped to the ones you care about. Each subscription is addressed by its `id`. You can update its URL and filters with `PATCH`, delete it, and rotate its signing secret with `/webhook_subscriptions/{id}/rotate_secret` if the secret leaks. Every webhook request Composio sends includes `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers. Store the secret as `COMPOSIO_WEBHOOK_SECRET` and verify each payload before trusting it. See [Verifying signatures](/docs/setting-up-triggers/subscribing-to-events#verifying-signatures) for the SDK and manual verification flows. # Event types A subscription's `enabled_events` controls which events get delivered to its URL. Two broad families exist: * **Trigger events** like `composio.trigger.message` — payloads emitted by [triggers](/docs/triggers) you've enabled (new email, new issue, etc.). * **Lifecycle events** like `composio.connected_account.expired` — emitted when a [connected account](/docs/auth-configuration/connected-accounts) changes state. List everything you can subscribe to with the `/webhook_subscriptions/event_types` endpoint, then scope a subscription to the events you handle. # Detecting connection expiry Composio automatically refreshes OAuth tokens before they expire. But when a refresh token is revoked or expires, the connection enters an `EXPIRED` state and the user must re-authenticate. Subscribe to the `composio.connected_account.expired` event to detect this proactively, instead of waiting for a tool execution to fail. > This event is only available with [V3 webhook payloads](/docs/setting-up-triggers/subscribing-to-events#webhook-payload-versions). New organizations use V3 by default. Add `composio.connected_account.expired` to the subscription's `enabled_events`: ```bash curl -X POST https://backend.composio.dev/api/v3.1/webhook_subscriptions \ -H "X-API-KEY: " \ -H "Content-Type: application/json" \ -d '{ "webhook_url": "https://example.com/webhook", "enabled_events": [ "composio.trigger.message", "composio.connected_account.expired" ] }' ``` When a connection expires, Composio sends a webhook with the connected account details: ```json { "id": "evt_847cdfcd-d219-4f18-a6dd-91acd42ca94a", "type": "composio.connected_account.expired", "metadata": { "project_id": "pr_your-project-id", "org_id": "ok_your-org-id" }, "data": { "id": "ca_your-connected-account-id", "toolkit": { "slug": "gmail" }, "auth_config": { "id": "ac_your-auth-config-id", "auth_scheme": "OAUTH2" }, "status": "EXPIRED", "status_reason": "OAuth refresh token expired" }, "timestamp": "2026-02-06T12:00:00.000Z" } ``` Route on `type` to handle expiry alongside trigger events: **Python:** ```python from composio import Composio, WebhookEventType composio = Composio() @app.post("/webhook") async def webhook_handler(request: Request): payload = await request.json() event_type = payload.get("type") if event_type == WebhookEventType.CONNECTION_EXPIRED: account_id = payload["data"]["id"] toolkit = payload["data"]["toolkit"]["slug"] # Look up the user and send them a re-auth link session = composio.create(user_id=lookup_user(account_id)) connection_request = session.authorize(toolkit) notify_user(connection_request.redirect_url) elif event_type == WebhookEventType.TRIGGER_MESSAGE: # Handle trigger events pass return {"status": "ok"} ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); type NextApiRequest = { body: any }; type NextApiResponse = { status: (code: number) => { json: (data: any) => void } }; declare function lookupUser(accountId: string): string; declare function notifyUser(url: string): void; export default async function webhookHandler(req: NextApiRequest, res: NextApiResponse) { const payload = req.body; if (payload.type === 'composio.connected_account.expired') { const accountId = payload.data.id; const toolkit = payload.data.toolkit.slug; // Look up the user and send them a re-auth link const session = await composio.create(lookupUser(accountId)); const connectionRequest = await session.authorize(toolkit); if (connectionRequest.redirectUrl) { notifyUser(connectionRequest.redirectUrl); } } else if (payload.type === 'composio.trigger.message') { // Handle trigger events } res.status(200).json({ status: 'ok' }); } ``` > Always [verify webhook signatures](/docs/setting-up-triggers/subscribing-to-events#verifying-signatures) before processing events in production. # Endpoints --- # AuthConfigs (/reference/sdk-reference/python/auth-configs) # Methods ## list() Lists authentication configurations based on provided filter criteria. ```python def list(query: auth_config_list_params.AuthConfigListParams = ...) -> auth_config_list_response.AuthConfigListResponse ``` **Parameters** | Name | Type | | -------- | ---------------------------------------------- | | `query?` | `auth_config_list_params.AuthConfigListParams` | **Returns** `auth_config_list_response.AuthConfigListResponse` *** ## create() Create a new auth config ```python def create(toolkit: str, options: auth_config_create_params.AuthConfig) -> auth_config_create_response.AuthConfig ``` **Parameters** | Name | Type | | --------- | -------------------------------------- | | `toolkit` | `str` | | `options` | `auth_config_create_params.AuthConfig` | **Returns** `auth_config_create_response.AuthConfig` — The created auth config. *** ## get() Retrieves a specific authentication configuration by its ID ```python def get(nanoid: str) -> auth_config_retrieve_response.AuthConfigRetrieveResponse ``` **Parameters** | Name | Type | | -------- | ----- | | `nanoid` | `str` | **Returns** `auth_config_retrieve_response.AuthConfigRetrieveResponse` — The retrieved auth config. *** ## update() Updates an existing authentication configuration. This method allows you to modify properties of an auth config such as credentials, scopes, or tool restrictions. The update type (custom or default) determines which fields can be updated. ```python def update(nanoid: str, options: auth_config_update_params.AuthConfigUpdateParams) -> Dict ``` **Parameters** | Name | Type | | --------- | -------------------------------------------------- | | `nanoid` | `str` | | `options` | `auth_config_update_params.AuthConfigUpdateParams` | **Returns** `Dict` — The updated auth config. *** ## delete() Deletes an existing authentication configuration. ```python def delete(nanoid: str) -> Dict ``` **Parameters** | Name | Type | | -------- | ----- | | `nanoid` | `str` | **Returns** `Dict` — The deleted auth config. *** ## enable() Enables an existing authentication configuration. ```python def enable(nanoid: str) -> Dict ``` **Parameters** | Name | Type | | -------- | ----- | | `nanoid` | `str` | **Returns** `Dict` — The enabled auth config. *** ## disable() Disables an existing authentication configuration. ```python def disable(nanoid: str) -> Dict ``` **Parameters** | Name | Type | | -------- | ----- | | `nanoid` | `str` | **Returns** `Dict` — The disabled auth config. *** [View source](https://github.com/composiohq/composio/blob/next/python/composio/core/models/auth_configs.py#L18) --- # Composio (/reference/sdk-reference/python/composio) # Properties | Name | Type | | -------------------------------------------------------------------------- | ------------------- | | [`tools`](/reference/sdk-reference/python/tools) | `Tools` | | [`toolkits`](/reference/sdk-reference/python/toolkits) | `Toolkits` | | [`triggers`](/reference/sdk-reference/python/triggers) | `Triggers` | | [`auth_configs`](/reference/sdk-reference/python/auth-configs) | `AuthConfigs` | | [`connected_accounts`](/reference/sdk-reference/python/connected-accounts) | `ConnectedAccounts` | | [`mcp`](/reference/sdk-reference/python/mcp) | `MCP` | [View source](https://github.com/composiohq/composio/blob/next/python/composio/sdk.py#L49) --- # ConnectedAccounts (/reference/sdk-reference/python/connected-accounts) # Methods ## update() Update a connected account's alias and/or credentials. ```python def update(nanoid: str, alias: str | None = ..., connection: connected_account_patch_params.Connection | None = ...) -> connected_account_patch_response.ConnectedAccountPatchRes... ``` **Parameters** | Name | Type | | ------------- | --------------------------------------------------- | | `nanoid` | `str` | | `alias?` | `str \| None` | | `connection?` | `connected_account_patch_params.Connection \| None` | **Returns** `connected_account_patch_response.ConnectedAccountPatchRes...` — Response with `id`, `status`, and `success`. **Example** ```python # Set an alias composio.connected_accounts.update('ca_abc123', alias='work-gmail') # Clear an alias composio.connected_accounts.update('ca_abc123', alias='') ``` *** ## update\_acl() Update the per-user ACL on a SHARED connected account. Experimental — shape may change in future releases. Only valid on SHARED connections; raises `ComposioAclOnlyForSharedError` on a PRIVATE connection. Omit a parameter to leave it unchanged; pass an empty list to clear an allow/deny list. At least one parameter must be provided. ```python def update_acl(nanoid: str, allow_all_users: bool | None = ..., allowed_user_ids: List[str | None] = ..., not_allowed_user_ids: List[str | None] = ...) -> connected_account_patch_response.ConnectedAccountPatchRes... ``` **Parameters** | Name | Type | | ----------------------- | ------------------- | | `nanoid` | `str` | | `allow_all_users?` | `bool \| None` | | `allowed_user_ids?` | `List[str \| None]` | | `not_allowed_user_ids?` | `List[str \| None]` | **Returns** `connected_account_patch_response.ConnectedAccountPatchRes...` — Response with `id`, `status`, and `success`. **Example** ```python composio.connected_accounts.update_acl( 'ca_abc', allow_all_users=True, not_allowed_user_ids=['user_bob'], ) ``` *** ## initiate() Compound function to create a new connected account. This function creates a new connected account and returns a connection request. Users can then wait for the connection to be established using the `wait_for_connection` method. ```python def initiate(user_id: str, auth_config_id: str, callback_url: str | None = ..., allow_multiple: bool = ..., config: connected_account_create_params.ConnectionState | None = ..., alias: str | None = ...) -> ConnectionRequest ``` **Parameters** | Name | Type | | ----------------- | --------------------------------------------------------- | | `user_id` | `str` | | `auth_config_id` | `str` | | `callback_url?` | `str \| None` | | `allow_multiple?` | `bool` | | `config?` | `connected_account_create_params.ConnectionState \| None` | | `alias?` | `str \| None` | **Returns** `ConnectionRequest` — The connection request. *** ## link() Create a Composio Connect Link for a user to connect their account to a given auth config. This method will return an external link which you can use for the user to connect their account. ```python def link(user_id: str, auth_config_id: str, callback_url: str | None = ..., alias: str | None = ..., allow_multiple: bool = ..., experimental: link_create_params.Experimental | None = ...) -> ConnectionRequest ``` **Parameters** | Name | Type | | ----------------- | ----------------------------------------- | | `user_id` | `str` | | `auth_config_id` | `str` | | `callback_url?` | `str \| None` | | `alias?` | `str \| None` | | `allow_multiple?` | `bool` | | `experimental?` | `link_create_params.Experimental \| None` | **Returns** `ConnectionRequest` — Connection request object. **Example** ```python # Create a connection request and redirect the user to the redirect url connection_request = composio.connected_accounts.link('user_123', 'auth_config_123') redirect_url = connection_request.redirect_url print(f"Visit: {redirect_url} to authenticate your account") # Wait for the connection to be established connected_account = connection_request.wait_for_connection() # Create a connection request with callback URL connection_request = composio.connected_accounts.link( 'user_123', 'auth_config_123', callback_url='https://your-app.com/callback' ) redirect_url = connection_request.redirect_url print(f"Visit: {redirect_url} to authenticate your account") # Wait for the connection to be established connected_account = composio.connected_accounts.wait_for_connection(connection_request.id) connection_request = composio.connected_accounts.link( 'user_creator', 'auth_config_123', experimental={ 'account_type': 'SHARED', 'acl_config_for_shared': { 'allow_all_users': True, 'not_allowed_user_ids': ['user_bob'], }, }, ) ``` *** ## wait\_for\_connection() Wait for connected account with given ID to be active ```python def wait_for_connection(id: str, timeout: float | None = ...) -> connected_account_retrieve_response.ConnectedAccountRetri... ``` **Parameters** | Name | Type | | ---------- | --------------- | | `id` | `str` | | `timeout?` | `float \| None` | **Returns** `connected_account_retrieve_response.ConnectedAccountRetri...` *** [View source](https://github.com/composiohq/composio/blob/next/python/composio/core/models/connected_accounts.py#L340) --- # Python SDK Reference (/reference/sdk-reference/python) # Python SDK Reference Complete API reference for the `composio` Python package. # Installation # Classes | Class | Description | | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | | [`Composio`](/reference/sdk-reference/python/composio) | Composio SDK for Python. Generic parameters: TTool: The individual tool type re... | | [`Tools`](/reference/sdk-reference/python/tools) | Tools class definition This class is used to manage tools in the Composio SDK. ... | | [`Toolkits`](/reference/sdk-reference/python/toolkits) | Toolkits are a collectiono of tools that can be used to perform various tasks. T... | | [`Triggers`](/reference/sdk-reference/python/triggers) | Triggers (instance) class | | [`ConnectedAccounts`](/reference/sdk-reference/python/connected-accounts) | Manage connected accounts. This class is used to manage connected accounts in t... | | [`AuthConfigs`](/reference/sdk-reference/python/auth-configs) | Manage authentication configurations. | | [`MCP`](/reference/sdk-reference/python/mcp) | MCP (Model Control Protocol) class. Provides enhanced MCP server operations Thi... | | [`Session`](/reference/sdk-reference/python/session) | A Composio session — the object returned by `composio.create(...)` / \`\`composi... | # Quick Start ```python from composio import Composio composio = Composio(api_key="your-api-key") # Get tools for a user tools = composio.tools.get("user-123", toolkits=["github"]) # Execute a tool result = composio.tools.execute( "GITHUB_GET_REPOS", arguments={"owner": "composio"}, user_id="user-123" ) ``` # Decorators ## before\_execute [View source](https://github.com/composiohq/composio/blob/next/python/composio/core/models/_modifiers.py#L280) ```python @before_execute(modifier: BeforeExecute | None = ..., tools: List[str | None] = ..., toolkits: List[str | None] = ...) def my_modifier(...): ... ``` ## after\_execute [View source](https://github.com/composiohq/composio/blob/next/python/composio/core/models/_modifiers.py#L241) ```python @after_execute(modifier: AfterExecute | None = ..., tools: List[str | None] = ..., toolkits: List[str | None] = ...) def my_modifier(...): ... ``` ## before\_file\_upload Build a `Modifier` for the file-upload hook (same scoping pattern as :func:`before_execute`). Your callable may take **either**: - a single `context` argument (:class:`BeforeFileUploadContext`) — the preferred form, exposes `context["source"]` (`"path"` or `"url"`), or - three positional arguments `(path, tool, toolkit)` — legacy form, kept for back-compat. Return a new path/URL string to substitute, or `False` to abort the upload (raises :class:`~composio.exceptions.FileUploadAbortedError`). Pass the returned `Modifier` in `modifiers=[...]` on :meth:`composio.core.models.tools.Tools.execute` or `tools.get`. Multiple such modifiers are composed in list order. [View source](https://github.com/composiohq/composio/blob/next/python/composio/core/models/_modifiers.py#L319) ```python @before_file_upload(modifier: BeforeFileUploadLike | None = ..., tools: List[str | None] = ..., toolkits: List[str | None] = ...) def my_modifier(...): ... ``` ## schema\_modifier [View source](https://github.com/composiohq/composio/blob/next/python/composio/core/models/_modifiers.py#L377) ```python @schema_modifier(modifier: SchemaModifier | None = ..., tools: List[str | None] = ..., toolkits: List[str | None] = ...) def my_modifier(...): ... ``` --- # MCP (/reference/sdk-reference/python/mcp) > **Deprecated**: Use a session's MCP endpoint instead: `composio.create(user_id, mcp=True)` returns a session that exposes `session.mcp.url` / `session.mcp.headers`. MCP is now opt-in per session; this standalone `composio.mcp` server-management API is kept only for backwards compatibility. Prefer the session MCP endpoint; do not generate new code against `composio.mcp`. See [https://docs.composio.dev/docs/sessions-via-mcp](https://docs.composio.dev/docs/sessions-via-mcp) # Methods ## create() Create a new MCP server configuration with specified toolkits and authentication settings. ```python def create(name: str, toolkits: List[Union[ConfigToolkit, str]], manually_manage_connections: bool = ..., allowed_tools: List[str | None] = ...) -> MCPCreateResponse ``` **Parameters** | Name | Type | | ------------------------------ | --------------------------------- | | `name` | `str` | | `toolkits` | `List[Union[ConfigToolkit, str]]` | | `manually_manage_connections?` | `bool` | | `allowed_tools?` | `List[str \| None]` | **Returns** `MCPCreateResponse` — Created server details with generate method **Example** ```python >>> # Using toolkit configuration objects with auth >>> server = composio.experimental.mcp.create( ... 'personal-mcp-server', ... toolkits=[ ... { ... 'toolkit': 'github', ... 'auth_config_id': 'ac_xyz', ... }, ... { ... 'toolkit': 'slack', ... 'auth_config_id': 'ac_abc', ... }, ... ], ... allowed_tools=['GITHUB_CREATE_ISSUE', 'GITHUB_LIST_REPOS', 'SLACK_SEND_MESSAGE'], ... manually_manage_connections=False ... ) >>> >>> # Using simple toolkit names (most common usage) >>> server = composio.experimental.mcp.create( ... 'simple-mcp-server', ... toolkits=['composio_search', 'text_to_pdf'], ... allowed_tools=['COMPOSIO_SEARCH_DUCK_DUCK_GO_SEARCH', 'TEXT_TO_PDF_CONVERT_TEXT_TO_PDF'] ... ) >>> >>> # Using all tools from toolkits (default behavior) >>> server = composio.experimental.mcp.create( ... 'all-tools-server', ... toolkits=['composio_search', 'text_to_pdf'] ... # allowed_tools=None means all tools from these toolkits ... ) >>> >>> # Get server instance for a user >>> mcp = server.generate('user_12345') ``` *** ## list() List MCP servers with optional filtering and pagination. ```python def list(page_no: int | None = ..., limit: int | None = ..., toolkits: str | None = ..., auth_config_ids: str | None = ..., name: str | None = ..., order_by: Literal['created_at', 'updated_at' | None] = ..., order_direction: Literal['asc', 'desc' | None] = ...) -> MCPListResponse ``` **Parameters** | Name | Type | | ------------------ | --------------------------------------------- | | `page_no?` | `int \| None` | | `limit?` | `int \| None` | | `toolkits?` | `str \| None` | | `auth_config_ids?` | `str \| None` | | `name?` | `str \| None` | | `order_by?` | `Literal['created_at', 'updated_at' \| None]` | | `order_direction?` | `Literal['asc', 'desc' \| None]` | **Returns** `MCPListResponse` — Paginated list of MCP servers **Example** ```python >>> # List all servers >>> all_servers = composio.experimental.mcp.list() >>> >>> # List with pagination >>> paged_servers = composio.experimental.mcp.list(page_no=2, limit=5) >>> >>> # Filter by toolkit >>> github_servers = composio.experimental.mcp.list(toolkits='github', name='personal') ``` *** ## get() Retrieve detailed information about a specific MCP server/config. ```python def get(server_id: str) ``` **Parameters** | Name | Type | | ----------- | ----- | | `server_id` | `str` | **Example** ```python >>> server = composio.experimental.mcp.get('mcp_12345') >>> >>> print(server['name']) # "My Personal MCP Server" >>> print(server['allowed_tools']) # ["GITHUB_CREATE_ISSUE", "SLACK_SEND_MESSAGE"] >>> print(server['toolkits']) # ["github", "slack"] >>> print(server['server_instance_count']) # 3 ``` *** ## update() Update an existing MCP server configuration. ```python def update(server_id: str, name: str | None = ..., toolkits: List[Union[ConfigToolkit, str | None]] = ..., manually_manage_connections: bool | None = ..., allowed_tools: List[str | None] = ...) ``` **Parameters** | Name | Type | | ------------------------------ | ----------------------------------------- | | `server_id` | `str` | | `name?` | `str \| None` | | `toolkits?` | `List[Union[ConfigToolkit, str \| None]]` | | `manually_manage_connections?` | `bool \| None` | | `allowed_tools?` | `List[str \| None]` | **Example** ```python >>> # Update server name only >>> updated_server = composio.experimental.mcp.update( ... 'mcp_12345', ... name='My Updated MCP Server' ... ) >>> >>> # Update toolkits and tools >>> server_with_new_tools = composio.experimental.mcp.update( ... 'mcp_12345', ... toolkits=['github', 'slack'], ... allowed_tools=['GITHUB_CREATE_ISSUE', 'SLACK_SEND_MESSAGE'] ... ) >>> >>> # Update with auth configs >>> server_with_auth = composio.experimental.mcp.update( ... 'mcp_12345', ... toolkits=[ ... {'toolkit': 'github', 'auth_config_id': 'auth_abc123'}, ... {'toolkit': 'slack', 'auth_config_id': 'auth_def456'} ... ], ... allowed_tools=['GITHUB_CREATE_ISSUE', 'SLACK_SEND_MESSAGE'], ... manually_manage_connections=False ... ) ``` *** ## delete() Permanently delete an MCP server configuration. ```python def delete(server_id: str) -> Dict[str, Any] ``` **Parameters** | Name | Type | | ----------- | ----- | | `server_id` | `str` | **Returns** `Dict[str, Any]` — Deletion result **Example** ```python >>> # Delete a server >>> result = composio.experimental.mcp.delete('mcp_12345') >>> >>> if result['deleted']: ... print(f"Server {result['id']} has been successfully deleted") >>> else: ... print(f"Failed to delete server {result['id']}") ``` *** ## generate() Get server URLs for an existing MCP server. This matches the TypeScript implementation exactly. ```python def generate(user_id: str, mcp_config_id: str, manually_manage_connections: bool | None = ...) -> MCPServerInstance ``` **Parameters** | Name | Type | | ------------------------------ | -------------- | | `user_id` | `str` | | `mcp_config_id` | `str` | | `manually_manage_connections?` | `bool \| None` | **Returns** `MCPServerInstance` — MCP server instance **Example** ```python >>> mcp = composio.experimental.mcp.generate( ... 'user_12345', ... 'mcp_67890', ... manually_manage_connections=False ... ) >>> >>> print(mcp['url']) # Server URL for the user >>> print(mcp['allowed_tools']) # Available tools ``` *** [View source](https://github.com/composiohq/composio/blob/next/python/composio/core/models/mcp.py#L104) --- # Session (/reference/sdk-reference/python/session) # Properties | Name | Type | | -------------- | --------------------------------- | | `session_id` | `str` | | `experimental` | `'ToolRouterSessionExperimental'` | | `preload` | `Any` | # Methods ## tools() Get provider-wrapped tools for execution with your AI framework. Returns tools configured for this session, wrapped in the format expected by your AI provider (OpenAI, Anthropic, LangChain, etc.). When custom tools are bound to the session, execution of COMPOSIO\_MULTI\_EXECUTE\_TOOL is intercepted: local tools are executed in-process, remote tools are sent to the backend. ```python def tools(modifiers: 'Modifiers' | None = ...) -> TToolCollection ``` **Parameters** | Name | Type | | ------------ | --------------------- | | `modifiers?` | `'Modifiers' \| None` | **Returns** `TToolCollection` *** ## authorize() Authorize a toolkit for the user and get a connection request. Initiates the OAuth flow and returns a ConnectionRequest with redirect URL. ```python def authorize(toolkit: str, callback_url: str | None = ..., alias: str | None = ..., experimental: session_link_params.Experimental | None = ...) -> ConnectionRequest ``` **Parameters** | Name | Type | | --------------- | ------------------------------------------ | | `toolkit` | `str` | | `callback_url?` | `str \| None` | | `alias?` | `str \| None` | | `experimental?` | `session_link_params.Experimental \| None` | **Returns** `ConnectionRequest` *** ## toolkits() Get toolkit connection states for the session. ```python def toolkits(toolkits: List[str | None] = ..., next_cursor: str | None = ..., limit: int | None = ..., is_connected: bool | None = ..., search: str | None = ...) -> ToolkitConnectionsDetails ``` **Parameters** | Name | Type | | --------------- | ------------------- | | `toolkits?` | `List[str \| None]` | | `next_cursor?` | `str \| None` | | `limit?` | `int \| None` | | `is_connected?` | `bool \| None` | | `search?` | `str \| None` | **Returns** `ToolkitConnectionsDetails` *** ## search() Search for tools by semantic use case. Returns relevant tools for the given query with schemas and guidance. ```python def search(query: str, model: str | None = ...) -> SessionSearchResponse ``` **Parameters** | Name | Type | | -------- | ------------- | | `query` | `str` | | `model?` | `str \| None` | **Returns** `SessionSearchResponse` *** ## execute() Execute a tool within the session. For custom tools, accepts the original slug (e.g. "GREP") or the full slug (e.g. "LOCAL\_GREP"). Custom tools are executed in-process; remote tools are sent to the Composio backend. ```python def execute(tool_slug: str, arguments: Dict[str, Any | None] = ..., account: str | None = ...) -> SessionExecuteResponse ``` **Parameters** | Name | Type | | ------------ | ------------------------ | | `tool_slug` | `str` | | `arguments?` | `Dict[str, Any \| None]` | | `account?` | `str \| None` | **Returns** `SessionExecuteResponse` *** ## custom\_tools() List all custom tools registered in this session. Returns tools with their final slugs, schemas, and resolved toolkit. ```python def custom_tools(toolkit: str | None = ...) -> List[RegisteredCustomTool] ``` **Parameters** | Name | Type | | ---------- | ------------- | | `toolkit?` | `str \| None` | **Returns** `List[RegisteredCustomTool]` — Array of registered custom tools *** ## custom\_toolkits() List all custom toolkits registered in this session. Returns toolkits with their tools showing final slugs. ```python def custom_toolkits() -> List[RegisteredCustomToolkit] ``` **Returns** `List[RegisteredCustomToolkit]` *** ## proxy\_execute() Proxy an API call through Composio's auth layer. ```python def proxy_execute(toolkit: str, endpoint: str, method: Literal['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], body: Any = ..., parameters: List[Dict[str, Any | None]] = ...) -> SessionProxyExecuteResponse ``` **Parameters** | Name | Type | | ------------- | -------------------------------------------------- | | `toolkit` | `str` | | `endpoint` | `str` | | `method` | `Literal['GET', 'POST', 'PUT', 'DELETE', 'PATCH']` | | `body?` | `Any` | | `parameters?` | `List[Dict[str, Any \| None]]` | **Returns** `SessionProxyExecuteResponse` — Proxied API response *** ## update() Partially update the session configuration. Only the fields provided will be changed; omitted fields are preserved. Mutates this session's `preload` in-place. Pass `None` for `manage_connections`, `sandbox`/`workbench`, or `multi_account` to clear the stored value. `workbench` is a backwards-compatible alias for `sandbox`. Prefer `sandbox` in new code. All parameters use the same types as the Stainless-generated `client.tool_router.session.patch()` method. ```python def update(toolkits: Union[session_patch_params.Toolkits, 'Omit'] = ..., tools: Union[Dict[str, session_patch_params.Tools], 'Omit'] = ..., tags: Union[session_patch_params.Tags, 'Omit'] = ..., auth_configs: Union[Dict[str, str], 'Omit'] = ..., connected_accounts: Union[Dict[str, SequenceNotStr[str | None]], 'Omit'] = ..., manage_connections: Union[session_patch_params.ManageConnections | None, 'Omit'] = ..., sandbox: Union[session_patch_params.Workbench | None, 'Omit'] = ..., workbench: Union[session_patch_params.Workbench | None, 'Omit'] = ..., multi_account: Union[session_patch_params.MultiAccount | None, 'Omit'] = ..., preload: Union[session_patch_params.Preload, 'Omit'] = ...) -> None ``` **Parameters** | Name | Type | | --------------------- | --------------------------------------------------------------- | | `toolkits?` | `Union[session_patch_params.Toolkits, 'Omit']` | | `tools?` | `Union[Dict[str, session_patch_params.Tools], 'Omit']` | | `tags?` | `Union[session_patch_params.Tags, 'Omit']` | | `auth_configs?` | `Union[Dict[str, str], 'Omit']` | | `connected_accounts?` | `Union[Dict[str, SequenceNotStr[str \| None]], 'Omit']` | | `manage_connections?` | `Union[session_patch_params.ManageConnections \| None, 'Omit']` | | `sandbox?` | `Union[session_patch_params.Workbench \| None, 'Omit']` | | `workbench?` | `Union[session_patch_params.Workbench \| None, 'Omit']` | | `multi_account?` | `Union[session_patch_params.MultiAccount \| None, 'Omit']` | | `preload?` | `Union[session_patch_params.Preload, 'Omit']` | *** ## delete() Delete this session. Deleted sessions immediately stop being retrievable or executable. An already-deleted session surfaces the backend 404. ```python def delete() -> ToolRouterSessionDeleteResponse ``` **Returns** `ToolRouterSessionDeleteResponse` *** [View source](https://github.com/composiohq/composio/blob/next/python/composio/core/models/tool_router_session.py#L85) --- # Toolkits (/reference/sdk-reference/python/toolkits) # Methods ## list() List all toolkits. ```python def list(category: str | None = ..., cursor: str | None = ..., limit: float | None = ..., sort_by: Literal['usage', 'alphabetically' | None] = ..., managed_by: Literal['composio', 'all', 'project' | None] = ...) -> toolkit_list_response.ToolkitListResponse ``` **Parameters** | Name | Type | | ------------- | ----------------------------------------------- | | `category?` | `str \| None` | | `cursor?` | `str \| None` | | `limit?` | `float \| None` | | `sort_by?` | `Literal['usage', 'alphabetically' \| None]` | | `managed_by?` | `Literal['composio', 'all', 'project' \| None]` | **Returns** `toolkit_list_response.ToolkitListResponse` *** ## get() ```python def get(slug: str | None = ..., query: toolkit_list_params.ToolkitListParams | None = ...) -> Union[toolkit_retrieve_response.ToolkitRetrieveResponse, ... ``` **Parameters** | Name | Type | | -------- | ----------------------------------------------- | | `slug?` | `str \| None` | | `query?` | `toolkit_list_params.ToolkitListParams \| None` | **Returns** `Union[toolkit_retrieve_response.ToolkitRetrieveResponse, ...` *** ## list\_categories() List all categories of toolkits. ```python def list_categories() ``` *** ## authorize() Authorize a user to a toolkit If auth config is not found, it will be created using composio managed auth. ```python def authorize(user_id: str, toolkit: str) ``` **Parameters** | Name | Type | | --------- | ----- | | `user_id` | `str` | | `toolkit` | `str` | *** ## get\_connected\_account\_initiation\_fields() Get the required property for a given toolkit and auth scheme. ```python def get_connected_account_initiation_fields(toolkit: str, auth_scheme: AuthSchemeL, required_only: bool = ...) -> AuthFieldsT ``` **Parameters** | Name | Type | | ---------------- | ------------- | | `toolkit` | `str` | | `auth_scheme` | `AuthSchemeL` | | `required_only?` | `bool` | **Returns** `AuthFieldsT` *** ## get\_auth\_config\_creation\_fields() Get the required property for a given toolkit and auth scheme. ```python def get_auth_config_creation_fields(toolkit: str, auth_scheme: AuthSchemeL, required_only: bool = ...) -> AuthFieldsT ``` **Parameters** | Name | Type | | ---------------- | ------------- | | `toolkit` | `str` | | `auth_scheme` | `AuthSchemeL` | | `required_only?` | `bool` | **Returns** `AuthFieldsT` *** [View source](https://github.com/composiohq/composio/blob/next/python/composio/core/models/toolkits.py#L26) --- # Tools (/reference/sdk-reference/python/tools) # Methods ## get\_raw\_composio\_tool\_by\_slug() Returns schema for the given tool slug. ```python def get_raw_composio_tool_by_slug(slug: str) -> Tool ``` **Parameters** | Name | Type | | ------ | ----- | | `slug` | `str` | **Returns** `Tool` *** ## get\_raw\_composio\_tools() Get a list of tool schemas based on the provided filters. ```python def get_raw_composio_tools(tools: list[str | None] = ..., search: str | None = ..., toolkits: list[str | None] = ..., scopes: List[str | None] = ..., limit: int | None = ...) -> list[Tool] ``` **Parameters** | Name | Type | | ----------- | ------------------- | | `tools?` | `list[str \| None]` | | `search?` | `str \| None` | | `toolkits?` | `list[str \| None]` | | `scopes?` | `List[str \| None]` | | `limit?` | `int \| None` | **Returns** `list[Tool]` *** ## get\_raw\_tool\_router\_meta\_tools() Fetches the tools exposed by a tool router session. This method fetches helper/meta tools and any preloaded app tools from the Composio API and transforms them to the expected format. It provides access to the underlying tool data without provider-specific wrapping. ```python def get_raw_tool_router_meta_tools(session_id: str, modifiers: 'Modifiers' | None = ...) -> list[Tool] ``` **Parameters** | Name | Type | | ------------ | --------------------- | | `session_id` | `str` | | `modifiers?` | `'Modifiers' \| None` | **Returns** `list[Tool]` — The list of meta tools **Example** ```python from composio import Composio composio = Composio() tools_model = composio.tools # Get meta tools for a session meta_tools = tools_model.get_raw_tool_router_meta_tools("session_123") print(meta_tools) # Get meta tools with schema modifiers from composio.core.models import schema_modifier @schema_modifier def modify_schema(tool: str, toolkit: str, schema): # Customize the schema schema.description = f"Modified: {schema.description}" return schema meta_tools = tools_model.get_raw_tool_router_meta_tools( "session_123", modifiers=[modify_schema] ) ``` *** ## get() Get a tool or list of tools based on the provided arguments. The return type is automatically inferred based on the provider's generic parameters. For example: - OpenAIProvider -> list\[ChatCompletionToolParam] - AnthropicProvider -> list\[ToolParam] - CustomProvider\[MyTool, list\[MyTool]] -> list\[MyTool] ```python def get(user_id: str, slug: str | None = ..., tools: list[str | None] = ..., search: str | None = ..., toolkits: list[str | None] = ..., scopes: List[str | None] = ..., modifiers: Modifiers | None = ..., limit: int | None = ...) -> TToolCollection ``` **Parameters** | Name | Type | | ------------ | ------------------- | | `user_id` | `str` | | `slug?` | `str \| None` | | `tools?` | `list[str \| None]` | | `search?` | `str \| None` | | `toolkits?` | `list[str \| None]` | | `scopes?` | `List[str \| None]` | | `modifiers?` | `Modifiers \| None` | | `limit?` | `int \| None` | **Returns** `TToolCollection` — Provider-specific tool collection (TToolCollection). *** ## execute() Execute a tool with the provided parameters. This method calls the Composio API to execute the tool and returns the response. ```python def execute(slug: str, arguments: Dict, connected_account_id: str | None = ..., custom_auth_params: tool_execute_params.CustomAuthParams | None = ..., custom_connection_data: tool_execute_params.CustomConnectionData | None = ..., user_id: str | None = ..., text: str | None = ..., version: str | None = ..., dangerously_skip_version_check: bool | None = ..., modifiers: Modifiers | None = ...) -> ToolExecutionResponse ``` **Parameters** | Name | Type | | --------------------------------- | -------------------------------------------------- | | `slug` | `str` | | `arguments` | `Dict` | | `connected_account_id?` | `str \| None` | | `custom_auth_params?` | `tool_execute_params.CustomAuthParams \| None` | | `custom_connection_data?` | `tool_execute_params.CustomConnectionData \| None` | | `user_id?` | `str \| None` | | `text?` | `str \| None` | | `version?` | `str \| None` | | `dangerously_skip_version_check?` | `bool \| None` | | `modifiers?` | `Modifiers \| None` | **Returns** `ToolExecutionResponse` — The response from the tool. *** ## proxy() Proxy a tool call to the Composio API ```python def proxy(endpoint: str, method: Literal['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'], body: object | None = ..., connected_account_id: str | None = ..., parameters: List[tool_proxy_params.Parameter | None] = ..., custom_connection_data: tool_proxy_params.CustomConnectionData | None = ...) -> tool_proxy_response.ToolProxyResponse ``` **Parameters** | Name | Type | | ------------------------- | ---------------------------------------------------------- | | `endpoint` | `str` | | `method` | `Literal['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD']` | | `body?` | `object \| None` | | `connected_account_id?` | `str \| None` | | `parameters?` | `List[tool_proxy_params.Parameter \| None]` | | `custom_connection_data?` | `tool_proxy_params.CustomConnectionData \| None` | **Returns** `tool_proxy_response.ToolProxyResponse` *** [View source](https://github.com/composiohq/composio/blob/next/python/composio/core/models/tools.py#L89) --- # Triggers (/reference/sdk-reference/python/triggers) # Methods ## set\_webhook\_subscription() Create or update the project webhook subscription used for webhook delivery. If a subscription already exists, the first subscription is updated. Otherwise a new subscription is created. By default this subscribes to V3 trigger message events. ```python def set_webhook_subscription(webhook_url: str, enabled_events: Sequence[str | None] = ..., version: Union[WebhookVersion, str] = ...) -> WebhookSubscription ``` **Parameters** | Name | Type | | ----------------- | ---------------------------- | | `webhook_url` | `str` | | `enabled_events?` | `Sequence[str \| None]` | | `version?` | `Union[WebhookVersion, str]` | **Returns** `WebhookSubscription` **Example** ```python composio.triggers.set_webhook_subscription( webhook_url=f"{APP_URL}/webhooks/composio", ) ``` *** ## get\_type() Get a trigger type by its slug Uses the global toolkit version provided when initializing composio instance to fetch trigger for specific toolkit version ```python def get_type(slug: str) -> TriggersTypeRetrieveResponse ``` **Parameters** | Name | Type | | ------ | ----- | | `slug` | `str` | **Returns** `TriggersTypeRetrieveResponse` — The trigger type *** ## list\_active() List all active triggers ```python def list_active(trigger_ids: list[str | None] = ..., trigger_names: list[str | None] = ..., auth_config_ids: list[str | None] = ..., connected_account_ids: list[str | None] = ..., show_disabled: bool | None = ..., limit: int | None = ..., cursor: str | None = ...) ``` **Parameters** | Name | Type | | ------------------------ | ------------------- | | `trigger_ids?` | `list[str \| None]` | | `trigger_names?` | `list[str \| None]` | | `auth_config_ids?` | `list[str \| None]` | | `connected_account_ids?` | `list[str \| None]` | | `show_disabled?` | `bool \| None` | | `limit?` | `int \| None` | | `cursor?` | `str \| None` | *** ## list() List all the trigger types. ```python def list(cursor: str | None = ..., limit: int | None = ..., toolkit_slugs: list[str | None] = ...) ``` **Parameters** | Name | Type | | ---------------- | ------------------- | | `cursor?` | `str \| None` | | `limit?` | `int \| None` | | `toolkit_slugs?` | `list[str \| None]` | *** ## create() Create a trigger instance ```python def create(slug: str, user_id: str | None = ..., connected_account_id: str | None = ..., trigger_config: Dict[str, Any | None] = ...) -> trigger_instance_upsert_response.TriggerInstanceUpsertRes... ``` **Parameters** | Name | Type | | ----------------------- | ------------------------ | | `slug` | `str` | | `user_id?` | `str \| None` | | `connected_account_id?` | `str \| None` | | `trigger_config?` | `Dict[str, Any \| None]` | **Returns** `trigger_instance_upsert_response.TriggerInstanceUpsertRes...` — The trigger instance *** ## subscribe() Subscribe to a trigger and receive trigger events. ```python def subscribe(timeout: float = ...) -> TriggerSubscription ``` **Parameters** | Name | Type | | ---------- | ------- | | `timeout?` | `float` | **Returns** `TriggerSubscription` — The trigger subscription handler. *** ## verify\_webhook() Verify an incoming webhook payload and signature. This method validates that the webhook request is authentic by: 1. Validating the webhook timestamp is within the tolerance window 2. Verifying the HMAC-SHA256 signature using the correct algorithm 3. Parsing the payload and detecting the webhook version (V1, V2, or V3) ```python def verify_webhook(id: str, payload: str, secret: str, signature: str, timestamp: str, tolerance: int = ...) -> VerifyWebhookResult ``` **Parameters** | Name | Type | | ------------ | ----- | | `id` | `str` | | `payload` | `str` | | `secret` | `str` | | `signature` | `str` | | `timestamp` | `str` | | `tolerance?` | `int` | **Returns** `VerifyWebhookResult` — VerifyWebhookResult containing version, normalized payload, and raw payload :raises WebhookSignatureVerificationError: If the signature verification fails :raises WebhookPayloadError: If the payload cannot be parsed or is invalid **Example** ```python # In a Flask webhook handler @app.route('/webhook', methods=['POST']) def webhook(): try: result = composio.triggers.verify_webhook( id=request.headers.get('webhook-id', ''), payload=request.get_data(as_text=True), signature=request.headers.get('webhook-signature', ''), timestamp=request.headers.get('webhook-timestamp', ''), secret=os.environ['COMPOSIO_WEBHOOK_SECRET'], ) # Process the verified payload print(f"Version: {result['version']}") print(f"Received trigger: {result['payload']['trigger_slug']}") return 'OK', 200 except WebhookSignatureVerificationError: return 'Unauthorized', 401 ``` *** ## parse() Parse an incoming webhook request into a typed, normalized trigger payload. Pass a framework request object, or pass `body=` and `headers=` explicitly. When `verify_secret` is provided, the SDK verifies the webhook signature before returning the normalized trigger payload. When it is omitted, the SDK parses the body without verifying the signature. `request` may be any object exposing the request body and headers, such as a Flask, Django, or FastAPI request. The body is read from `.body` (or `.data` / `.get_data()`), and the headers are read from `.headers`. Because this SDK is synchronous, async frameworks must pass an already-read raw body, for example via `body=await request.body()`. ```python def parse(request: Any = ..., body: Union[str, bytes, Mapping[str, Any], None] = ..., headers: Union[Mapping[str, Any], None] = ..., verify_secret: Union[str, None, Omit] = ..., tolerance: int = ...) -> VerifyWebhookResult ``` **Parameters** | Name | Type | | ---------------- | -------------------------------------------- | | `request?` | `Any` | | `body?` | `Union[str, bytes, Mapping[str, Any], None]` | | `headers?` | `Union[Mapping[str, Any], None]` | | `verify_secret?` | `Union[str, None, Omit]` | | `tolerance?` | `int` | **Returns** `VerifyWebhookResult` — VerifyWebhookResult containing version, normalized payload, and raw payload :raises ValidationError: If `verify_secret` is empty, or is set but signature headers are missing :raises WebhookSignatureVerificationError: If signature verification fails :raises WebhookPayloadError: If the payload cannot be parsed **Example** ```python # Flask: verify the signature @app.route('/webhooks/composio', methods=['POST']) def webhook(): try: result = composio.triggers.parse( request, verify_secret=os.environ['COMPOSIO_WEBHOOK_SECRET'], ) print(f"Trigger: {result['payload']['trigger_slug']}") print(f"Event data: {result['payload']['payload']}") return 'OK', 200 except exceptions.WebhookSignatureVerificationError: return 'Unauthorized', 401 # FastAPI: parse without verifying after reading the async body @app.post('/webhooks/composio') async def webhook(request: Request): raw = await request.body() result = composio.triggers.parse(body=raw, headers=request.headers) return {'trigger': result['payload']['trigger_slug']} ``` *** [View source](https://github.com/composiohq/composio/blob/next/python/composio/core/models/triggers.py#L881) --- # AuthConfigs (/reference/sdk-reference/typescript/auth-configs) # Usage Access this class through the `composio.authConfigs` property: ```typescript const composio = new Composio({ apiKey: 'your-api-key' }); const result = await composio.authConfigs.list(); ``` # Methods ## create() Create a new auth config ```typescript async create(toolkit: string, options: CreateAuthConfigParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | -------------------------------------- | | `toolkit` | `string` | Unique identifier of the toolkit | | `options` | `CreateAuthConfigParams` | Options for creating a new auth config | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — Created auth config **Example** ```typescript const authConfig = await authConfigs.create('my-toolkit', { type: AuthConfigTypes.CUSTOM, name: 'My Custom Auth Config', authScheme: AuthSchemeTypes.API_KEY, credentials: { apiKey: '1234567890', }, }); ``` *** ## delete() Deletes an authentication configuration. This method permanently removes an auth config from the Composio platform. This action cannot be undone and will prevent any connected accounts that use this auth config from functioning. ```typescript async delete(nanoid: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | -------------------------------------------------- | | `nanoid` | `string` | The unique identifier of the auth config to delete | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The deletion response **Example** ```typescript // Delete an auth config await composio.authConfigs.delete('auth_abc123'); ``` *** ## disable() Disables an authentication configuration. This is a convenience method that calls updateStatus with 'DISABLED'. When disabled, the auth config cannot be used to create new connected accounts or authenticate with third-party services, but existing connections may continue to work. ```typescript async disable(nanoid: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | --------------------------------------------------- | | `nanoid` | `string` | The unique identifier of the auth config to disable | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The updated auth config details **Example** ```typescript // Disable an auth config await composio.authConfigs.disable('auth_abc123'); ``` *** ## enable() Enables an authentication configuration. This is a convenience method that calls updateStatus with 'ENABLED'. When enabled, the auth config can be used to create new connected accounts and authenticate with third-party services. ```typescript async enable(nanoid: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | -------------------------------------------------- | | `nanoid` | `string` | The unique identifier of the auth config to enable | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The updated auth config details **Example** ```typescript // Enable an auth config await composio.authConfigs.enable('auth_abc123'); ``` *** ## get() Retrieves a specific authentication configuration by its ID. This method fetches detailed information about a single auth config and transforms the response to the SDK's standardized format. ```typescript async get(nanoid: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ---------------------------------------------------- | | `nanoid` | `string` | The unique identifier of the auth config to retrieve | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The auth config details **Example** ```typescript // Get an auth config by ID const authConfig = await composio.authConfigs.get('auth_abc123'); console.log(authConfig.name); // e.g., 'GitHub Auth' console.log(authConfig.toolkit.slug); // e.g., 'github' ``` *** ## list() Lists authentication configurations based on provided filter criteria. This method retrieves auth configs from the Composio API, transforms them to the SDK format, and supports filtering by various parameters. ```typescript async list(query?: AuthConfigListParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ---------------------------------------------------- | | `query?` | `AuthConfigListParams` | Optional query parameters for filtering auth configs | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — A paginated list of auth configurations **Example** ```typescript // List all auth configs const allConfigs = await composio.authConfigs.list(); // List auth configs for a specific toolkit const githubConfigs = await composio.authConfigs.list({ toolkit: 'github' }); // Search auth configs by name or id const searchedConfigs = await composio.authConfigs.list({ search: 'github', showDisabled: true }); // List Composio-managed auth configs const managedConfigs = await composio.authConfigs.list({ isComposioManaged: true }); ``` *** ## update() Updates an existing authentication configuration. This method allows you to modify properties of an auth config such as credentials, scopes, or tool restrictions. The update type (custom or default) determines which fields can be updated. ```typescript async update(nanoid: string, data: AuthConfigUpdateParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | -------------------------------------------------------------- | | `nanoid` | `string` | The unique identifier of the auth config to update | | `data` | `AuthConfigUpdateParams` | The data to update, which can be either custom or default type | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The updated auth config **Example** ```typescript // Update a custom auth config with new credentials const updatedConfig = await composio.authConfigs.update('auth_abc123', { type: 'custom', credentials: { apiKey: 'new-api-key-value' } }); // Update a default auth config with new scopes const updatedConfig = await composio.authConfigs.update('auth_abc123', { type: 'default', scopes: ['read:user', 'repo'] }); ``` *** ## updateStatus() Updates the status of an authentication configuration. This method allows you to enable or disable an auth config. When disabled, the auth config cannot be used to create new connected accounts or authenticate with third-party services. ```typescript async updateStatus(status: 'ENABLED' | 'DISABLED', nanoid: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------- | ------------------------------------------- | | `status` | `'ENABLED' \| 'DISABLED'` | The status to set ('ENABLED' or 'DISABLED') | | `nanoid` | `string` | The unique identifier of the auth config | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The updated auth config details **Example** ```typescript // Disable an auth config await composio.authConfigs.updateStatus('DISABLED', 'auth_abc123'); // Enable an auth config await composio.authConfigs.updateStatus('ENABLED', 'auth_abc123'); ``` *** --- # Composio (/reference/sdk-reference/typescript/composio) # Constructor ## constructor() Creates a new instance of the Composio SDK. The constructor initializes the SDK with the provided configuration options, sets up the API client, and initializes all core models (tools, toolkits, etc.). ```typescript constructor(config?: ComposioConfig): Composio ``` **Parameters** | Name | Type | Description | | --------- | ---------------- | ------------------------------------------ | | `config?` | `ComposioConfig` | Configuration options for the Composio SDK | **Returns** `Composio` **Example** ```typescript // Initialize with default configuration const composio = new Composio(); // Initialize with custom API key and base URL const composio = new Composio({ apiKey: 'your-api-key', baseURL: 'https://api.composio.dev' }); // Initialize with custom provider const composio = new Composio({ apiKey: 'your-api-key', provider: new CustomProvider() }); ``` *** # Properties | Name | Type | Description | | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | `authConfigs` | `AuthConfigs` | Manage authentication configurations for toolkits | | `connectedAccounts` | `ConnectedAccounts` | Manage authenticated connections | | `create` | `(userId: string, config: \{ authConfigs?: Record; connectedAccounts?: Record; experimental?: \{ assistivePrompt?: \{ userTimezone?: string \}; customToolkits?: CustomToolkit[]; customTools?: CustomTool[] \}; manageConnections?: boolean \| \{ callbackUrl?: string; enable?: boolean; waitForConnections?: boolean \}; mcp?: boolean; multiAccount?: \{ enable: boolean; maxAccountsPerToolkit?: number; requireExplicitSelection?: boolean \}; preload?: \{ tools?: ...[] \| 'all' \}; sandbox?: \{ autoOffloadThreshold?: number; enable: boolean; enableProxyExecution?: boolean; sandboxSize?: 'standard' \| 'medium' \| 'large' \| 'xlarge' \}; sessionPreset?: 'direct_tools'; tags?: ... \| ... \| ... \| ...[] \| \{ disable?: ...[]; enable?: ...[] \}; toolkits?: string[] \| \{ disable: ...[] \} \| \{ enable: ...[] \}; tools?: Record; workbench?: \{ autoOffloadThreshold?: number; enable: boolean; enableProxyExecution?: boolean; sandboxSize?: 'standard' \| 'medium' \| 'large' \| 'xlarge' \} \} & \{ mcp: true \}, requestOptions: ComposioRequestOptions) => Promise` | Creates a new tool router session for a user. | | Use `sessionPreset: SessionPreset.DIRECT_TOOLS` when all needed tools | | | | should be exposed directly; see `ToolRouterCreateSessionConfig`. | | | | `experimental` | `Experimental` | Experimental SDK methods whose shape may change in future releases. | | Prefer domain-specific mounts (for example | | | | `composio.connectedAccounts.updateAcl(...)`) when available; this | | | | namespace keeps compatibility aliases while APIs are experimental. | | | | Stateless experimental factories (e.g. `experimental_createTool`) stay | | | | at the top level. | | | | `files` | `Files` | Upload and download files | | `mcp` | `MCP` | Model Context Protocol server management. | | `provider` | `TProvider` | The tool provider instance used for wrapping tools in framework-specific formats | | `sessions` | `Sessions` | Create and reuse Composio sessions. | Prefer `composio.sessions.create(...)` for new code. The top-level `composio.create(...)` method is kept as an alias. | \| `toolkits` | `Toolkits` | Retrieve toolkit metadata and authorize user connections | \| `toolRouter` | `ToolRouter` | Legacy alias for `composio.sessions`. | \| `tools` | `Tools` | List, retrieve, and execute tools | \| `triggers` | `Triggers` | Manage webhook triggers and event subscriptions | \| `use` | `(id: string, options: \{ customToolkits?: CustomToolkit[]; customTools?: CustomTool[]; mcp: true \}, requestOptions: ComposioRequestOptions) => Promise` | Use an existing tool router session | # Methods ## createSession() (deprecated) > **Deprecated**: Will be removed in a future version of the SDK. Instead, construct a new instance directly with the headers you need: `new Composio(\{ ...existingConfig, defaultHeaders \})`. For one-off overrides, pass per-call `requestOptions` where supported. Creates a new instance of the Composio SDK with custom request options while preserving the existing configuration. This method is particularly useful when you need to: * Add custom headers for specific requests * Track request contexts with unique identifiers * Override default request behavior for a subset of operations The new instance inherits all configuration from the parent instance (apiKey, baseURL, provider, etc.) but allows you to specify custom request options that will be used for all API calls made through this session. ```typescript createSession(options?: { headers?: ComposioRequestHeaders }): Composio ``` **Parameters** | Name | Type | | ---------- | ---------------------------------------- | | `options?` | `\{ headers?: ComposioRequestHeaders \}` | **Returns** `Composio` — A new Composio instance with the custom request options applied. **Example** ```typescript // Create a base Composio instance const composio = new Composio({ apiKey: 'your-api-key' }); // Create a session with request tracking headers const composioWithCustomHeaders = composio.createSession({ headers: { 'x-request-id': '1234567890', 'x-correlation-id': 'session-abc-123', 'x-custom-header': 'custom-value' } }); // Use the session for making API calls with the custom headers await composioWithCustomHeaders.tools.list(); ``` *** ## flush() Flush any pending telemetry and wait for it to complete. In Node.js-compatible environments, telemetry is automatically flushed on process exit. However, in environments like Cloudflare Workers that don't support process exit events, you should call this method manually to ensure all telemetry is sent. ```typescript async flush(): Promise ``` **Returns** `Promise` — A promise that resolves when all pending telemetry has been sent. **Example** ```typescript // In a Cloudflare Worker, use ctx.waitUntil to ensure telemetry is flushed export default { async fetch(request: Request, env: Env, ctx: ExecutionContext) { const composio = new Composio({ apiKey: env.COMPOSIO_API_KEY }); // Do your work... const result = await composio.tools.execute(...); // Ensure telemetry flushes before worker terminates ctx.waitUntil(composio.flush()); return new Response(JSON.stringify(result)); } }; ``` *** ## getClient() Get the Composio SDK client. ```typescript getClient(): ComposioClient ``` **Returns** `ComposioClient` — The Composio API client. *** ## getConfig() Get the configuration SDK is initialized with. Returns a frozen shallow clone — the SDK has already snapshotted configuration values such as `dangerouslyAllowAutoUploadDownloadFiles`, `fileUploadDirs`, and `fileDownloadDir` into its internal models, so mutating the live config object would silently no-op. Freezing makes that contract visible at the call site instead of letting the mutation appear successful. ```typescript getConfig(): Readonly ``` **Returns** `Readonly` — The frozen configuration the SDK is initialized with. *** --- # ConnectedAccounts (/reference/sdk-reference/typescript/connected-accounts) # Usage Access this class through the `composio.connectedAccounts` property: ```typescript const composio = new Composio({ apiKey: 'your-api-key' }); const result = await composio.connectedAccounts.list(); ``` # Methods ## delete() Deletes a connected account. This method permanently removes a connected account from the Composio platform. This action cannot be undone and will revoke any access tokens associated with the account. ```typescript async delete(nanoid: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | -------------------------------------------------------- | | `nanoid` | `string` | The unique identifier of the connected account to delete | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The deletion response **Example** ```typescript // Delete a connected account await composio.connectedAccounts.delete('conn_abc123'); ``` *** ## disable() Disable a connected account ```typescript async disable(nanoid: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ------------------------------------------ | | `nanoid` | `string` | Unique identifier of the connected account | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — Updated connected account details **Example** ```typescript // Disable a connected account const disabledAccount = await composio.connectedAccounts.disable('conn_abc123'); console.log(disabledAccount.isDisabled); // true // You can also use updateStatus with a reason // const disabledAccount = await composio.connectedAccounts.updateStatus('conn_abc123', { // enabled: false, // reason: 'No longer needed' // }); ``` *** ## enable() Enable a connected account ```typescript async enable(nanoid: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ------------------------------------------ | | `nanoid` | `string` | Unique identifier of the connected account | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — Updated connected account details **Example** ```typescript // Enable a previously disabled connected account const enabledAccount = await composio.connectedAccounts.enable('conn_abc123'); console.log(enabledAccount.isDisabled); // false ``` *** ## get() Retrieves a specific connected account by its ID. This method fetches detailed information about a single connected account and transforms the response to the SDK's standardized format. ```typescript async get(nanoid: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ---------------------------------------------- | | `nanoid` | `string` | The unique identifier of the connected account | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The connected account details **Example** ```typescript // Get a connected account by ID const account = await composio.connectedAccounts.get('conn_abc123'); console.log(account.status); // e.g., 'ACTIVE' console.log(account.toolkit.slug); // e.g., 'github' ``` *** ## initiate() Compound function to create a new connected account. This function creates a new connected account and returns a connection request. Users can then wait for the connection to be established using the `waitForConnection` method. **Deprecated for Composio-managed OAuth (OAuth1, OAuth2, DCR\_OAUTH).** The legacy `POST /api/v3/connected_accounts` endpoint that this method wraps is being retired for Composio-managed auth configs on redirectable schemes. The cutover is **2026-05-08** for new organizations and **2026-07-03** for all remaining organizations. After your org's cutover, this method will throw ComposioLegacyConnectedAccountsEndpointRetiredError for that specific combination. Use ConnectedAccounts.link for Composio-managed OAuth — it works for every redirectable scheme regardless of whether the auth config is Composio-managed or custom, and the return shape is the same. Custom auth configs (your own OAuth app) and non-OAuth schemes (API key, bearer token, basic auth) are unaffected and continue to work on `initiate()`. See [https://docs.composio.dev/docs/changelog/2026/04/24](https://docs.composio.dev/docs/changelog/2026/04/24) ```typescript async initiate(userId: string, authConfigId: string, options?: CreateConnectedAccountOptions, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------------- | -------------------------------------------- | | `userId` | `string` | User ID of the connected account | | `authConfigId` | `string` | Auth config ID of the connected account | | `options?` | `CreateConnectedAccountOptions` | Options for creating a new connected account | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — Connection request object **Example** ```typescript // For OAuth2 authentication const connectionRequest = await composio.connectedAccounts.initiate( 'user_123', 'auth_config_123', { callbackUrl: 'https://your-app.com/callback', config: AuthScheme.OAuth2({ access_token: 'your_access_token', token_type: 'Bearer' }) } ); // For API Key authentication const connectionRequest = await composio.connectedAccounts.initiate( 'user_123', 'auth_config_123', { config: AuthScheme.ApiKey({ api_key: 'your_api_key' }) } ); // For Basic authentication const connectionRequest = await composio.connectedAccounts.initiate( 'user_123', 'auth_config_123', { config: AuthScheme.Basic({ username: 'your_username', password: 'your_password' }) } ); ``` *** ## link() ```typescript async link(userId: string, authConfigId: string, options?: CreateConnectedAccountLinkOptions, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ----------------------------------- | ----------------------------------------------------------------------------------------- | | `userId` | `string` | \{string} - The external user ID to create the connected account for. | | `authConfigId` | `string` | \{string} - The auth config ID to create the connected account for. | | `options?` | `CreateConnectedAccountLinkOptions` | \{CreateConnectedAccountLinkOptions} - Options for creating a new connected account link. | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — Connection request object **Example** ```typescript // create a connection request and redirect the user to the redirect url const connectionRequest = await composio.connectedAccounts.link('user_123', 'auth_config_123'); const redirectUrl = connectionRequest.redirectUrl; console.log(`Visit: ${redirectUrl} to authenticate your account`); // Wait for the connection to be established const connectedAccount = await connectionRequest.waitForConnection() ``` ```typescript // create a connection request and redirect the user to the redirect url const connectionRequest = await composio.connectedAccounts.link('user_123', 'auth_config_123', { callbackUrl: 'https://your-app.com/callback' }); const redirectUrl = connectionRequest.redirectUrl; console.log(`Visit: ${redirectUrl} to authenticate your account`); // Wait for the connection to be established const connectedAccount = await composio.connectedAccounts.waitForConnection(connectionRequest.id); ``` *** ## list() Lists all connected accounts based on provided filter criteria. This method retrieves connected accounts from the Composio API with optional filtering. ```typescript async list(query?: ConnectedAccountListParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ---------------------------- | ---------------------------------------------------------- | | `query?` | `ConnectedAccountListParams` | Optional query parameters for filtering connected accounts | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — A paginated list of connected accounts **Example** ```typescript // List all connected accounts const allAccounts = await composio.connectedAccounts.list(); // List accounts for a specific user const userAccounts = await composio.connectedAccounts.list({ userIds: ['user123'] }); // List accounts for a specific toolkit const githubAccounts = await composio.connectedAccounts.list({ toolkitSlugs: ['github'] }); ``` *** ## refresh() Refreshes a connected account's authentication credentials. This method attempts to refresh OAuth tokens or other credentials associated with the connected account. This is useful when a token has expired or is about to expire. ```typescript async refresh(nanoid: string, options?: ConnectedAccountRefreshOptions, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | -------------------------------- | --------------------------------------------------------- | | `nanoid` | `string` | The unique identifier of the connected account to refresh | | `options?` | `ConnectedAccountRefreshOptions` | | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The response containing the refreshed account details **Example** ```typescript // Refresh a connected account's credentials const refreshedAccount = await composio.connectedAccounts.refresh('conn_abc123'); ``` *** ## update() Enable or disable a connected account. Accepts `{ enabled: boolean }`. Use `updateAcl()` for ACL writes on SHARED connections. ```typescript async update(nanoid: string, params: UpdateConnectedAccountParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------------ | ---------------------------------------------- | | `nanoid` | `string` | The unique identifier of the connected account | | `params` | `UpdateConnectedAccountParams` | The update parameters | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The update response **Example** ```typescript // Disable an account await composio.connectedAccounts.update('ca_abc123', { enabled: false }); ``` *** ## updateAcl() Update the per-user ACL on a SHARED connected account. **Experimental — shape may change in future releases.** Only meaningful for SHARED connections — calling this on a PRIVATE connection raises `ComposioAclOnlyForSharedError` (400). ACL writes require the connection's creator or an API key. PATCH semantics: omit a field to leave it unchanged; pass an empty array to clear an allow/deny list. At least one field must be provided. ```typescript async updateAcl(nanoid: string, params: UpdateConnectedAccountAclParams): Promise ``` **Parameters** | Name | Type | Description | | -------- | --------------------------------- | ---------------------------------------------- | | `nanoid` | `string` | The unique identifier of the connected account | | `params` | `UpdateConnectedAccountAclParams` | The ACL fields to patch | **Returns** `Promise` — The PATCH response **Example** ```typescript // Allow every userId to use this SHARED connection await composio.connectedAccounts.updateAcl('ca_abc123', { allowAllUsers: true }); // Targeted allow list await composio.connectedAccounts.updateAcl('ca_abc123', { allowedUserIds: ['user_alice', 'user_bob'], }); // Clear the allow list (back to deny-by-default unless allowAllUsers is true) await composio.connectedAccounts.updateAcl('ca_abc123', { allowedUserIds: [] }); ``` *** ## updateStatus() Update the status of a connected account ```typescript async updateStatus(nanoid: string, params: ConnectedAccountUpdateStatusParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------------------ | ------------------------------------------ | | `nanoid` | `string` | Unique identifier of the connected account | | `params` | `ConnectedAccountUpdateStatusParams` | Parameters for updating the status | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — Updated connected account details **Example** ```typescript // Enable a connected account const updatedAccount = await composio.connectedAccounts.updateStatus('conn_abc123', { enabled: true }); // Disable a connected account with a reason const disabledAccount = await composio.connectedAccounts.updateStatus('conn_abc123', { enabled: false, reason: 'Token expired' }); ``` *** ## waitForConnection() Waits for a connection request to complete and become active. This method continuously polls the Composio API to check the status of a connection until it either becomes active, enters a terminal error state, or times out. ```typescript async waitForConnection(connectedAccountId: string, timeout?: number): Promise ``` **Parameters** | Name | Type | Description | | -------------------- | -------- | ---------------------------------------------------------- | | `connectedAccountId` | `string` | The ID of the connected account to wait for | | `timeout?` | `number` | Maximum time to wait in milliseconds (default: 60 seconds) | **Returns** `Promise` — The finalized connected account data **Example** ```typescript // Wait for a connection to complete with default timeout const connectedAccount = await composio.connectedAccounts.waitForConnection('conn_123abc'); // Wait with a custom timeout of 2 minutes const connectedAccount = await composio.connectedAccounts.waitForConnection('conn_123abc', 120000); ``` *** --- # Experimental (/reference/sdk-reference/typescript/experimental) # Usage Access this class through the `composio.experimental` property: ```typescript const composio = new Composio({ apiKey: 'your-api-key' }); const result = await composio.experimental.list(); ``` # Methods ## updateAcl() (deprecated) > **Deprecated**: Use `composio.connectedAccounts.updateAcl(...)` instead — ACL updates graduated onto the `connectedAccounts` mount. This experimental alias is kept only for backwards compatibility and will be removed once the API graduates. Prefer the `connectedAccounts` mount; do not generate new code against this alias. Compatibility alias for `composio.connectedAccounts.updateAcl(...)`. Update the per-user ACL on a SHARED connected account. **Experimental — shape may change in future releases.** Only meaningful for SHARED connections — calling this on a PRIVATE connection raises `ComposioAclOnlyForSharedError` (400). ACL writes require the connection's creator or an API key. PATCH semantics: omit a field to leave it unchanged; pass an empty array to clear an allow/deny list. At least one field must be provided. Resolution rule (deny wins): 1. requesting `userId` in `notAllowedUserIds` → DENY 2. `allowAllUsers === true` → ALLOW 3. requesting `userId` in `allowedUserIds` → ALLOW 4. otherwise → DENY ```typescript async updateAcl(nanoid: string, params: UpdateConnectedAccountAclParams): Promise ``` **Parameters** | Name | Type | | -------- | --------------------------------- | | `nanoid` | `string` | | `params` | `UpdateConnectedAccountAclParams` | **Returns** `Promise` — The PATCH response (`\{ id, status, success \}`). To read the updated ACL block, call `composio.connectedAccounts.get(nanoid)` after the promise resolves and inspect `account.experimental?.aclConfigForShared`. **Example** ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: '...' }); // Allow every userId to use this connection await composio.connectedAccounts.updateAcl('ca_abc', { allowAllUsers: true }); // Everyone except a specific user await composio.connectedAccounts.updateAcl('ca_abc', { allowAllUsers: true, notAllowedUserIds: ['user_bob'], }); // Targeted allow await composio.connectedAccounts.updateAcl('ca_abc', { allowedUserIds: ['user_alice', 'user_bob'], }); // Revoke a previously-granted allow list (back to deny-by-default) await composio.connectedAccounts.updateAcl('ca_abc', { allowedUserIds: [] }); ``` **Empty-array semantics — read carefully.** Passing `[]` for either list **replaces** the list, it does not extend it: * `allowedUserIds: []` → revoke all previously-granted user IDs (state reverts to deny-by-default unless `allowAllUsers` is true). * `notAllowedUserIds: []` → **clears the deny list**, which silently re-grants access to users you previously blocked. Always pair an empty deny list with a deliberate audit of the allow side. ``` --- ``` --- # TypeScript SDK Reference (/reference/sdk-reference/typescript) # Installation # Classes | Class | Description | | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------- | | [`Composio`](/reference/sdk-reference/typescript/composio) | This is the core class for Composio. | | [`AuthConfigs`](/reference/sdk-reference/typescript/auth-configs) | AuthConfigs class | | [`ConnectedAccounts`](/reference/sdk-reference/typescript/connected-accounts) | ConnectedAccounts class | | [`Experimental`](/reference/sdk-reference/typescript/experimental) | Experimental API | | [`MCP`](/reference/sdk-reference/typescript/mcp) | MCP (Model Control Protocol) class | | [`RemoteFile`](/reference/sdk-reference/typescript/remote-file) | Represents a file stored in a tool router session's file mount. | | [`Sessions`](/reference/sdk-reference/typescript/sessions) | First-class API for creating and reusing Composio sessions. | | [`Toolkits`](/reference/sdk-reference/typescript/toolkits) | Toolkits class | | [`Session`](/reference/sdk-reference/typescript/session) | A Composio session — the object returned by `composio.sessions.create(...)` | | [`Session files`](/reference/sdk-reference/typescript/session-files) | File mount for a Composio session, reached via `session.experimental.files` | | [`Tools`](/reference/sdk-reference/typescript/tools) | This class is used to manage tools in the Composio SDK. | | [`Triggers`](/reference/sdk-reference/typescript/triggers) | Trigger (Instance) class | # Quick Start ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY }); // Get tools for a user const tools = await composio.tools.get('user-123', { toolkits: ['github'] }); // Execute a tool const result = await composio.tools.execute('GITHUB_GET_REPOS', { userId: 'user-123', arguments: { owner: 'composio' } }); ``` --- # MCP (/reference/sdk-reference/typescript/mcp) > **Deprecated**: Use a session's MCP endpoint instead: `composio.create(userId, \{ mcp: true \})` returns a session that exposes `session.mcp.url` / `session.mcp.headers`. MCP is now opt-in per session; the standalone `composio.mcp` server-management API is kept only for backwards compatibility. Prefer the session MCP endpoint; do not generate new code against `composio.mcp`. See [https://docs.composio.dev/docs/sessions-via-mcp](https://docs.composio.dev/docs/sessions-via-mcp) # Usage Access this class through the `composio.mcp` property: ```typescript const composio = new Composio({ apiKey: 'your-api-key' }); const result = await composio.mcp.list(); ``` # Properties | Name | Type | | -------- | ---------- | | `client` | `Composio` | # Methods ## create() Create a new MCP configuration. ```typescript async create(name: string, mcpConfig: MCPConfigCreationParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | | ----------------- | ------------------------- | | `name` | `string` | | `mcpConfig` | `MCPConfigCreationParams` | | `requestOptions?` | `ComposioRequestOptions` | **Returns** `Promise` — Created server details with instance getter **Example** ```typescript const server = await composio.mcpConfig.create("personal-mcp-server", { toolkits: ["github", "slack"], allowedTools: ["GMAIL_FETCH_EMAILS", "SLACK_SEND_MESSAGE"], manuallyManageConnections: false } }); const server = await composio.mcpConfig.create("personal-mcp-server", { toolkits: [{ toolkit: "gmail", authConfigId: "ac_243434343" }], allowedTools: ["GMAIL_FETCH_EMAILS"], manuallyManageConnections: false } }); ``` *** ## delete() Delete an MCP server configuration permanently ```typescript async delete(serverId: string, requestOptions?: ComposioRequestOptions): Promise<{ id: string; deleted: boolean }> ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ------------------------------------------------- | | `serverId` | `string` | The unique identifier of the MCP server to delete | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise<\{ id: string; deleted: boolean \}>` — Confirmation object with server ID and deletion status **Example** ```typescript // Delete an MCP server by ID const result = await composio.experimental.mcp.delete("mcp_12345"); if (result.deleted) { console.log(`Server ${result.id} has been successfully deleted`); } else { console.log(`Failed to delete server ${result.id}`); } // Example with error handling try { const result = await composio.experimental.mcp.delete("mcp_12345"); console.log("Deletion successful:", result); } catch (error) { console.error("Failed to delete MCP server:", error.message); } // Delete and verify from list await composio.experimental.mcp.delete("mcp_12345"); const servers = await composio.experimental.mcp.list({}); const serverExists = servers.items.some(server => server.id === "mcp_12345"); console.log("Server still exists:", serverExists); // Should be false ``` *** ## generate() Get server URLs for an existing MCP server. The response is wrapped according to the provider's specifications. ```typescript async generate(userId: string, mcpConfigId: string, options?: MCPGetInstanceParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ------------------------------------------------------------------------------ | | `userId` | `string` | \{string} external user id from your database for whom you want the server for | | `mcpConfigId` | `string` | \{string} config id of the MCPConfig for which you want to create a server for | | `options?` | `MCPGetInstanceParams` | \{object} additional options | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` **Example** ```typescript import { Composio } from "@composio/code"; const composio = new Composio(); const mcp = await composio.experimental.mcp.generate("default", ""); ``` *** ## get() Retrieve detailed information about a specific MCP server by its ID ```typescript async get(serverId: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | --------------------------------------------------- | | `serverId` | `string` | The unique identifier of the MCP server to retrieve | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — Complete MCP server details including configuration, tools, and metadata **Example** ```typescript // Get a specific MCP server by ID const server = await composio.experimental.mcp.get("mcp_12345"); console.log(server.name); // "My Personal MCP Server" console.log(server.allowedTools); // ["GITHUB_CREATE_ISSUE", "SLACK_SEND_MESSAGE"] console.log(server.toolkits); // ["github", "slack"] console.log(server.serverInstanceCount); // 3 // Access setup commands for different clients console.log(server.commands.claude); // Claude setup command console.log(server.commands.cursor); // Cursor setup command console.log(server.commands.windsurf); // Windsurf setup command // Use the MCP URL for direct connections const mcpUrl = server.MCPUrl; ``` *** ## list() List the MCP servers with optional filtering and pagination ```typescript async list(options: MCPListParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | -------------------------------- | | `options` | `MCPListParams` | Filtering and pagination options | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — Paginated list of MCP servers with metadata **Example** ```typescript // List all MCP servers const allServers = await composio.experimental.mcp.list({}); // List with pagination const pagedServers = await composio.experimental.mcp.list({ page: 2, limit: 5 }); // Filter by toolkit const githubServers = await composio.experimental.mcp.list({ toolkits: ['github', 'slack'] }); // Filter by name const namedServers = await composio.experimental.mcp.list({ name: 'personal' }); ``` *** ## update() Update an existing MCP server configuration with new settings ```typescript async update(serverId: string, config: MCPUpdateParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ------------------------------------------------- | | `serverId` | `string` | The unique identifier of the MCP server to update | | `config` | `MCPUpdateParams` | Update configuration parameters | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — Updated MCP server configuration with all details **Example** ```typescript // Update server name only const updatedServer = await composio.experimental.mcp.update("mcp_12345", { name: "My Updated MCP Server" }); // Update toolkits and tools const serverWithNewTools = await composio.experimental.mcp.update("mcp_12345", { toolkits: [ { toolkit: "github", authConfigId: "auth_abc123", allowedTools: ["GITHUB_CREATE_ISSUE", "GITHUB_LIST_REPOS"] }, { toolkit: "slack", authConfigId: "auth_xyz789", allowedTools: ["SLACK_SEND_MESSAGE", "SLACK_LIST_CHANNELS"] } ] }); // Update connection management setting const serverWithManualAuth = await composio.experimental.mcp.update("mcp_12345", { name: "Manual Auth Server", manuallyManageConnections: true }); // Complete update example const fullyUpdatedServer = await composio.experimental.mcp.update("mcp_12345", { name: "Production MCP Server", toolkits: [ { toolkit: "gmail", authConfigId: "auth_gmail_prod", } ], allowedTools: ["GMAIL_SEND_EMAIL", "GMAIL_FETCH_EMAILS"] manuallyManageConnections: false }); console.log("Updated server:", fullyUpdatedServer.name); console.log("New tools:", fullyUpdatedServer.allowedTools); ``` *** --- # RemoteFile (/reference/sdk-reference/typescript/remote-file) # Usage Access this class through the `composio.remoteFile` property: ```typescript const composio = new Composio({ apiKey: 'your-api-key' }); const result = await composio.remoteFile.list(); ``` # Properties | Name | Type | Description | | -------------------- | -------- | -------------------------------------------------------- | | `downloadUrl` | `string` | Presigned URL for downloading the file | | `expiresAt` | `string` | ISO 8601 timestamp when the download URL expires | | `mountRelativePath` | `string` | Relative path within the mount (e.g. "report.pdf") | | `sandboxMountPrefix` | `string` | Absolute mount path inside the sandbox (e.g. /mnt/files) | # Methods ## blob() Fetches the file content as a Blob. ```typescript async blob(): Promise ``` **Returns** `Promise` — The file content as a Blob *** ## buffer() Fetches the file content as a buffer. ```typescript async buffer(): Promise ``` **Returns** `Promise` — The file content as a Uint8Array *** ## save() Downloads and saves the file to the local filesystem. Requires a Node.js runtime with file system support (not available in Cloudflare Workers/Edge). ```typescript async save(path?: string): Promise ``` **Parameters** | Name | Type | Description | | ------- | -------- | --------------------------------------------------------------------------------------------------------------------- | | `path?` | `string` | Local path to save the file. If omitted, saves to the Composio temp directory using the filename from the mount path. | **Returns** `Promise` — The absolute path where the file was saved *** ## text() Fetches the file content as UTF-8 text. ```typescript async text(): Promise ``` **Returns** `Promise` — The file content as a string *** ## parse() Parses an API response (snake\_case) and returns a RemoteFile instance. ```typescript parse(data: unknown): RemoteFile ``` **Parameters** | Name | Type | Description | | ------ | --------- | -------------------------------------- | | `data` | `unknown` | Raw API response with snake\_case keys | **Returns** `RemoteFile` — A RemoteFile instance *** --- # Session files (/reference/sdk-reference/typescript/session-files) # Methods ## delete() Deletes a file or directory at the specified path on the session's file mount. Removes the file or directory from the virtual filesystem. Use with caution: deletion is typically irreversible. Ensure the path exists and is intended for removal. ```typescript async delete(remotePath: string, options?: ToolRouterSessionFilesMountDeleteOptions): Promise ``` **Parameters** | Name | Type | Description | | ------------ | ------------------------------------------ | --------------------------------------------------------- | | `remotePath` | `string` | The path of the file or directory to delete on the mount. | | `options?` | `ToolRouterSessionFilesMountDeleteOptions` | Optional configuration for the delete operation. | **Returns** `Promise` — Confirmation of deletion (implementation-specific). **Example** ```typescript const session = await composio.toolRouter.use('session_123'); await session.experimental.files.delete('/temp/cache.json'); ``` ```typescript // Delete from a custom mount await session.experimental.files.delete('/old-backup', { mountId: 'custom-mount', }); ``` *** ## download() Downloads a file from the session's file mount to the local filesystem. Retrieves a file stored in the session's virtual filesystem (e.g., one produced by a tool or previously uploaded) and saves it to the specified local path. ```typescript async download(filePath: string, options?: ToolRouterSessionFilesMountDownloadOptions): Promise ``` **Parameters** | Name | Type | Description | | ---------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `filePath` | `string` | The path of the file on the mount to download, or the local path where the file should be saved (implementation-specific). | | `options?` | `ToolRouterSessionFilesMountDownloadOptions` | Optional configuration for the download. | **Returns** `Promise` — The downloaded file data or path (implementation-specific). **Example** ```typescript const session = await composio.toolRouter.use('session_123'); const result = await session.experimental.files.download('/output/report.pdf'); ``` ```typescript // Download from a custom mount await session.experimental.files.download('/exports/data.json', { mountId: 'custom-mount', }); ``` *** ## list() Lists files and directories at the specified path on the session's file mount. Use this to browse the virtual filesystem attached to the tool router session. The path is relative to the mount root (e.g., `"/"` for root, `"/documents"` for a subdirectory). Supports cursor-based pagination via `cursor` and `limit` options. ```typescript async list(options?: ToolRouterSessionFilesMountListOptions): Promise ``` **Parameters** | Name | Type | Description | | ---------- | ---------------------------------------- | ---------------------------------------------- | | `options?` | `ToolRouterSessionFilesMountListOptions` | Optional configuration for the list operation. | **Returns** `Promise` — List of files with nextCursor for pagination. **Example** ```typescript const session = await composio.toolRouter.use('session_123'); const { items, nextCursor } = await session.experimental.files.list({ path: '/' }); ``` ```typescript // Paginated listing let result = await session.experimental.files.list({ path: '/', limit: 10 }); while (result.nextCursor) { result = await session.experimental.files.list({ path: '/', cursor: result.nextCursor, limit: 10 }); } ``` *** ## upload() Uploads a file to the session's file mount. Accepts a file path (local or URL), a native File object, or a raw buffer. The file is stored in the virtual filesystem associated with the tool router session. ```typescript async upload(input: string | File | ArrayBuffer | Uint8Array, options?: ToolRouterSessionFilesMountUploadOptions): Promise ``` **Parameters** | Name | Type | Description | | | ---------- | --------------------------------------------- | ---------------------------------------------------------------------- | ------------ | | `input` | `string \| File \| ArrayBuffer \| Uint8Array` | File path (string), native File, or raw buffer (ArrayBuffer | Uint8Array). | | `options?` | `ToolRouterSessionFilesMountUploadOptions` | Optional configuration. When passing a buffer, remotePath is required. | | **Returns** `Promise` — Metadata about the uploaded file. **Example** ```typescript // From file path (local or URL) await session.experimental.files.upload('/path/to/report.pdf'); await session.experimental.files.upload('https://example.com/file.pdf'); ``` ```typescript // From native File (e.g. from input[type=file]) await session.experimental.files.upload(fileInput.files[0]); ``` ```typescript // From raw buffer await session.experimental.files.upload(buffer, { remotePath: 'data.json', mimetype: 'application/json' }); ``` *** --- # Session (/reference/sdk-reference/typescript/session) # Properties | Name | Type | Description | | --------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `configVersion` | `number` | | | `experimental` | `SessionExperimental` | | | `mcp` | `\{ headers?: Record; type: 'http' \| 'sse'; url: string \}` | Hosted MCP endpoint (`session.mcp.url` / `session.mcp.headers`). Exists on every session at runtime, but only surfaced in the type when the session is created with `\{ mcp: true \}` (which returns `Session`); the default `SessionWithoutMcp` omits `mcp`, so MCP is an explicit opt-in. See [https://docs.composio.dev/docs/sessions-via-mcp](https://docs.composio.dev/docs/sessions-via-mcp) | | `preload` | `Preload` | | | `sandbox` | `Workbench` | Resolved sandbox (code-execution) config returned by the API. `enable` defaults to `true` server-side. | | `sessionId` | `string` | | | `warnings` | `Warning[]` | | # Methods ## authorize() Initiate an authorization flow for a toolkit. Returns a ConnectionRequest with a redirect URL for the user. Pass `experimental: { accountType: 'SHARED', aclConfigForShared }` to create a SHARED connection with a per-user ACL in one flow. Default behaviour (omit the block) creates a PRIVATE connection. Experimental — shape may change in future releases. `aclConfigForShared` is validated against the same caps as `composio.connectedAccounts.link()` (≤1000 entries per list, each `userId` 1..256 characters). Invalid input throws `ValidationError` at the SDK boundary. ```typescript async authorize(toolkit: string, options?: { callbackUrl?: string; alias?: string; experimental?: { accountType?: ConnectedAccountType; aclConfigForShared?: ConnectedAccountAclConfig; }; }, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `toolkit` | `string` | | `options?` | `\{ callbackUrl?: string; alias?: string; experimental?: \{ accountType?: ConnectedAccountType; aclConfigForShared?: ConnectedAccountAclConfig; \}; \}` | | `requestOptions?` | `ComposioRequestOptions` | **Returns** `Promise` *** ## customToolkits() List all custom toolkits registered in this session. Returns toolkits with their tools showing final slugs. ```typescript customToolkits(): RegisteredCustomToolkit[] ``` **Returns** `RegisteredCustomToolkit[]` — Array of registered custom toolkits *** ## customTools() List all custom tools registered in this session. Returns tools with their final slugs, schemas, and resolved toolkit. ```typescript customTools(options?: { toolkit?: string }): RegisteredCustomTool[] ``` **Parameters** | Name | Type | | ---------- | ------------------------ | | `options?` | `\{ toolkit?: string \}` | **Returns** `RegisteredCustomTool[]` — Array of registered custom tools *** ## delete() Delete this session. Deleted sessions immediately stop being retrievable or executable. Deleting an already-deleted session surfaces the backend 404. ```typescript async delete(requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | | ----------------- | ------------------------ | | `requestOptions?` | `ComposioRequestOptions` | **Returns** `Promise` *** ## execute() Execute a tool within the session. For custom tools, accepts the original slug (e.g. "GREP") or the full slug (e.g. "LOCAL\_GREP"). Custom tools are executed in-process; remote tools are sent to the Composio backend. ```typescript async execute(toolSlug: string, arguments_?: Record, options?: ToolRouterSessionExecuteOptions, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | --------------------------------- | -------------------------- | | `toolSlug` | `string` | The tool slug to execute | | `arguments_?` | `Record` | Optional tool arguments | | `options?` | `ToolRouterSessionExecuteOptions` | Optional execution options | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The tool execution result *** ## proxyExecute() Proxy an API call through Composio's auth layer using the session's connected account. The backend resolves the connected account from the toolkit within the session. ```typescript async proxyExecute(params: SessionProxyExecuteParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | --------------------------- | -------------------------------------------------------------------------------- | | `params` | `SessionProxyExecuteParams` | Proxy request parameters (toolkit, endpoint, method, body, headers/query params) | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The proxied API response with status, data, headers *** ## search() Search for tools by semantic use case. Returns relevant tools for the given query with schemas and guidance. ```typescript async search(params: { query: string; toolkits?: string[]; }, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | | ----------------- | ------------------------------------------- | | `params` | `\{ query: string; toolkits?: string[]; \}` | | `requestOptions?` | `ComposioRequestOptions` | **Returns** `Promise` *** ## toolkits() Query the connection state of toolkits in the session. Supports pagination and filtering by toolkit slugs. ```typescript async toolkits(options?: ToolRouterToolkitsOptions, requestOptions?: ComposioRequestOptions): Promise<{ cursor: string | undefined; items: { connection?: { authConfig?: ... | ...; connectedAccount?: { id: ...; status: ... }; isActive: boolean }; isNoAuth: boolean; logo?: string; name: string; slug: string }[]; totalPages: number }> ``` **Parameters** | Name | Type | | ----------------- | --------------------------- | | `options?` | `ToolRouterToolkitsOptions` | | `requestOptions?` | `ComposioRequestOptions` | **Returns** `Promise<\{ cursor: string \| undefined; items: \{ connection?: \{ authConfig?: ... \| ...; connectedAccount?: \{ id: ...; status: ... \}; isActive: boolean \}; isNoAuth: boolean; logo?: string; name: string; slug: string \}[]; totalPages: number \}>` *** ## tools() Get the tools available in the session, formatted for your AI framework. Requires a provider to be configured in the Composio constructor. When custom tools are bound to the session, execution of COMPOSIO\_MULTI\_EXECUTE\_TOOL is intercepted: local tools are executed in-process, remote tools are sent to the backend. ```typescript async tools(modifiers?: SessionMetaToolOptions, requestOptions?: ComposioRequestOptions): Promise> ``` **Parameters** | Name | Type | | ----------------- | ------------------------ | | `modifiers?` | `SessionMetaToolOptions` | | `requestOptions?` | `ComposioRequestOptions` | **Returns** `Promise>` *** ## update() Partially update the session configuration. Only the fields provided will be changed; omitted fields are preserved. Mutates this session's `configVersion`, `preload`, and `warnings` in-place. ```typescript async update(config: ToolRouterUpdateSessionConfig, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | | ----------------- | ------------------------------- | | `config` | `ToolRouterUpdateSessionConfig` | | `requestOptions?` | `ComposioRequestOptions` | **Returns** `Promise` *** --- # Sessions (/reference/sdk-reference/typescript/sessions) # Usage Access this class through the `composio.sessions` property: ```typescript const composio = new Composio({ apiKey: 'your-api-key' }); const result = await composio.sessions.list(); ``` # Methods ## create() Creates a new tool router session for a user. Use `sessionPreset: SessionPreset.DIRECT_TOOLS` when all needed tools should be exposed directly; see `ToolRouterCreateSessionConfig`. **Overload 1** ```typescript async create(userId: string, config: ToolRouterCreateSessionConfig & { mcp: true }, requestOptions?: ComposioRequestOptions): Promise> ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------------------------------- | ----------------------------------------------------------------------- | | `userId` | `string` | \{string} The user id to create the session for | | `config` | `ToolRouterCreateSessionConfig & \{ mcp: true \}` | \{ToolRouterCreateSessionConfig} The config for the tool router session | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise>` — The tool router session **Overload 2** ```typescript async create(userId: string, config?: ToolRouterCreateSessionConfig, requestOptions?: ComposioRequestOptions): Promise> ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------------- | ----------------------------------------------------------------------- | | `userId` | `string` | \{string} The user id to create the session for | | `config?` | `ToolRouterCreateSessionConfig` | \{ToolRouterCreateSessionConfig} The config for the tool router session | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise>` — The tool router session **Example** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const session = await composio.sessions.create('user_123', { toolkits: ['gmail'], manageConnections: true, experimental: { customTools: [myCustomTool], customToolkits: [myToolkit], }, }); ``` *** ## delete() Delete a tool router session by ID. Deleted sessions immediately stop being retrievable or executable. Deleting a missing or already-deleted session surfaces the backend 404. ```typescript async delete(id: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | | ----------------- | ------------------------ | | `id` | `string` | | `requestOptions?` | `ComposioRequestOptions` | **Returns** `Promise` *** ## use() Use an existing session **Overload 1** ```typescript async use(id: string, options: { customTools?: CustomTool[]; customToolkits?: CustomToolkit[]; mcp: true }, requestOptions?: ComposioRequestOptions): Promise> ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------------------------------------------------------------- | -------------------------------------- | | `id` | `string` | \{string} The id of the session to use | | `options` | `\{ customTools?: CustomTool[]; customToolkits?: CustomToolkit[]; mcp: true \}` | | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise>` — The tool router session **Overload 2** ```typescript async use(id: string, options?: { customTools?: CustomTool[]; customToolkits?: CustomToolkit[]; mcp?: boolean }, requestOptions?: ComposioRequestOptions): Promise> ``` **Parameters** | Name | Type | Description | | ----------------- | ----------------------------------------------------------------------------------- | -------------------------------------- | | `id` | `string` | \{string} The id of the session to use | | `options?` | `\{ customTools?: CustomTool[]; customToolkits?: CustomToolkit[]; mcp?: boolean \}` | | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise>` — The tool router session **Example** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const id = 'session_123'; const session = await composio.sessions.use(id); console.log(session.mcp.url); console.log(session.mcp.headers); ``` *** --- # Toolkits (/reference/sdk-reference/typescript/toolkits) # Usage Access this class through the `composio.toolkits` property: ```typescript const composio = new Composio({ apiKey: 'your-api-key' }); const result = await composio.toolkits.list(); ``` # Methods ## authorize() Authorizes a user to use a toolkit. This method will create an auth config if one doesn't exist and initiate a connection request. ```typescript async authorize(userId: string, toolkitSlug: string, authConfigId?: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ------------------------------------ | | `userId` | `string` | The user id of the user to authorize | | `toolkitSlug` | `string` | The slug of the toolkit to authorize | | `authConfigId?` | `string` | | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The connection request object **Example** ```typescript const connectionRequest = await composio.toolkits.authorize(userId, 'github'); ``` *** ## get() Retrieves a specific toolkit by its slug identifier. **Overload 1** ```typescript async get(slug: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ----------------------------------------------------- | | `slug` | `string` | The unique slug identifier of the toolkit to retrieve | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The toolkit object with detailed information **Overload 2** ```typescript async get(query?: ToolkitListParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | --------------------------------------- | | `query?` | `ToolkitListParams` | The query parameters to filter toolkits | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — A paginated list of toolkits matching the query criteria **Example** ```typescript // Get a specific toolkit const githubToolkit = await composio.toolkits.get('github'); console.log(githubToolkit.name); // GitHub console.log(githubToolkit.authConfigDetails); // Authentication configuration details ``` *** ## getAuthConfigCreationFields() Retrieves the fields required for creating an auth config for a toolkit. ```typescript async getAuthConfigCreationFields(toolkitSlug: string, authScheme: AuthSchemeType, options: { requiredOnly?: boolean }): Promise ``` **Parameters** | Name | Type | Description | | ------------- | ------------------------------ | -------------------------------------------------- | | `toolkitSlug` | `string` | The slug of the toolkit to retrieve the fields for | | `authScheme` | `AuthSchemeType` | The auth scheme to retrieve the fields for | | `options` | `\{ requiredOnly?: boolean \}` | | **Returns** `Promise` — The fields required for creating an auth config *** ## getConnectedAccountInitiationFields() Retrieves the fields required for initiating a connected account for a toolkit. ```typescript async getConnectedAccountInitiationFields(toolkitSlug: string, authScheme: AuthSchemeType, options: { requiredOnly?: boolean }): Promise ``` **Parameters** | Name | Type | Description | | ------------- | ------------------------------ | -------------------------------------------------- | | `toolkitSlug` | `string` | The slug of the toolkit to retrieve the fields for | | `authScheme` | `AuthSchemeType` | The auth scheme to retrieve the fields for | | `options` | `\{ requiredOnly?: boolean \}` | | **Returns** `Promise` — The fields required for initiating a connected account *** ## listCategories() Retrieves all toolkit categories available in the Composio SDK. This method fetches the complete list of categories from the Composio API and transforms the response to use camelCase property naming. ```typescript async listCategories(requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | | ----------------- | ------------------------ | | `requestOptions?` | `ComposioRequestOptions` | **Returns** `Promise` — The list of toolkit categories **Example** ```typescript // Get all toolkit categories const categories = await composio.toolkits.listCategories(); console.log(categories.items); // Array of category objects ``` *** --- # Tools (/reference/sdk-reference/typescript/tools) # Usage Access this class through the `composio.tools` property: ```typescript const composio = new Composio({ apiKey: 'your-api-key' }); const result = await composio.tools.list(); ``` # Methods ## execute() Executes a given tool with the provided parameters. This method calls the Composio API to execute the tool and returns the response. **Version Control:** By default, manual tool execution requires a specific toolkit version. If the version resolves to "latest", the execution will throw a `ComposioToolVersionRequiredError` unless `dangerouslySkipVersionCheck` is set to `true`. This helps prevent unexpected behavior when new toolkit versions are released. ```typescript async execute(slug: string, body: ToolExecuteParams, options?: ExecuteToolModifiers & ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ---------- | ----------------------------------------------- | --------------------------------------- | | `slug` | `string` | The slug/ID of the tool to be executed | | `body` | `ToolExecuteParams` | The parameters to be passed to the tool | | `options?` | `ExecuteToolModifiers & ComposioRequestOptions` | Optional modifiers and request options | **Returns** `Promise` — The response from the tool execution **Example** ```typescript const result = await composio.tools.execute('GITHUB_GET_REPOS', { userId: 'default', version: '20250909_00', arguments: { owner: 'composio' } }); ``` ```typescript const result = await composio.tools.execute('HACKERNEWS_GET_USER', { userId: 'default', arguments: { userId: 'pg' }, dangerouslySkipVersionCheck: true // Allows execution with "latest" version }); ``` ```typescript // If toolkitVersions are set during Composio initialization, no need to pass version const composio = new Composio({ toolkitVersions: { github: '20250909_00' } }); const result = await composio.tools.execute('GITHUB_GET_REPOS', { userId: 'default', arguments: { owner: 'composio' } }); ``` ```typescript const result = await composio.tools.execute('GITHUB_GET_ISSUES', { userId: 'default', version: '20250909_00', arguments: { owner: 'composio', repo: 'sdk' } }, { beforeExecute: ({ toolSlug, toolkitSlug, params }) => { console.log(`Executing ${toolSlug} from ${toolkitSlug}`); return params; }, afterExecute: ({ toolSlug, toolkitSlug, result }) => { console.log(`Completed ${toolSlug}`); return result; } }); ``` ```typescript const result = await composio.tools.execute('HACKERNEWS_GET_FRONTPAGE', { userId: 'default', arguments: {}, dangerouslySkipVersionCheck: true, }, { signal: AbortSignal.timeout(5_000) }); ``` *** ## executeSessionTool() Executes a tool based on a tool router session. ```typescript async executeSessionTool(toolSlug: string, body: ToolExecuteMetaParams, modifiers?: SessionExecuteMetaModifiers, tool?: Tool, options?: ToolRouterSessionExecuteOptions, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | --------------------------------- | ------------------------------------------------------------------- | | `toolSlug` | `string` | The slug of the tool to execute | | `body` | `ToolExecuteMetaParams` | The execution parameters | | `modifiers?` | `SessionExecuteMetaModifiers` | The modifiers to apply to the tool | | `tool?` | `Tool` | Optional tool schema used to resolve toolkit metadata for modifiers | | `options?` | `ToolRouterSessionExecuteOptions` | | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The response from the tool execution *** ## get() Get a list of tools from Composio based on filters. This method fetches the tools from the Composio API and wraps them using the provider. **Overload 1** ```typescript async get(userId: string, filters: ToolListParams, options?: ProviderOptions & ComposioRequestOptions): Promise> ``` **Parameters** | Name | Type | Description | | ---------- | ------------------------------------------ | ----------------------------------------------- | | `userId` | `string` | The user id to get the tools for | | `filters` | `ToolListParams` | The filters to apply when fetching tools | | `options?` | `ProviderOptions & ComposioRequestOptions` | Provider options, modifiers, and/or AbortSignal | **Returns** `Promise>` — The wrapped tools collection **Overload 2** ```typescript async get(userId: string, slug: string, options?: ProviderOptions & ComposioRequestOptions): Promise> ``` **Parameters** | Name | Type | Description | | ---------- | ------------------------------------------ | -------------------------------------------------------- | | `userId` | `string` | The user id to get the tool for | | `slug` | `string` | The slug of the tool to fetch | | `options?` | `ProviderOptions & ComposioRequestOptions` | Optional provider options including modifiers and signal | **Returns** `Promise>` — The wrapped tool **Example** ```typescript // Get tools from the GitHub toolkit const tools = await composio.tools.get('default', { toolkits: ['github'], limit: 10 }); // Timeout a slow search after 5s const emailTools = await composio.tools.get('default', { search: 'send email', }, { signal: AbortSignal.timeout(5_000) }); ``` *** ## getInput() Fetches the input parameters for a given tool. This method is used to get the input parameters for a tool before executing it. ```typescript async getInput(slug: string, body: ToolGetInputParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | --------------------------------------- | | `slug` | `string` | The ID of the tool to find input for | | `body` | `ToolGetInputParams` | The parameters to be passed to the tool | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The input parameters schema for the specified tool **Example** ```typescript // Get input parameters for a specific tool const inputParams = await composio.tools.getInput('GITHUB_CREATE_ISSUE', { userId: 'default' }); console.log(inputParams.schema); ``` *** ## getRawComposioToolBySlug() Retrieves a specific tool by its slug from the Composio API. This method fetches a single tool in raw format without provider-specific wrapping, providing direct access to the tool's schema and metadata. Tool versions are controlled at the Composio SDK initialization level through the `toolkitVersions` configuration. Local experimental custom tools are session-scoped; attach them when creating or reusing a Tool Router session, then use `session.tools()`, `session.customTools()`, or `session.execute()`. ```typescript async getRawComposioToolBySlug(slug: string, options?: ToolRetrievalOptions, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | -------------------------------------------------------------- | | `slug` | `string` | The unique identifier of the tool (e.g., 'GITHUB\_GET\_REPOS') | | `options?` | `ToolRetrievalOptions` | Optional configuration for tool retrieval | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The requested tool with its complete schema and metadata **Example** ```typescript // Get a tool by slug const tool = await composio.tools.getRawComposioToolBySlug('GITHUB_GET_REPOS'); console.log(tool.name, tool.description); // Get a tool with schema transformation const customizedTool = await composio.tools.getRawComposioToolBySlug( 'SLACK_SEND_MESSAGE', { modifySchema: ({ toolSlug, toolkitSlug, schema }) => { return { ...schema, description: `Enhanced ${schema.description} with custom modifications`, customMetadata: { lastModified: new Date().toISOString(), toolkit: toolkitSlug } }; } } ); // Access tool properties const githubTool = await composio.tools.getRawComposioToolBySlug('GITHUB_CREATE_ISSUE'); console.log({ slug: githubTool.slug, name: githubTool.name, toolkit: githubTool.toolkit?.name, version: githubTool.version, availableVersions: githubTool.availableVersions, inputParameters: githubTool.inputParameters }); ``` *** ## getRawComposioTools() Lists Composio API tools available to the SDK. This method fetches remote Composio tools from the API in raw format. The response can be filtered and modified as needed. Local experimental custom tools are session-scoped; attach them when creating or reusing a Tool Router session, then use `session.tools()`, `session.customTools()`, or `session.execute()`. It provides access to the underlying tool data without provider-specific wrapping. ```typescript async getRawComposioTools(query: ToolListParams, options?: SchemaModifierOptions, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ----------------------------------------------- | | `query` | `ToolListParams` | Query parameters to filter the tools (required) | | `options?` | `SchemaModifierOptions` | Optional configuration for tool retrieval | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — List of tools matching the query criteria **Example** ```typescript // Get tools from specific toolkits const githubTools = await composio.tools.getRawComposioTools({ toolkits: ['github'], limit: 10 }); // Get specific tools by slug const specificTools = await composio.tools.getRawComposioTools({ tools: ['GITHUB_GET_REPOS', 'HACKERNEWS_GET_USER'] }); // Get tools from specific toolkits const githubTools = await composio.tools.getRawComposioTools({ toolkits: ['github'], limit: 10 }); // Get tools with schema transformation const customizedTools = await composio.tools.getRawComposioTools({ toolkits: ['github'], limit: 5 }, { modifySchema: ({ toolSlug, toolkitSlug, schema }) => { // Add custom properties to tool schema return { ...schema, customProperty: `Modified ${toolSlug} from ${toolkitSlug}`, tags: [...(schema.tags || []), 'customized'] }; } }); // Search for tools const searchResults = await composio.tools.getRawComposioTools({ search: 'user management' }); // Get tools by authentication config const authSpecificTools = await composio.tools.getRawComposioTools({ authConfigIds: ['auth_config_123'] }); ``` *** ## getRawToolRouterSessionTools() Fetches tools exposed by a tool router session. This includes helper/meta tools plus any tools preloaded into the session. It provides access to the underlying tool data without provider-specific wrapping. ```typescript async getRawToolRouterSessionTools(sessionId: string, options?: SchemaModifierOptions, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ------------------------------------------------------------------ | | `sessionId` | `string` | \{string} The session id to get tools for | | `options?` | `SchemaModifierOptions` | \{SchemaModifierOptions} Optional configuration for tool retrieval | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The list of session tools **Example** ```typescript const sessionTools = await composio.tools.getRawToolRouterSessionTools('session_123'); console.log(sessionTools); ``` *** ## getToolsEnum() Fetches the list of all available tools in the Composio SDK. This method is mostly used by the CLI to get the list of tools. No filtering is done on the tools, the list is cached in the backend, no further optimization is required. ```typescript async getToolsEnum(requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | | ----------------- | ------------------------ | | `requestOptions?` | `ComposioRequestOptions` | **Returns** `Promise` — The complete list of all available tools with their metadata **Example** ```typescript // Get all available tools as an enum const toolsEnum = await composio.tools.getToolsEnum(); console.log(toolsEnum.items); ``` *** ## proxyExecute() Proxies a custom request to a toolkit/integration. This method allows sending custom requests to a specific toolkit or integration when you need more flexibility than the standard tool execution methods provide. ```typescript async proxyExecute(body: ToolProxyParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | --------------------------------------------------------------------------- | | `body` | `ToolProxyParams` | The parameters for the proxy request including toolkit slug and custom data | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The response from the proxied request **Example** ```typescript // Send a custom request to a toolkit const response = await composio.tools.proxyExecute({ toolkitSlug: 'github', userId: 'default', data: { endpoint: '/repos/owner/repo/issues', method: 'GET' } }); console.log(response.data); ``` *** --- # Triggers (/reference/sdk-reference/typescript/triggers) # Usage Access this class through the `composio.triggers` property: ```typescript const composio = new Composio({ apiKey: 'your-api-key' }); const result = await composio.triggers.list(); ``` # Methods ## create() Create a new trigger instance for a user If the connected account id is not provided, the first connected account for the user and toolkit will be used ```typescript async create(userId: string, slug: string, body?: TriggerInstanceUpsertParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ----------------------------- | --------------------------------------------- | | `userId` | `string` | The user id of the trigger instance | | `slug` | `string` | The slug of the trigger instance | | `body?` | `TriggerInstanceUpsertParams` | The parameters to create the trigger instance | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The created trigger instance *** ## delete() Delete a trigger instance ```typescript async delete(triggerId: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | -------------------------------- | | `triggerId` | `string` | The slug of the trigger instance | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` *** ## disable() Disable a trigger instance ```typescript async disable(triggerId: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ------------------------------ | | `triggerId` | `string` | The id of the trigger instance | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The updated trigger instance *** ## enable() Enable a trigger instance ```typescript async enable(triggerId: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ------------------------------ | | `triggerId` | `string` | The id of the trigger instance | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The updated trigger instance *** ## getType() Retrieve a trigger type by its slug for the provided version of the app Use the global toolkit versions param when initializing composio to pass a toolkitversion ```typescript async getType(slug: string, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ---------------------------- | | `slug` | `string` | The slug of the trigger type | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The trigger type object *** ## listActive() Fetch list of all the active triggers ```typescript async listActive(query?: TriggerInstanceListActiveParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | --------------------------------- | ---------------------------------------------------- | | `query?` | `TriggerInstanceListActiveParams` | The query parameters to filter the trigger instances | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — List of trigger instances **Example** ```typescript const triggers = await triggers.listActive({ authConfigIds: ['123'], connectedAccountIds: ['456'], }); ``` *** ## listEnum() Fetches the list of all the available trigger enums This method is used by the CLI where filters are not required. ```typescript async listEnum(requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | | ----------------- | ------------------------ | | `requestOptions?` | `ComposioRequestOptions` | **Returns** `Promise` *** ## listTypes() List all the trigger types ```typescript async listTypes(query?: TriggersTypeListParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ------------------------ | ------------------------------------------------ | | `query?` | `TriggersTypeListParams` | The query parameters to filter the trigger types | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The list of trigger types *** ## parse() Parse an incoming webhook HTTP request into a typed, normalized trigger payload. Dump the incoming request in and get back the parsed Composio trigger event. When `verifySecret` is provided, the request signature is verified before the payload is returned (delegating to verifyWebhook); without it, the body is parsed without verification. The `request` may be either a Fetch API `Request` (Next.js App Router, Hono, Remix) or a plain `{ body, headers }` object (Express with `express.raw`, Next.js Pages Router `req`). The signature headers (`webhook-id`, `webhook-timestamp`, `webhook-signature`) are read case-insensitively. ```typescript async parse(request: WebhookRequestLike, options?: ParseWebhookOptions): Promise ``` **Parameters** | Name | Type | Description | | ---------- | --------------------- | --------------------------------- | | `request` | `WebhookRequestLike` | The incoming webhook HTTP request | | `options?` | `ParseWebhookOptions` | Parse options | **Returns** `Promise` — The parsed (and optionally verified) webhook payload **Example** ```typescript // Express with express.raw (verify the signature) app.post('/webhooks/composio', express.raw({ type: 'application/json' }), async (req, res) => { try { const result = await composio.triggers.parse(req, { verifySecret: process.env.COMPOSIO_WEBHOOK_SECRET, }); console.log('Trigger:', result.payload.triggerSlug); console.log('Event data:', result.payload.payload); res.sendStatus(200); } catch (error) { res.sendStatus(401); } }); // Express without verifying (parse only) app.post('/webhooks/composio', express.raw({ type: 'application/json' }), async (req, res) => { const result = await composio.triggers.parse(req); console.log('Trigger:', result.payload.triggerSlug); res.sendStatus(200); }); ``` ```typescript // Next.js App Router (Request) — verify the signature export async function POST(request: Request) { try { const result = await composio.triggers.parse(request, { verifySecret: process.env.COMPOSIO_WEBHOOK_SECRET, }); console.log('Trigger:', result.payload.triggerSlug); console.log('Event data:', result.payload.payload); return new Response('OK', { status: 200 }); } catch (error) { return new Response('Unauthorized', { status: 401 }); } } // Next.js App Router — parse only (no verification) export async function POST(request: Request) { const result = await composio.triggers.parse(request); console.log('Trigger:', result.payload.triggerSlug); return new Response('OK', { status: 200 }); } ``` *** ## setWebhookSubscription() Create or update the project webhook subscription used for webhook delivery. If a subscription already exists, the first subscription is updated. Otherwise a new subscription is created. By default this subscribes to V3 trigger message events. ```typescript async setWebhookSubscription(params: SetWebhookSubscriptionParams): Promise ``` **Parameters** | Name | Type | | -------- | ------------------------------ | | `params` | `SetWebhookSubscriptionParams` | **Returns** `Promise` **Example** ```typescript await composio.triggers.setWebhookSubscription({ webhookUrl: `${APP_URL}/webhooks/composio`, }); ``` *** ## subscribe() Subscribe to all the triggers ```typescript async subscribe(fn: (_data: IncomingTriggerPayload) => void, filters: TriggerSubscribeParams): Promise ``` **Parameters** | Name | Type | Description | | --------- | ----------------------------------------- | ----------------------------------------------- | | `fn` | `(_data: IncomingTriggerPayload) => void` | The function to call when a trigger is received | | `filters` | `TriggerSubscribeParams` | The filters to apply to the triggers | **Returns** `Promise` **Example** ```typescript triggers.subscribe((data) => { console.log(data); }, ); ``` *** ## unsubscribe() Unsubscribe from all the triggers ```typescript async unsubscribe(): Promise ``` **Returns** `Promise` **Example** ```typescript composio.trigger.subscribe((data) => { console.log(data); }); await triggers.unsubscribe(); ``` *** ## update() Update an existing trigger instance ```typescript async update(triggerId: string, body: TriggerInstanceManageUpdateParams, requestOptions?: ComposioRequestOptions): Promise ``` **Parameters** | Name | Type | Description | | ----------------- | ----------------------------------- | --------------------------------------------- | | `triggerId` | `string` | The Id of the trigger instance | | `body` | `TriggerInstanceManageUpdateParams` | The parameters to update the trigger instance | | `requestOptions?` | `ComposioRequestOptions` | | **Returns** `Promise` — The updated trigger instance response *** ## verifyWebhook() Verify an incoming webhook payload and signature. This method validates that the webhook request is authentic by: 1. Verifying the HMAC-SHA256 signature matches the payload using the correct signing format 2. Optionally checking that the webhook timestamp is within the tolerance window The signature is computed as: `HMAC-SHA256(${webhookId}.${webhookTimestamp}.${payload}, secret)` and is expected in the format: `v1,base64EncodedSignature` ```typescript async verifyWebhook(params: VerifyWebhookParams): Promise ``` **Parameters** | Name | Type | Description | | -------- | --------------------- | --------------------------- | | `params` | `VerifyWebhookParams` | The verification parameters | **Returns** `Promise` — The verified and parsed webhook payload with version information **Example** ```typescript // In an Express.js webhook handler app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => { try { const result = await composio.triggers.verifyWebhook({ payload: req.body.toString(), signature: req.headers['webhook-signature'] as string, webhookId: req.headers['webhook-id'] as string, webhookTimestamp: req.headers['webhook-timestamp'] as string, secret: process.env.COMPOSIO_WEBHOOK_SECRET!, }); // Process the verified payload console.log('Webhook version:', result.version); console.log('Received trigger:', result.payload.triggerSlug); res.status(200).send('OK'); } catch (error) { console.error('Webhook verification failed:', error); res.status(401).send('Unauthorized'); } }); ``` *** --- # Auth Configs (/reference/v3/api-reference/auth-configs) {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/auth-configs.mdx, not this file. */} An auth config is a blueprint that defines how a toolkit authenticates across all your users. It specifies the authentication method, the scopes your tools can request, and which credentials Composio uses to run the OAuth or token flow. A single auth config applies to every user who connects that toolkit. When a user authenticates against it, Composio creates a [connected account](/reference/api-reference/connected-accounts) that stores their tokens and links them to your user ID. Each auth config defines: * **Auth scheme**: OAuth2, API key, Bearer token, or Basic Auth * **Scopes**: what your tools are allowed to do on the user's behalf * **Credentials**: Composio's managed app, or your own OAuth client and secrets Reach for a custom auth config when you need your own branding on consent screens, custom scopes, a dedicated rate-limit quota, or a custom toolkit instance. See [managed vs custom auth](/docs/custom-app-vs-managed-app) for the decision and [how Composio handles authentication](/docs/authentication) for the full picture. # Auth schemes The `auth_scheme` on an auth config determines how users authenticate to the toolkit. Composio supports four. The schemes available for a given toolkit come from the toolkit itself. | Scheme | What it is | When it's used | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `OAUTH2` | OAuth 2.0 authorization-code flow. The user authorizes through a hosted consent screen, and Composio stores and automatically refreshes the access and refresh tokens. | Most apps with user accounts (Gmail, GitHub, Slack, Notion, and so on). Uses Composio's managed OAuth app by default; bring your own for custom branding or scopes. | | `API_KEY` | A static API key the user provides. There's no OAuth flow: the key is stored on the connected account and sent on each request. | Services that authenticate with a key, such as SendGrid, Tavily, or PostHog. | | `BEARER_TOKEN` | A bearer access token you already hold (for example, from your own OAuth flow). Composio sends it as `Authorization: Bearer ` and does not refresh it, so you keep it current. | Bringing an existing OAuth or server-to-server token into Composio, or apps that issue long-lived tokens. | | `BASIC` | HTTP Basic authentication with a username and password. | Services that use Basic Auth. | Most OAuth toolkits work out of the box with Composio managed auth. For the others you supply the credential fields. To choose or customize the scheme, see [managed vs custom auth](/docs/custom-app-vs-managed-app). These endpoints use your project API key in the `x-api-key` header. Each auth config is addressed by its `nanoid`, and you can enable or disable one without deleting it. # Endpoints --- # Connected Accounts (/reference/v3/api-reference/connected-accounts) {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/connected-accounts.mdx, not this file. */} A connected account is a single user's authorized connection to a toolkit. It stores their credentials (OAuth tokens or API keys) and links them to your user ID, so your tools can act on that user's behalf. Composio creates a connected account when a user completes the flow defined by an [auth config](/reference/api-reference/auth-configs). From there you manage its full lifecycle: * **Create or link**: start a new connection, or generate an auth link session for the user to authorize. See [manually authenticating users](/docs/manually-authenticating). * **Refresh**: renew authentication for an account whose tokens have expired. * **Enable, disable, or update**: change an account's status or metadata without removing it. * **Revoke or delete**: revoke the grant at the provider, or remove the account from Composio. Each account is addressed by its `nanoid`. List endpoints accept filters so you can find accounts by user, toolkit, or auth config. # Link auth (Composio Connect Links) A Composio Connect Link is a hosted, secure sign-in page. You create one with the create auth link session endpoint, redirect the user to the returned URL, and Composio handles the rest: the user signs in, Composio creates the connected account, and Composio stores and refreshes its tokens. Credentials never pass through your app. This works for all Composio managed connections, with no OAuth credentials to set up. By default a connected account is `PRIVATE` and usable only by its owning user. Mark one `SHARED` to let other users reach it through a per-connection access control list. See [shared connections](/docs/shared-connections). These endpoints use your project API key in the `x-api-key` header. > Shared-connection ACL fields are experimental and nested under an `experimental` block on the wire. Pin a specific SDK version if you depend on the current shape. # Endpoints --- # Files (/reference/v3/api-reference/files) {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/files.mdx, not this file. */} These endpoints handle files that tools read and write during execution. When a tool produces or consumes a file, Composio stores it in object storage and exchanges it through presigned URLs rather than streaming bytes through the API. You reach for these endpoints to: * **List files** that tools have generated, optionally filtered by app and action. * **Request an upload**: get a presigned S3 URL, `PUT` your file to it, then pass the returned reference into a tool's input. This keeps large payloads out of request bodies. Tools receive a file reference and resolve the underlying object on their side. > File uploads are a two-step flow. Call the upload-request endpoint to mint a presigned URL, then upload the file contents directly to that URL. The API never receives the raw bytes. If your agent works with files inside a session, prefer the session file mount, where the sandbox exposes uploaded files to running code. See the [remote sandbox](/docs/sandbox/remote) for the sandbox helpers (`upload_local_file`, `smart_file_extract`) that build on this storage. These endpoints use your project API key in the `x-api-key` header. # Endpoints --- # MCP (/reference/v3/api-reference/mcp) {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/mcp.mdx, not this file. */} > This API is deprecated. Use a session's MCP endpoint instead. Create a session with `composio.create(userId, { mcp: true })`, then read the hosted URL off `session.mcp.url`. See [Using sessions via MCP](/docs/sessions-via-mcp) and [migrating MCP servers to sessions](/docs/migration-guide/mcp-servers-to-sessions). The MCP API is the standalone, hosted [Model Context Protocol](https://modelcontextprotocol.io) server-management surface. It let you stand up and manage a separate server config per toolkit, then mint a per-user MCP URL that any MCP-compatible client could connect to. These endpoints create, list, update, and delete MCP servers, including custom servers spanning multiple apps, generate per-user MCP URLs, and manage per-user server instances and their connected accounts. Sessions replace this. A single `composio.create(...)` gives you the same MCP URL pattern, keyed by `user_id`, while handling tool discovery, authentication, context, and versioning for you. Your existing tools, auth configs (`ac_…`), and connected accounts carry over with no re-authentication. To pin a session to a fixed tool list the way a server did, use the direct-tools preset described in [Configuring sessions](/docs/configuring-sessions). # Endpoints --- # Organization Management (/reference/v3/api-reference/organization-management) {/* Auto-generated from OpenAPI spec. Do not edit directly. */} Organization Management API endpoints # Endpoints --- # Projects (/reference/v3/api-reference/projects) {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/projects.mdx, not this file. */} Projects are Composio's multi-tenancy primitive. Every Composio account belongs to an **organization**. Inside an organization, **projects** are isolated environments that scope your API keys, connected accounts, auth configs, and webhook configurations. Resources in one project are not accessible from another. ```mermaid graph LR ORG["Organization (org_xxx)"] --- P1["Project: Production (proj_xxx)"] ORG --- P2["Project: Staging (proj_xxx)"] ORG --- TM["Team Members"] P1 --- A1["API Keys"] P1 --- A2["Connected Accounts"] P1 --- A3["Auth Configs"] P1 --- A4["Webhook Config"] P2 --- B1["..."] ``` Common reasons to use multiple projects: * **Separate environments**: keep production and staging isolated * **Separate products**: keep resources for different apps independent * **Client isolation**: give each client their own project with separate credentials and data # Managing projects Manage projects from the [dashboard](https://dashboard.composio.dev/~/org/) or via the API using an **organization API key** (`x-org-api-key`). > Project management endpoints use the `x-org-api-key` header, not the regular `x-api-key`. Find your org API key in the dashboard under **Settings > Organization**. There is no limit on the number of projects per organization. Project names must be unique within the organization. Create a project with `should_create_api_key: true` to get an API key back in the response: ```bash curl -X POST https://backend.composio.dev/api/v3.1/org/owner/project/new \ -H "x-org-api-key: YOUR_ORG_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "my-staging-project", "should_create_api_key": true }' ``` ```json { "id": "proj_abc123xyz456", "name": "my-staging-project", "api_key": "ak_abc123xyz456" } ``` The list endpoint supports pagination with `limit` and `cursor`; getting a project by ID returns the full project object including its API keys. # Project settings Each project has settings that control security, logging, and display behavior. The project detail endpoints return current configuration for inspection. Use **Settings > Project Settings** in the [dashboard](https://dashboard.composio.dev/~/project/settings/general) to update project settings. Notable security setting: `require_mcp_api_key`, when `true`, requires MCP server requests to include a valid `x-api-key` header. This defaults to `true` for organizations created on or after March 5, 2026. # Endpoints --- # Sessions (prev Tool Router) (/reference/v3/api-reference/tool-router) {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/tool-router.mdx, not this file. */} These are Composio's session endpoints. A **session** is the runtime context your agent uses to work for one of your users: it scopes which user's connected accounts are in play, which tools are available, how authentication happens, and where execution state lives. Read [What is a session?](/docs/how-composio-works) for the full concept. > Sessions were formerly called the "tool router", which is why these endpoints live under `tool_router`. They are the same thing. In the SDK you do not call these endpoints directly. Use `composio.create(...)` to start a session and `composio.use(...)` to resume one, then call `session.tools()`, `session.execute(...)`, and `session.authorize(...)` on the returned object. Reach for the raw API when you need lower-level control: creating and patching a session config, attaching to an existing session, searching for tools, executing tools and meta tools, opening link sessions for auth, proxying authenticated requests, and reading or writing files in a session mount. See [Configuring sessions](/docs/configuring-sessions) for toolkits, auth configs, account selection, and presets. # Endpoints --- # Toolkits (/reference/v3/api-reference/toolkits) {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/toolkits.mdx, not this file. */} A toolkit is a collection of related tools for a single app, like `gmail`, `github`, or `slack`. Each toolkit groups the actions for that service, its authentication requirements, and the trigger types it exposes. Reach for these endpoints when you want to: * List the toolkits in the catalog, sorted by popularity, to browse what is available before configuring a session. * Fetch a single toolkit by `slug` for its name, logo, categories, and metadata. * Fetch several toolkits at once with the multi endpoint. * List the available toolkit categories to filter the catalog by use case. * Read the toolkits changelog to track when tools or schemas change. These endpoints authenticate with your project API key in the `x-api-key` header. > Tools within a toolkit are versioned. When you execute a tool, resolve to a known version with `toolkit_versions=latest` or a pinned dated version. See the [toolkit versioning migration guide](/docs/migration-guide/toolkit-versioning). To browse toolkits visually, see the [toolkits catalog](/toolkits). For the concepts and SDK usage, see [Tools and toolkits](/docs/how-composio-works) and [Configuring sessions](/docs/configuring-sessions). # Endpoints --- # Tools (/reference/v3/api-reference/tools) {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/tools.mdx, not this file. */} Tools are the individual executable actions inside a toolkit, like `GMAIL_SEND_EMAIL` or `GITHUB_CREATE_ISSUE`. Each tool has an input schema describing its parameters and an output schema describing what it returns. Tool slugs are SCREAMING\_SNAKE\_CASE and follow a `{TOOLKIT}_{ACTION}` pattern. Reach for these endpoints when you want to: * List or search the catalog of available tools, optionally scoped to one or more toolkits. * Fetch a single tool's input and output schema by `tool_slug` before constructing a call. * Execute a tool on behalf of a user's connected account, or generate the inputs for an execution from natural language. * Look up the OAuth scopes a set of tools requires, or send an authenticated proxy request to an app's underlying API. These endpoints authenticate with your project API key in the `x-api-key` header. > Manual tool execution requires an explicit toolkit version. Pass `toolkit_versions=latest` (or pin a dated version like `20251027_00`) so calls resolve to a known tool definition. See the [toolkit versioning migration guide](/docs/migration-guide/toolkit-versioning). For the concepts behind tools, schemas, and authentication, see [Tools and toolkits](/docs/how-composio-works). # Proxy execute Proxy execute sends an authenticated HTTP request through a toolkit's [connected account](/docs/auth-configuration/connected-accounts) without a predefined tool, and Composio injects the OAuth token, API key, or other credentials on the server side so your code never touches raw secrets. Reach for it when you need an endpoint that Composio's predefined tools do not cover, when you need a request shape (custom query parameters, field masks, or advanced filters) that a predefined tool cannot express, or when a terminal agent would otherwise hardcode a bearer token in a `curl` call. Call it with `composio.tools.proxyExecute()` in the TypeScript SDK, `composio.tools.proxy()` in the Python SDK, or `POST /api/v3.1/tools/execute/proxy` over HTTP. The `endpoint` can be an absolute URL (`https://api.example.com/v1/resource`) or a path relative to the toolkit's base URL (`/v1/resource`), `method` is the HTTP verb, `connectedAccountId` selects the account to authenticate as, `body` carries the JSON payload, and `parameters` adds extra headers and query parameters. The response forwards the upstream `status`, `headers`, and parsed `data` verbatim. ```typescript import { Composio } from '@composio/core'; const composio = new Composio({ apiKey: 'your_api_key' }); const { status, data } = await composio.tools.proxyExecute({ endpoint: '/repos/composiohq/composio/issues/1', method: 'GET', connectedAccountId: 'ca_github_user_123', parameters: [{ name: 'Accept', value: 'application/vnd.github.v3+json', in: 'header' }], }); console.log(status, data); ``` > Proxy execute rejects cross-domain requests, so the `endpoint` must resolve to the same domain as the connected account's toolkit, and you should not set the `Authorization` header yourself because Composio injects the correct credential for the account's auth scheme. This is an intentional security boundary, not a quota, so it cannot be bypassed by reshaping the request. Proxy execute is a form of [direct tool execution](/docs/sessions-vs-direct-execution): it bypasses session state, tool schemas, and modifiers. If you are building an agent, prefer [sessions](/docs/configuring-sessions), and use the proxy only for the specific API call that is not available as a tool. The full request and response schema lives in the [`POST /api/v3.1/tools/execute/proxy`](/reference/api-reference/tools/postToolsExecuteProxy) reference. # Endpoints --- # Triggers (/reference/v3/api-reference/triggers) {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/triggers.mdx, not this file. */} Triggers let you subscribe to events from a user's connected app, such as a new Gmail message, a GitHub commit, or a Slack message, and receive the event data as a structured payload at your webhook endpoint. There are two layers to understand: * A **trigger type** is a template that defines what event to listen for and what configuration it needs. For example, `GITHUB_COMMIT_EVENT` requires an `owner` and a `repo`. Each toolkit exposes its own trigger types. * A **trigger instance** is a trigger type scoped to a specific user and connected account. Creating one produces an instance with its own `ti_*` ID that you can enable, disable, or delete independently. Reach for these endpoints when you want to: * Discover the trigger types a toolkit offers, or fetch one type by `slug` to inspect its config and payload schema. * Create or update a trigger instance for a connected account with the upsert endpoint. * List active trigger instances, or enable, disable, and delete an instance by `triggerId`. These endpoints authenticate with your project API key in the `x-api-key` header. > Creating a trigger instance only registers it. To actually receive events, set a webhook URL for your project once, then route incoming events on `metadata.trigger_slug`. See [Subscribing to events](/docs/setting-up-triggers/subscribing-to-events). For the full concept overview, see [Triggers](/docs/triggers). # Endpoints --- # Webhook Endpoints (/reference/v3/api-reference/webhook-endpoints) {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/webhook-endpoints.mdx, not this file. */} Webhook endpoints are per-OAuth-app webhook ingress configurations. They define the inbound URL a provider posts events to, along with the signing secret Composio stores and uses to verify those incoming payloads. Reach for these endpoints when an OAuth app you have configured needs to deliver provider-side events into Composio. You create an endpoint, configure or update it by its `nano_id`, and store the signing secret Composio uses to authenticate inbound requests. Each endpoint is addressed by its `nano_id`. The `POST` to `/webhook_endpoints/{nano_id}` replaces the full configuration, while `PATCH` updates it in place. This is distinct from [webhook subscriptions](/reference/api-reference/webhook-subscriptions), which control where Composio delivers outbound trigger events. To verify the signature on payloads Composio sends you, see [Verifying signatures](/docs/setting-up-triggers/subscribing-to-events#verifying-signatures). To set up the trigger events those payloads carry, see [Triggers](/docs/triggers). # Endpoints --- # Webhook Subscriptions (/reference/v3/api-reference/webhook-subscriptions) {/* Auto-generated from OpenAPI spec. Edit the overview at api-overviews/webhook-subscriptions.mdx, not this file. */} Webhook subscriptions are outbound delivery configurations. They define the URL Composio posts [trigger](/docs/triggers) events to, along with the signing secret and the set of event types you want to receive. Reach for these endpoints to register where Composio should send events, to filter delivery to specific event types, and to manage the signing secret used to verify those deliveries. List the available event types with the `/webhook_subscriptions/event_types` endpoint, then create a subscription scoped to the ones you care about. Each subscription is addressed by its `id`. You can update its URL and filters with `PATCH`, delete it, and rotate its signing secret with `/webhook_subscriptions/{id}/rotate_secret` if the secret leaks. Every webhook request Composio sends includes `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers. Store the secret as `COMPOSIO_WEBHOOK_SECRET` and verify each payload before trusting it. See [Verifying signatures](/docs/setting-up-triggers/subscribing-to-events#verifying-signatures) for the SDK and manual verification flows. # Event types A subscription's `enabled_events` controls which events get delivered to its URL. Two broad families exist: * **Trigger events** like `composio.trigger.message` — payloads emitted by [triggers](/docs/triggers) you've enabled (new email, new issue, etc.). * **Lifecycle events** like `composio.connected_account.expired` — emitted when a [connected account](/docs/auth-configuration/connected-accounts) changes state. List everything you can subscribe to with the `/webhook_subscriptions/event_types` endpoint, then scope a subscription to the events you handle. # Detecting connection expiry Composio automatically refreshes OAuth tokens before they expire. But when a refresh token is revoked or expires, the connection enters an `EXPIRED` state and the user must re-authenticate. Subscribe to the `composio.connected_account.expired` event to detect this proactively, instead of waiting for a tool execution to fail. > This event is only available with [V3 webhook payloads](/docs/setting-up-triggers/subscribing-to-events#webhook-payload-versions). New organizations use V3 by default. Add `composio.connected_account.expired` to the subscription's `enabled_events`: ```bash curl -X POST https://backend.composio.dev/api/v3.1/webhook_subscriptions \ -H "X-API-KEY: " \ -H "Content-Type: application/json" \ -d '{ "webhook_url": "https://example.com/webhook", "enabled_events": [ "composio.trigger.message", "composio.connected_account.expired" ] }' ``` When a connection expires, Composio sends a webhook with the connected account details: ```json { "id": "evt_847cdfcd-d219-4f18-a6dd-91acd42ca94a", "type": "composio.connected_account.expired", "metadata": { "project_id": "pr_your-project-id", "org_id": "ok_your-org-id" }, "data": { "id": "ca_your-connected-account-id", "toolkit": { "slug": "gmail" }, "auth_config": { "id": "ac_your-auth-config-id", "auth_scheme": "OAUTH2" }, "status": "EXPIRED", "status_reason": "OAuth refresh token expired" }, "timestamp": "2026-02-06T12:00:00.000Z" } ``` Route on `type` to handle expiry alongside trigger events: **Python:** ```python from composio import Composio, WebhookEventType composio = Composio() @app.post("/webhook") async def webhook_handler(request: Request): payload = await request.json() event_type = payload.get("type") if event_type == WebhookEventType.CONNECTION_EXPIRED: account_id = payload["data"]["id"] toolkit = payload["data"]["toolkit"]["slug"] # Look up the user and send them a re-auth link session = composio.create(user_id=lookup_user(account_id)) connection_request = session.authorize(toolkit) notify_user(connection_request.redirect_url) elif event_type == WebhookEventType.TRIGGER_MESSAGE: # Handle trigger events pass return {"status": "ok"} ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); type NextApiRequest = { body: any }; type NextApiResponse = { status: (code: number) => { json: (data: any) => void } }; declare function lookupUser(accountId: string): string; declare function notifyUser(url: string): void; export default async function webhookHandler(req: NextApiRequest, res: NextApiResponse) { const payload = req.body; if (payload.type === 'composio.connected_account.expired') { const accountId = payload.data.id; const toolkit = payload.data.toolkit.slug; // Look up the user and send them a re-auth link const session = await composio.create(lookupUser(accountId)); const connectionRequest = await session.authorize(toolkit); if (connectionRequest.redirectUrl) { notifyUser(connectionRequest.redirectUrl); } } else if (payload.type === 'composio.trigger.message') { // Handle trigger events } res.status(200).json({ status: 'ok' }); } ``` > Always [verify webhook signatures](/docs/setting-up-triggers/subscribing-to-events#verifying-signatures) before processing events in production. # Endpoints --- # Toolkits --- # Toolkits (/toolkits) --- # Managed Auth (/toolkits/managed-auth) Managed auth means Composio registers and maintains the OAuth app for a toolkit, so your users can connect their accounts with no OAuth credentials for you to set up. Most popular toolkits (GitHub, Gmail, Slack, and many more) ship with managed auth and work the moment you create a session. When a tool needs an account, the [`COMPOSIO_MANAGE_CONNECTIONS`](/toolkits/meta-tools/manage_connections) meta tool returns a secure [Connect Link](/docs/tools-direct/authenticating-tools#hosted-authentication-connect-link). The user signs in there, and Composio stores and refreshes their tokens. You write no auth code. See [Authentication](/docs/authentication) for the full flow. # When you don't need to do anything For any toolkit with managed auth, you don't register an OAuth app, manage credentials, or handle token refresh. Create a session and start calling tools: **Python:** ```python from composio import Composio composio = Composio() session = composio.create(user_id="user_123") # Tools that need an account return a Connect Link automatically ``` **TypeScript:** ```typescript import { Composio } from '@composio/core'; const composio = new Composio(); const session = await composio.create("user_123"); // Tools that need an account return a Connect Link automatically ``` # When you need your own credentials You need a custom auth config when you want to: * **Bring your own OAuth app**, so users see your app name on the consent screen instead of "Composio." * **Request specific scopes** beyond Composio's defaults. * **Use a toolkit without managed auth**, where you supply an API key, bearer token, or instance details. See [Managed vs custom auth](/docs/custom-app-vs-managed-app) for the full decision guide and setup steps. You can mix the two: use your own credentials for some toolkits and managed auth for the rest. # Find a toolkit Search the list below to check whether a toolkit has managed auth. Toolkits under **Composio Managed App Available** work out of the box. Toolkits under **Requires your own credentials** need a custom auth config. # Check programmatically To check a single toolkit from a script, call the toolkit endpoint and read its managed auth schemes: ```bash curl 'https://backend.composio.dev/api/v3.1/toolkits/posthog' \ -H 'x-api-key: YOUR_API_KEY' ``` A toolkit that lists managed auth schemes in the response has managed auth. An empty list means you need to supply your own credentials. --- # Pro Tools (/toolkits/pro-tools) Some tool calls cost more to run — search APIs, code sandboxes, ML inference. We call those pro tools and price them separately. # What counts as a pro tool? - [Search APIs](/toolkits/composio_search): Composio Search, Perplexity, Exa, SerpAPI - [Code execution](/toolkits/codeinterpreter): Sandboxed runtimes like E2B } title="Web scraping & data extraction" description="Crawlers and structured extraction" /> } title="AI/ML inference" description="Hosted model calls and embeddings" /> } title="Document processing & OCR" description="PDF, image, and document parsing" /> } title="Compute-intensive operations" description="Long-running or heavy transforms" /> # Pricing Pro tool calls are roughly 3x the cost of a standard tool call. Full pricing is on the [pricing page](https://composio.dev/pricing). | Plan | Included Standard Tool Calls | Included Pro Tool Calls | Usage Based Standard Tool Calls | Usage Based Pro Tool Calls | | ------------------ | ---------------------------- | ----------------------- | ------------------------------- | -------------------------- | | Totally Free | 20k | 1k | – | – | | Ridiculously Cheap | 200k | 5k | $0.299/1k | $0.897/1k | | Serious Business | 2M | 50k | $0.249/1k | $0.747/1k | | Enterprise | Flexible | Flexible | Flexible | Flexible | # Rate limits Pro tools have lower rate limits than standard tool calls. If you need more, [contact us](mailto:billing@composio.dev). | Spending Tier | Standard Tool Calls Rate Limit | Pro Tool Calls Rate Limit | | ------------- | ------------------------------ | ------------------------- | | Free | 100/min | 1,000/hour | | Paid | 5,000/min | 10,000/hour | | Enterprise | Custom | Custom | --- # Get Tool Schemas (/toolkits/meta-tools/get_tool_schemas) {/* Auto-generated by scripts/generate-meta-tools.ts — do not edit manually */} import { MetaToolDetailServer } from '@/components/meta-tools/meta-tool-page'; --- # Meta Tools (/toolkits/meta-tools) {/* Auto-generated by scripts/generate-meta-tools.ts — do not edit manually. To change the intro prose or a tool's one-line summary, edit the template and the override map in lib/meta-tool-overrides.ts. */} import { Callout } from 'fumadocs-ui/components/callout'; Every Composio [session](/docs/how-composio-works) hands your agent a small set of meta tools instead of hundreds of raw tool definitions. The agent uses them to find the right tools for a task, connect the accounts those tools need, execute them, and process the results, all at runtime and all sharing one `session_id`. This keeps your context window small: you load a handful of meta tools, not a catalog of 500+ apps. The agent searches for what it needs when it needs it. A typical workflow runs in order: call `COMPOSIO_SEARCH_TOOLS` to discover tools and open a session, call `COMPOSIO_MANAGE_CONNECTIONS` if a toolkit is not yet connected, then run the tools with `COMPOSIO_MULTI_EXECUTE_TOOL`. Reach for the workbench and bash tools when responses are large enough to process out of context. | Tool | What it does | | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`COMPOSIO_GET_TOOL_SCHEMAS`](/toolkits/meta-tools/get_tool_schemas) | Returns the full input schema for tools you already know the slug for, so you can build schema-compliant arguments before executing | | [`COMPOSIO_MANAGE_CONNECTIONS`](/toolkits/meta-tools/manage_connections) | Checks connection status for a toolkit and returns a branded authentication link when the user needs to connect, covering OAuth, API keys, and every other auth type | | [`COMPOSIO_MULTI_EXECUTE_TOOL`](/toolkits/meta-tools/multi_execute_tool) | Executes up to 50 tools in parallel and returns structured outputs ready for immediate analysis | | [`COMPOSIO_REMOTE_BASH_TOOL`](/toolkits/meta-tools/remote_bash_tool) | Runs bash commands in a remote sandbox for file operations, data processing, and system tasks | | [`COMPOSIO_REMOTE_WORKBENCH`](/toolkits/meta-tools/remote_workbench) | Runs Python in a persistent remote sandbox to process large remote files and script bulk or repeated tool executions | | [`COMPOSIO_SEARCH_TOOLS`](/toolkits/meta-tools/search_tools) | Discovers the right tools across 500+ apps for a task and returns them with an execution plan, connection status, and the `session_id` that ties the rest of the workflow together | > These schemas are for reference only. We do not guarantee backward compatibility for parameter names or response shapes, so do not rely on them as structured type definitions in your code. --- # Manage Connections (/toolkits/meta-tools/manage_connections) {/* Auto-generated by scripts/generate-meta-tools.ts — do not edit manually */} import { MetaToolDetailServer } from '@/components/meta-tools/meta-tool-page'; --- # Multi Execute Tool (/toolkits/meta-tools/multi_execute_tool) {/* Auto-generated by scripts/generate-meta-tools.ts — do not edit manually */} import { MetaToolDetailServer } from '@/components/meta-tools/meta-tool-page'; --- # Remote Bash Tool (/toolkits/meta-tools/remote_bash_tool) {/* Auto-generated by scripts/generate-meta-tools.ts — do not edit manually */} import { MetaToolDetailServer } from '@/components/meta-tools/meta-tool-page'; --- # Remote Workbench (/toolkits/meta-tools/remote_workbench) {/* Auto-generated by scripts/generate-meta-tools.ts — do not edit manually */} import { MetaToolDetailServer } from '@/components/meta-tools/meta-tool-page'; --- # Search Tools (/toolkits/meta-tools/search_tools) {/* Auto-generated by scripts/generate-meta-tools.ts — do not edit manually */} import { MetaToolDetailServer } from '@/components/meta-tools/meta-tool-page';