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

# Approvals (/docs/approvals)

By default, your agent can run any tool in a session as soon as it decides to. You can configure the session to require the user's approval before any tool runs, such as tools that send, post or delete things on their behalf.

When your agent calls a tool that requires approval, Composio doesn't run it. It returns an elicitation that asks the user to approve the call, and runs the tool once it receives their answer.

> **Experimental**: Approvals are experimental. They need `composio` newer than 0.27.0 or `@composio/core` newer than 0.24.0.

## Choosing which tools need approval [#choosing-which-tools-need-approval]

You choose which tools need approval with their [tags](/docs/tool-tags) when you create a session. For example, this session asks the user before any tool that creates, updates or deletes something:

**Python:**

```python
session = composio.create(
    user_id="user_123",
    # Everywhere: ask before tools that create, update or delete
    tags={"require_approval": ["createHint", "updateHint", "destructiveHint"]},
)
```

**TypeScript:**

```typescript
import { Composio } from '@composio/core';
const composio = new Composio({ apiKey: 'your_api_key' });
const session = await composio.create('user_123', {
  // Everywhere: ask before tools that create, update or delete
  tags: { requireApproval: ['createHint', 'updateHint', 'destructiveHint'] },
});
```

To handle a toolkit differently, override the session-wide `tags` in `tools.<toolkit>.tags`. This session asks before the same tools everywhere, except GitHub, where it only asks before tools that delete:

**Python:**

```python
session = composio.create(
    user_id="user_123",
    # Everywhere: ask before tools that create, update or delete
    tags={"require_approval": ["createHint", "updateHint", "destructiveHint"]},
    tools={
        # GitHub: ask only before tools that delete
        "github": {"tags": {"require_approval": ["destructiveHint"]}},
    },
)
```

**TypeScript:**

```typescript
import { Composio } from '@composio/core';
const composio = new Composio({ apiKey: 'your_api_key' });
const session = await composio.create('user_123', {
  // Everywhere: ask before tools that create, update or delete
  tags: { requireApproval: ['createHint', 'updateHint', 'destructiveHint'] },
  tools: {
    // GitHub: ask only before tools that delete
    github: { tags: { requireApproval: ['destructiveHint'] } },
  },
});
```

The override replaces the session-wide `tags` for that toolkit, including any tags you block, so list everything you still want for it.

## Asking the user [#asking-the-user]

When a tool needs approval, `session.execute()` raises an error:

**Python:**

```python
from composio.exceptions import ToolInputRequiredError

arguments = {"recipient_email": "priya@acme.com", "subject": "Q3 numbers"}

try:
    session.execute("GMAIL_SEND_EMAIL", arguments=arguments)
except ToolInputRequiredError as error:
    questions = error.input_requests
    request_state = error.request_state
```

**TypeScript:**

```typescript
import { Composio, ComposioToolInputRequiredError, type ToolRouterInputRequest } from '@composio/core';
const composio = new Composio({ apiKey: 'your_api_key' });
const session = await composio.create('user_123');
const args = { recipient_email: 'priya@acme.com', subject: 'Q3 numbers' };
let questions: Record<string, ToolRouterInputRequest> = {};
let requestState: string | undefined;

try {
  await session.execute('GMAIL_SEND_EMAIL', args);
} catch (error) {
  if (error instanceof ComposioToolInputRequiredError) {
    questions = error.inputRequests;
    requestState = error.requestState;
  }
}
```

The error's `input_requests` holds the questions to ask the user, keyed by ID. Each question has a `message` describing the tool call, and a `requested_schema` describing the answer the user can give:

```json
{
  "elicit_0f2f8872eea523ed": {
    "type": "elicitation",
    "mode": "form",
    "message": "Your agent wants to run the following tool:\n\nGMAIL_SEND_EMAIL({\"recipient_email\": \"priya@acme.com\", \"subject\": \"Q3 numbers\"})",
    "requested_schema": {
      "type": "object",
      "properties": {
        "decision": {
          "type": "string",
          "title": "Decision",
          "oneOf": [
            { "const": "approve", "title": "Approve" },
            { "const": "always_approve", "title": "Always approve" },
            { "const": "deny", "title": "Deny" }
          ]
        }
      },
      "required": ["decision"]
    }
  }
}
```

Show the user the `message` and a form built from `requested_schema`. Build the form from the schema, since the choices vary between questions.

One tool execution can return several questions, for example when the agent runs several tools at once with `COMPOSIO_MULTI_EXECUTE_TOOL`. Answer all of them when you send the call again.

The questions follow the format of [MCP elicitation](https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation) requests, so you can reuse an existing elicitation UI.

## Sending the answer [#sending-the-answer]

Send the same call again with the user's answers:

**Python:**

```python
result = session.execute(
    "GMAIL_SEND_EMAIL",
    arguments=arguments,
    input_responses={
        "elicit_0f2f8872eea523ed": {
            "action": "accept",
            "content": {"decision": "approve"},
        }
    },
    request_state=request_state,
)
```

**TypeScript:**

```typescript
import { Composio } from '@composio/core';
const composio = new Composio({ apiKey: 'your_api_key' });
const session = await composio.create('user_123');
const args = { recipient_email: 'priya@acme.com', subject: 'Q3 numbers' };
declare const requestState: string | undefined;
const result = await session.execute('GMAIL_SEND_EMAIL', args, {
  inputResponses: {
    elicit_0f2f8872eea523ed: {
      action: 'accept',
      content: { decision: 'approve' },
    },
  },
  requestState,
});
```

When the user picks **Always approve**, their approval lasts for the rest of the session. Later calls to that tool on the same connected account run without asking again.

## Approvals in MCP clients [#approvals-in-mcp-clients]

Approvals use MCP [elicitation](https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation), which lets a server ask the user for input through the client. When your agent connects to the session [over MCP](/docs/sessions-via-mcp), clients that support elicitation, like Claude Code, ask the user for you. With clients that don't support elicitation, calls that need approval are denied. To run them without approval instead, set `no_elicitation_support_fallback` to `"allow"`:

**Python:**

```python
session = composio.create(
    user_id="user_123",
    tools={
        "gmail": {
            "enable": ["GMAIL_FETCH_EMAILS", "GMAIL_SEND_EMAIL"],
            "require_approval": ["GMAIL_SEND_EMAIL"],
        }
    },
    experimental={"no_elicitation_support_fallback": "allow"},
)
```

**TypeScript:**

```typescript
import { Composio } from '@composio/core';
const composio = new Composio({ apiKey: 'your_api_key' });
const session = await composio.create('user_123', {
  tools: {
    gmail: {
      enable: ['GMAIL_FETCH_EMAILS', 'GMAIL_SEND_EMAIL'],
      requireApproval: ['GMAIL_SEND_EMAIL'],
    },
  },
  experimental: { noElicitationSupportFallback: 'allow' },
});
```

## Current limitations [#current-limitations]

* **Workbench and bash scripts.** Composio uses a model to find the tools a `COMPOSIO_REMOTE_WORKBENCH` or `COMPOSIO_REMOTE_BASH_TOOL` script calls, so it can miss some. If approval must never be skipped, [disable the sandbox](/docs/configuring-sessions#disabling-the-sandbox).
* **Proxy execute.** `session.proxy_execute()` is currently disabled for a toolkit when any of its tools need approval.
* **Tags.** Rules based on [tags](/docs/tool-tags) are only as accurate as the tags, which Composio assigns on a best-effort basis. List tools explicitly for anything you must gate.
* **Providers.** The SDK's `handle_tool_calls` and framework providers don't handle approval yet. Run tool calls with `session.execute()`, as in the [full example](#full-example) below.

## Full example [#full-example]

An agent loop with Claude that asks for approval in the terminal:

**Python:**

```python
import json

import anthropic
from composio import Composio
from composio.exceptions import ToolInputRequiredError
from composio_anthropic import AnthropicProvider

composio = Composio(provider=AnthropicProvider())
client = anthropic.Anthropic()

session = composio.create(
    user_id="user_123",
    toolkits={"enable": ["gmail"]},
    tools={
        "gmail": {
            # The agent can fetch and send emails
            "enable": ["GMAIL_FETCH_EMAILS", "GMAIL_SEND_EMAIL"],
            # but needs the user's approval to send one
            "require_approval": ["GMAIL_SEND_EMAIL"],
        }
    },
)
tools = session.tools()

def ask_user(question):
    """Show one question in the terminal and return the user's answer."""
    # Describes the tool call, like which email the agent wants to send
    print(question["message"])
    # Ask for each field in requested_schema, like the decision to approve or deny
    content = {}
    for name, field in question["requested_schema"]["properties"].items():
        choices = [choice["const"] for choice in field.get("oneOf", [])]
        content[name] = input(f"{field.get('title', name)} ({'/'.join(choices)}): ")
    return {"action": "accept", "content": content}

def run_tool(block):
    """Run one tool call, asking the user whenever Composio needs their input."""
    # No answers yet on the first try
    input_responses, request_state = None, None
    while True:
        try:
            result = session.execute(
                block.name,
                arguments=block.input,
                input_responses=input_responses,
                request_state=request_state,
            )
            break
        except ToolInputRequiredError as error:
            # The tool needs approval: ask the user every question, keyed by its ID
            input_responses = {
                question_id: ask_user(question)
                for question_id, question in error.input_requests.items()
            }
            # Send this back with the answers when you retry the call
            request_state = error.request_state

    # Return the result to Claude
    return {
        "type": "tool_result",
        "tool_use_id": block.id,
        "content": json.dumps({"data": result.data, "error": result.error}),
        "is_error": result.result_type == "failed",
    }

messages = [
    {"role": "user", "content": "Email priya@acme.com with the subject 'Q3 numbers' and say the report is ready."}
]
response = client.messages.create(
    model="claude-opus-4-6", max_tokens=4096, tools=tools, messages=messages
)

# Run the tools Claude calls until it has a final answer
while response.stop_reason == "tool_use":
    messages.append({"role": "assistant", "content": response.content})
    messages.append({
        "role": "user",
        "content": [run_tool(block) for block in response.content if block.type == "tool_use"],
    })
    response = client.messages.create(
        model="claude-opus-4-6", max_tokens=4096, tools=tools, messages=messages
    )

for block in response.content:
    if block.type == "text":
        print(block.text)
```

**TypeScript:**

```typescript
import Anthropic from '@anthropic-ai/sdk';
import { AnthropicProvider } from '@composio/anthropic';
import {
  Composio,
  ComposioToolInputRequiredError,
  type ToolRouterInputRequest,
  type ToolRouterSessionExecuteOptions,
} from '@composio/core';
import * as readline from 'node:readline/promises';

const composio = new Composio({ provider: new AnthropicProvider() });
const client = new Anthropic();
const terminal = readline.createInterface({ input: process.stdin, output: process.stdout });

const session = await composio.create('user_123', {
  toolkits: { enable: ['gmail'] },
  tools: {
    gmail: {
      // The agent can fetch and send emails
      enable: ['GMAIL_FETCH_EMAILS', 'GMAIL_SEND_EMAIL'],
      // but needs the user's approval to send one
      requireApproval: ['GMAIL_SEND_EMAIL'],
    },
  },
});
const tools = await session.tools();

// Show one question in the terminal and return the user's answer
async function askUser(question: ToolRouterInputRequest) {
  // Describes the tool call, like which email the agent wants to send
  console.log(question.message);
  // Ask for each field in requestedSchema, like the decision to approve or deny
  const { properties } = question.requestedSchema as {
    properties: Record<string, { title?: string; oneOf?: { const: string }[] }>;
  };
  const content: Record<string, string> = {};
  for (const [name, field] of Object.entries(properties)) {
    const choices = field.oneOf?.map((choice) => choice.const) ?? [];
    content[name] = await terminal.question(`${field.title ?? name} (${choices.join('/')}): `);
  }
  return { action: 'accept' as const, content };
}

// Run one tool call, asking the user whenever Composio needs their input
async function runTool(block: Anthropic.ToolUseBlock): Promise {
  // No answers yet on the first try
  let answers: ToolRouterSessionExecuteOptions = {};
  while (true) {
    try {
      const result = await session.execute(block.name, block.input as Record<string, unknown>, answers);
      // Return the result to Claude
      return {
        type: 'tool_result',
        tool_use_id: block.id,
        content: JSON.stringify({ data: result.data, error: result.error }),
        is_error: result.resultType === 'failed',
      };
    } catch (error) {
      if (!(error instanceof ComposioToolInputRequiredError)) throw error;
      // The tool needs approval: ask the user every question, keyed by its ID
      const inputResponses: NonNullable = {};
      for (const [questionId, question] of Object.entries(error.inputRequests)) {
        inputResponses[questionId] = await askUser(question);
      }
      // Retry the call with the answers, and the request state if there is one
      answers = { inputResponses, requestState: error.requestState };
    }
  }
}

const messages: Anthropic.MessageParam[] = [
  { role: 'user', content: "Email priya@acme.com with the subject 'Q3 numbers' and say the report is ready." },
];
let response = await client.messages.create({
  model: 'claude-opus-4-6',
  max_tokens: 4096,
  tools,
  messages,
});

// Run the tools Claude calls until it has a final answer
while (response.stop_reason === 'tool_use') {
  messages.push({ role: 'assistant', content: response.content });
  const results: Anthropic.ToolResultBlockParam[] = [];
  for (const block of response.content) {
    if (block.type === 'tool_use') results.push(await runTool(block));
  }
  messages.push({ role: 'user', content: results });
  response = await client.messages.create({
    model: 'claude-opus-4-6',
    max_tokens: 4096,
    tools,
    messages,
  });
}

for (const block of response.content) {
  if (block.type === 'text') console.log(block.text);
}
terminal.close();
```

## Runnable examples [#runnable-examples]

Try the [TypeScript and Python approval examples](https://github.com/ComposioHQ/composio/tree/next/ts/examples/approvals) for a minimal tool call, a multi-tool batch, and an MCP client with elicitation. They use read-only Hacker News calls, so you don't need a connected account or model API key. The examples include tests for CI and unattended staging checks for approve, deny, decline, cancel, and **Always approve**.

## Next [#next]

- [Configuring sessions](/docs/configuring-sessions): Enable toolkits and tools, filter by tags, and update a session's configuration.

---

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

---

# Composio SDK — Notes for AI Code Generators

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

---

## 1. Recommended Integration: Sessions

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

### Native Tools

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

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

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

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

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

### MCP

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

---

## 2. Integration Notes

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

---

# 3. Calling the REST API directly

## REST API version

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

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

## Tool-endpoint version defaults on v3.1

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

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

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

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


---

## Terminology Migration (old → current)

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

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

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

