Tools

Executing Tools

Markdown

If you're building an agent, we recommend using sessions instead. Sessions handle tool fetching, authentication, and execution automatically. See the quickstart to get started, or Sessions vs Direct Execution to understand the tradeoffs.

LLMs on their own can only do generation. Tool calling changes that by letting them interact with external services. Instead of just drafting an email, the model can call GMAIL_SEND_EMAIL to actually send it. The tool's results feed back to the LLM, closing the loop so it can decide, act, observe, and adapt.

In Composio, every tool is a single API action—fully described with schema, parameters, and return type. Tools live inside toolkits like Gmail, Slack, or GitHub, and Composio handles authentication and user scoping.

User Scoping: All tools are scoped to a specific user - that's why every example includes a user_id. Learn how to structure User IDs in User Management. Each user must authenticate with their respective services (Gmail, Calendar, etc.) - see Authentication.

Using Chat Completions

Use the Composio SDK with providers like OpenAI, Anthropic, and Google AI. To learn how to set up these providers, see Providers.

from composio import Composio
from composio_openai import OpenAIProvider
from openai import OpenAI
from datetime import datetime

# Use a unique identifier for each user in your application
user_id = "user-k7334"

# Create composio client
composio = Composio(provider=OpenAIProvider(), api_key="your_composio_api_key")

# Create openai client
openai = OpenAI()

# Get calendar tools for this user
tools = composio.tools.get(
    user_id=user_id,
    tools=["GOOGLECALENDAR_EVENTS_LIST"]
)

# Ask the LLM to check calendar
result = openai.chat.completions.create(
    model="gpt-5.4",
    tools=tools,
    messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": f"What's on my calendar for the next 7 days?"}
    ]
)

# Handle tool calls
result = composio.provider.handle_tool_calls(user_id=user_id, response=result)
print(result)
import { Composio } from '@composio/core';
import { AnthropicProvider } from '@composio/anthropic';
import { Anthropic } from '@anthropic-ai/sdk';

// Use a unique identifier for each user in your application
const userId = 'user-k7334';

// Create anthropic client
const anthropic = new Anthropic();

// Create Composio client
const composio = new Composio({
  apiKey: "your-composio-api-key",
  provider: new AnthropicProvider(),
});

// Get calendar tools for this user
const tools = await composio.tools.get(userId, {
  tools: ['GOOGLECALENDAR_EVENTS_LIST'],
});

// Ask the LLM to check calendar
const msg = await anthropic.messages.create({
  model: 'claude-sonnet-4-6',
  tools: tools,
  messages: [
    {
      role: 'user',
      content: `What's on my calendar for the next 7 days?`,
    },
  ],
  max_tokens: 1024,
});

// Handle tool calls
const result = await composio.provider.handleToolCalls(userId, msg);
console.log('Results:', JSON.stringify(result, null, 2));

Using Agentic Frameworks

Agentic frameworks automatically handle the tool execution loop. Composio provides support for frameworks like this by making sure the tools are formatted into the correct objects for the agentic framework to execute.

import asyncio
from agents import Agent, Runner
from composio import Composio
from composio_openai_agents import OpenAIAgentsProvider

# Use a unique identifier for each user in your application
user_id = "user-k7334"

# Initialize Composio toolset
composio = Composio(provider=OpenAIAgentsProvider(), api_key="your_composio_api_key")

# Get all tools for the user
tools = composio.tools.get(
    user_id=user_id,
    toolkits=["COMPOSIO_SEARCH"],
)

# Create an agent with the tools
agent = Agent(
    name="Deep Researcher",
    instructions="You are an investigative journalist.",
    tools=tools,
)

async def main():
    result = await Runner.run(
        starting_agent=agent,
        input=("Do a thorough DEEP research on Golden Gate Bridge"),
    )
    print(result.final_output)

# Run the agent
asyncio.run(main())
import { Composio } from '@composio/core';
import { generateText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { VercelProvider } from '@composio/vercel';

// Use a unique identifier for each user in your application
const userId = 'user-k7334';

// Initialize Composio toolset
const composio = new Composio({
  apiKey: process.env.COMPOSIO_API_KEY,
  provider: new VercelProvider(),
});

// Get all tools for the user
const tools = await composio.tools.get(userId, {
  toolkits: ['HACKERNEWS_GET_LATEST_POSTS'],
  limit: 10,
});

// Generate text with tool use
const { text } = await generateText({
  model: anthropic('claude-sonnet-4-6'),
  messages: [
    {
      role: 'user',
      content: 'Do a thorough DEEP research on the top articles on Hacker News about Composio',
    },
  ],
  tools,
});

console.log(text);

Direct Tool Execution

If you just want to call a tool without using any framework or LLM provider, you can use the execute method directly.

Finding tool parameters and types:

Platform UI: Auth Configs → Select your toolkit → Tools & Triggers → Select the tool to see its required and optional parameters

CLI: For Python and TypeScript projects, run composio generate to generate types. Learn more →

from composio import Composio

user_id = "user-k7334"
# Configure toolkit versions at SDK level
composio = Composio(
    api_key="your_composio_key",
    toolkit_versions={"github": "20251027_00"}
)

# Find available arguments for any tool in the Composio dashboard
result = composio.tools.execute(
    "GITHUB_LIST_STARGAZERS",
    user_id=user_id,
    arguments={"owner": "ComposioHQ", "repo": "composio", "page": 1, "per_page": 5}
)
print(result)
import { Composio } from "@composio/core";

const userId = "user-k7334";
// Configure toolkit versions at SDK level
const composio = new Composio({
    apiKey: "your_composio_key",
    toolkitVersions: { github: "20251027_00" }
});

// Find available arguments for any tool in the Composio dashboard
const result = await composio.tools.execute("GITHUB_LIST_STARGAZERS", {
  userId,
  arguments: {
    "owner": "ComposioHQ",
    "repo": "composio",
    "page": 1,
    "per_page": 5
  },
});
console.log('GitHub stargazers:', JSON.stringify(result, null, 2));

The examples above configure toolkit versions at SDK initialization. You can also pass versions per-execution or use environment variables. See toolkit versioning for all configuration options.

Choosing a version strategy: Use latest when tool outputs are consumed by an LLM or AI agent — agents adapt to schema changes dynamically. Pin to a specific version when your code parses outputs programmatically. See choosing between latest and a pinned version.

Proxy Execute

You can proxy requests to any supported toolkit API and let Composio inject the authentication state. This is useful when you need an API endpoint that isn't available as a predefined tool.

Proxy execute requires auth context. Pass connected_account_id / connectedAccountId, or provide custom_connection_data if you're supplying auth manually.

The endpoint can be a relative path or absolute URL. Relative paths are resolved against the connected account's base URL. Absolute URLs are only accepted when they use the same scheme and registrable domain as that base URL. Cross-subdomain requests on the same registrable domain still work.

Prefer relative endpoints when possible. They resolve against the connected account's base URL and avoid same-domain validation issues.

# Send a proxy request to the endpoint
response = composio.tools.proxy(
    endpoint="/repos/composiohq/composio/issues/1",
    method="GET",
    connected_account_id="ca_jI6********",  # use connected account for github
    parameters=[
        {
            "name": "Accept",
            "value": "application/vnd.github.v3+json",
            "type": "header",
        },
    ],
)

print(response)
// Send a proxy request to the endpoint
const { data } = await composio.tools.proxyExecute({
    endpoint:'/repos/composiohq/composio/issues/1',
    method: 'GET',
    connectedAccountId: 'ca_jI*****', // use connected account for github
    parameters:[
        {
            "name": "Accept",
            "value": "application/vnd.github.v3+json",
            "in": "header",
        },
    ],
});

console.log(data);

Need an API that isn't supported by any Composio toolkit, or want to extend an existing one? Learn how to create custom tools.

Automatic File Handling

Automatic file handling is off by default. Enable it with dangerouslyAllowAutoUploadDownloadFiles: true (TypeScript) or dangerously_allow_auto_upload_download_files=True (Python). Once enabled, pass file paths to tools that accept files and get local paths back from tools that return files.

File Upload

Pass local file paths, URLs, or File objects to tools that accept files. The SDK uploads the file to Composio's storage and converts it to the format the tool expects — you don't need to construct the file metadata object (s3key, name, mimetype) yourself.

composio = Composio(
    api_key="your_composio_key",
    dangerously_allow_auto_upload_download_files=True,
)

# Upload a local file to Google Drive
result = composio.tools.execute(
    slug="GOOGLEDRIVE_UPLOAD_FILE",
    user_id="user-1235***",
    arguments={"file_to_upload": os.path.join(os.getcwd(), "document.pdf")},
)

print(result)  # Print Google Drive file details
// Upload a local file to Google Drive
const result = await composio.tools.execute('GOOGLEDRIVE_UPLOAD_FILE', {
  userId: 'user-4235***',
  arguments: {
    file_to_upload: path.join(__dirname, 'document.pdf')
  }
});

console.log(result.data);  // Contains Google Drive file details

This also works with public URLs. For example, to send a Gmail email with an attachment from a URL:

result = composio.tools.execute(
    slug="GMAIL_SEND_EMAIL",
    user_id="user-1235***",
    arguments={
        "recipient_email": "recipient@example.com",
        "subject": "Report attached",
        "body": "Please find the report attached.",
        "attachment": "https://example.com/report.pdf",
    },
)
const result = await composio.tools.execute('GMAIL_SEND_EMAIL', {
  userId: 'user-1235***',
  arguments: {
    recipient_email: 'recipient@example.com',
    subject: 'Report attached',
    body: 'Please find the report attached.',
    attachment: 'https://example.com/report.pdf',
  },
});

File Download

When tools return files, Composio downloads them to the local directory and provides the file path in the response:

composio = Composio(
    api_key="your_composio_key",
    dangerously_allow_auto_upload_download_files=True,
    file_download_dir="./downloads",  # Optional: Specify download directory
)

result = composio.tools.execute(
    "GOOGLEDRIVE_DOWNLOAD_FILE",
    user_id="user-1235***",
    arguments={"file_id": "your_file_id"},
)

# Result includes local file path
print(result)
// Download a file from Google Drive
const result = await composio.tools.execute('GOOGLEDRIVE_DOWNLOAD_FILE', {
    userId: 'user-1235***',
    arguments: {
      file_id: 'your-file-id'
    }
  });

// Result includes local file path
console.log(result);

Security for local file paths

Automatic upload reads local paths (or uses them in files.upload) and sends file content to Composio storage. To reduce the chance of accidentally uploading secrets (SSH keys, cloud credentials, .env files, or assistant project folders), the SDK checks resolved paths against a default denylist of path segments and file-name patterns. HTTP/HTTPS URLs and File objects are not matched like local path strings; only local string paths go through this check.

Disabling protection or wiring a hook that always approves paths can allow exfiltration of sensitive files if an LLM or malicious input points at credential locations. Prefer keeping protection on, adding fileUploadPathDenySegments (TypeScript) / file_upload_path_deny_segments (Python) for your own secret directories, and using a beforeFileUpload / before_file_upload hook for auditing or strict allowlists. On Python, you can use the @before_file_upload decorator with modifiers=[...] the same way as @before_execute — see Before file upload (Python) on the before-execution modifiers page.

Constructor options (defaults protect sensitive locations):

import { Composio } from '@composio/core';

const composio = new Composio({
  apiKey: process.env.COMPOSIO_API_KEY!,
  // default true — block paths under built-in segments (e.g. .ssh, .aws) and risky basenames
  sensitiveFileUploadProtection: true,
  // optional: extra path *components* anywhere in the resolved path, merged with the built-in list
  fileUploadPathDenySegments: ['internal-only-secrets'],
});
from composio import Composio

composio = Composio(
    api_key="your_composio_key",
    sensitive_file_upload_protection=True,
    file_upload_path_deny_segments=("internal-only-secrets",),
)

Per-execution upload hook (run after the path is chosen but before read/upload; return a new path, false to abort, or throw). TypeScript passes beforeFileUpload on the third argument to tools.execute. In Python, add @before_file_upload functions to the modifiers list for get / execute (see Before file upload (Python)).

await composio.tools.execute(
  'GOOGLEDRIVE_UPLOAD_FILE',
  {
    userId: 'user-4235***',
    arguments: { file_to_upload: path.join(__dirname, 'document.pdf') },
    dangerouslySkipVersionCheck: true,
  },
  { beforeFileUpload }
);
import os
from composio import Composio, before_file_upload

# New form: take a single ``context`` arg; ``context["source"]`` is "path"
# or "url". The legacy ``(path, tool, toolkit)`` 3-arg form is still accepted.
@before_file_upload(tools=["GOOGLEDRIVE_UPLOAD_FILE"])
def audit_path(ctx) -> str | bool:
    if ctx["source"] != "path":
        return ctx["path"]
    # ...audit ctx["path"]...
    return ctx["path"]

composio = Composio()

composio.tools.execute(
    "GOOGLEDRIVE_UPLOAD_FILE",
    {"file_to_upload": os.path.join(os.getcwd(), "document.pdf")},
    user_id="user-1235***",
    modifiers=[audit_path],
)

Errors: ComposioSensitiveFilePathBlockedError / SensitiveFilePathBlockedError when a path is blocked; ComposioFileUploadAbortedError / FileUploadAbortedError when the hook returns false (or equivalent abort).

Automatic file handling (off by default)

Both SDKs ship with automatic file handling off. Opt in with dangerouslyAllowAutoUploadDownloadFiles: true (TypeScript) or dangerously_allow_auto_upload_download_files=True (Python). Even with the flag enabled, local paths must resolve inside an allowlisted directory — see the matrix in the legacy auto-upload removal changelog for the full fileUploadDirs / file_upload_dirs semantics.

When auto-handling is off, handle the staging yourself.

The Python SDK does not expose a top-level manual-staging API. Either:

  1. Enable auto-handling and configure file_upload_dirs to cover the directories your code reads from:
from composio import Composio

composio = Composio(
    api_key="your_composio_key",
    dangerously_allow_auto_upload_download_files=True,
    file_upload_dirs=["/srv/agent/uploads", "~/.composio/temp"],
)
  1. Or keep auto-handling off and pass an already-staged { "name", "mimetype", "s3key" } dict directly into tools.execute. Pre-stage your files via your own pipeline (or an upstream tool) and inject the descriptor:
result = composio.tools.execute(
    "GOOGLEDRIVE_UPLOAD_FILE",
    user_id="user-1",
    arguments={"file_to_upload": prestaged_descriptor},
)

With dangerouslyAllowAutoUploadDownloadFiles: false (the default), call composio.files.upload(...) to stage paths/URLs explicitly. That call bypasses fileUploadDirs by design — the caller controls the path:

const composio = new Composio({
  apiKey: process.env.COMPOSIO_API_KEY,
  // Default: dangerouslyAllowAutoUploadDownloadFiles is false; set true only to opt in
});

// Stage manually using composio.files API
const fileData = await composio.files.upload({
  file: path.join(__dirname, 'document.pdf'),
  toolSlug: 'GOOGLEDRIVE_UPLOAD_FILE',
  toolkitSlug: 'googledrive'
});

// Then pass the { name, mimetype, s3key } descriptor into tools.execute
await composio.tools.execute('GOOGLEDRIVE_UPLOAD_FILE', {
  userId: 'user-1',
  arguments: { file_to_upload: fileData },
});