20 KiB
Modifiers
Composio SDK provides powerful middleware capabilities through modifiers that allow you to customize and extend the behavior of tools. This guide explains how to use modifiers to transform tool schemas, modify inputs before execution, and transform outputs after execution.
Important: Not all modifiers are supported by all provider types. Schema modifiers work with all providers, but execution modifiers (beforeExecute and afterExecute) are only supported by agentic providers. See the Provider-Specific Modifier Support section for details.
What are Modifiers?
Modifiers are functions that intercept and modify the normal flow of tool operations. Composio supports three types of modifiers:
- Schema Modifiers: Transform a tool's schema before it's wrapped for the provider (supported by all providers)
- Before Execution Modifiers: Modify inputs before a tool is executed (supported by agentic providers only)
- After Execution Modifiers: Transform outputs after a tool has executed (supported by agentic providers only)
Schema Modifiers
Schema modifiers allow you to transform a tool's schema before it's wrapped for the provider. This is useful for customizing tool descriptions, parameter definitions, or adding metadata.
// Modify tool schema when getting tools
const tools = await composio.tools.get(
'default',
{
toolkits: ['github'],
},
{
modifySchema: ({ toolSlug, toolkitSlug, schema }) => {
// Add a prefix to all tool descriptions
if (tool.description) {
tool.description = `[Enhanced] ${tool.description}`;
}
// Modify parameters for a specific tool
if (toolSlug === 'GITHUB_GET_REPO') {
if (tool.inputParameters?.properties?.owner) {
// Add more detailed description
tool.inputParameters.properties.owner.description =
'GitHub organization or user name (e.g., "composio")';
}
}
return tool;
},
}
);
Before Execution Modifiers
Before execution modifiers allow you to modify the input parameters before a tool is executed. This is useful for parameter validation, transformation, or adding default values.
// Modify parameters before execution
const result = await composio.tools.execute(
'GITHUB_GET_REPO',
{
userId: 'default',
arguments: {
owner: 'composio',
repo: 'sdk',
},
},
{
beforeExecute: ({ toolSlug, toolkitSlug, params }) => {
// Convert owner names to lowercase
if (params.arguments.owner) {
params.arguments.owner = params.arguments.owner.toLowerCase();
}
// Add a default branch parameter
if (toolSlug === 'GITHUB_GET_REPO' && !params.arguments.branch) {
params.arguments.branch = 'main';
}
// Add tracing for debugging
console.log(`Executing ${toolSlug} from ${toolkitSlug} toolkit`);
return params;
},
}
);
After Execution Modifiers
After execution modifiers allow you to transform the output after a tool has executed. This is useful for data formatting, filtering, or enrichment.
// Modify results after execution
const result = await composio.tools.execute(
'GITHUB_GET_REPO',
{
userId: 'default',
arguments: {
owner: 'composio',
repo: 'sdk',
},
},
{
afterExecute: ({ toolSlug, toolkitSlug, result }) => {
// Only when execution was successful
if (result.successful) {
// Filter out sensitive data
if (result.data.token) {
delete result.data.token;
}
// Add a timestamp
result.data.fetchedAt = new Date().toISOString();
// Transform data formats
if (result.data.created_at) {
result.data.createdAt = new Date(result.data.created_at).toLocaleString();
delete result.data.created_at;
}
}
return result;
},
}
);
Using Multiple Modifiers
You can use multiple modifiers together, but remember that execution modifiers (beforeExecute and afterExecute) only work with agentic providers:
// With an agentic provider (e.g., Vercel, Langchain)
const agenticProvider = new VercelProvider();
const composio = new Composio({
apiKey: 'your-api-key',
provider: agenticProvider,
});
// All modifiers work with agentic providers
const tools = await composio.tools.get(
'default',
{
toolkits: ['github'],
},
{
// Schema modifier
modifySchema: ({ toolSlug, toolkitSlug, schema }) => {
// Enhance tool schema
return schema;
},
// Before execution modifier (only works with agentic providers)
beforeExecute: ({ toolSlug, toolkitSlug, params }) => {
// Modify execution parameters
return params;
},
// After execution modifier (only works with agentic providers)
afterExecute: ({ toolSlug, toolkitSlug, result }) => {
// Transform execution results
return result;
},
}
);
// With a non-agentic provider (e.g., OpenAI)
const nonAgenticProvider = new OpenAIProvider();
const composioNonAgentic = new Composio({
apiKey: 'your-api-key',
provider: nonAgenticProvider,
});
// Only schema modifiers work with non-agentic providers
const openaiTools = await composioNonAgentic.tools.get(
'default',
{
toolkits: ['github'],
},
{
// Schema modifier (works with all providers)
modifySchema: (toolSlug, toolkitSlug, tool) => {
// Enhance tool schema
return tool;
},
// These will be ignored by non-agentic providers
beforeExecute: () => {}, // No effect with non-agentic providers
afterExecute: () => {}, // No effect with non-agentic providers
}
);
Creating Reusable Modifiers
For better code organization, you can create reusable modifier functions:
// Define reusable modifiers
const addTimestamps = ({ toolSlug, toolkitSlug, result }) => {
if (result.successful) {
result.data.executedAt = new Date().toISOString();
}
return result;
};
const logExecutions = ({ toolSlug, toolkitSlug, params }) => {
console.log(`Executing ${toolSlug} from ${toolkitSlug} at ${new Date().toISOString()}`);
return params;
};
const sanitizeInputs = ({ toolSlug, toolkitSlug, params }) => {
// Sanitize input parameters to prevent injection attacks
if (params.arguments) {
Object.keys(params.arguments).forEach(key => {
if (typeof params.arguments[key] === 'string') {
params.arguments[key] = sanitizeString(params.arguments[key]);
}
});
}
return params;
};
// Use the reusable modifiers
const result = await composio.tools.execute(
'GITHUB_GET_REPO',
{
userId: 'default',
arguments: {
owner: 'composio',
repo: 'sdk',
},
},
{
beforeExecute: sanitizeInputs,
afterExecute: addTimestamps,
}
);
Practical Use Cases
Comparing Agentic and Non-Agentic Provider Usage
Here's a side-by-side comparison of how to use modifiers with different provider types:
// Example from examples/modifiers/src/index.ts
// Non-agentic provider example (OpenAI)
const openai = new Composio({
apiKey: process.env.COMPOSIO_API_KEY,
provider: new OpenAIProvider(),
});
// Schema modifiers work with non-agentic providers
const nonAgenticTools = await openai.tools.get('default', 'HACKERNEWS_GET_USER', {
// Schema modifier works with all providers
modifySchema: (toolSlug, toolkitSlug, tool) => {
// Customize the input parameters
if (tool.inputParameters?.properties?.userId) {
tool.inputParameters.properties.userId.description = 'HackerNews username (e.g., "pg")';
}
return tool;
},
});
// Agentic provider example (Vercel)
const vercel = new Composio({
apiKey: process.env.COMPOSIO_API_KEY,
provider: new VercelProvider(),
});
// All modifiers work with agentic providers
const agenticTools = await vercel.tools.get('default', 'HACKERNEWS_GET_USER', {
// Schema modifier
modifySchema: ({ toolSlug, toolkitSlug, schema }) => {
if (tool.inputParameters?.properties?.userId) {
tool.inputParameters.properties.userId.description = 'HackerNews username (e.g., "pg")';
}
return schema;
},
// Execution modifiers (only work with agentic providers)
beforeExecute: ({ toolSlug, toolkitSlug, params }) => {
console.log(`Executing ${toolSlug} from ${toolkitSlug}`);
return params;
},
afterExecute: ({ toolSlug, toolkitSlug, result }) => {
if (result.successful) {
result.data.processedAt = new Date().toISOString();
}
return result;
},
});
Manual Execution Modifiers with Non-Agentic Providers
For non-agentic providers, you can use the provider's helper methods to apply execution modifiers:
// Get the OpenAI provider instance
const openaiProvider = composio.provider as OpenAIProvider;
// When handling a tool call from OpenAI
const toolOutputs = await openaiProvider.handleToolCalls(
'default', // userId
completion, // OpenAI completion object
{ connectedAccountId: 'account_123' }, // Options
{
// Manually apply execution modifiers
beforeExecute: ({ toolSlug, toolkitSlug, params }) => {
console.log(`Executing ${toolSlug}`);
return params;
},
afterExecute: ({ toolSlug, toolkitSlug, result }) => {
result.data.processedAt = new Date().toISOString();
return result;
},
}
);
Caching Tool Results (Agentic Providers Only)
Note: This example uses execution modifiers and will only work with agentic providers.
// Simple in-memory cache
const cache = new Map();
const cacheModifier = ({ toolSlug, toolkitSlug, params }) => {
// Create a cache key based on tool slug and arguments
const cacheKey = `${toolSlug}-${JSON.stringify(params.arguments)}`;
// Add cache key to params for the after modifier to use
params.__cacheKey = cacheKey;
// Check if we have a cached result
if (cache.has(cacheKey)) {
const cachedResult = cache.get(cacheKey);
if (Date.now() - cachedResult.timestamp < 60000) {
// 1 minute cache
// Throw a special error to short-circuit execution
throw { __cached: true, result: cachedResult.result };
}
}
return params;
};
const cacheAfterModifier = ({ toolSlug, toolkitSlug, result }) => {
if (result.successful && result.__cacheKey) {
// Store in cache
cache.set(result.__cacheKey, {
result,
timestamp: Date.now(),
});
// Remove the cache key from the result
delete result.__cacheKey;
}
return result;
};
// Use with try/catch to handle the cache hit
try {
const result = await composio.tools.execute(
'GITHUB_GET_REPO',
{
userId: 'default',
arguments: {
owner: 'composio',
repo: 'sdk',
},
},
{
beforeExecute: cacheModifier,
afterExecute: cacheAfterModifier,
}
);
console.log('Fresh result:', result);
} catch (error) {
if (error.__cached) {
console.log('Cached result:', error.result);
} else {
throw error; // Re-throw if it's a real error
}
}
Adding Authentication Headers
const authModifier = ({ toolSlug, toolkitSlug, params }) => {
// Add authentication parameters for specific toolkits
if (toolkitSlug === 'custom-api') {
if (!params.customAuthParams) {
params.customAuthParams = {};
}
if (!params.customAuthParams.parameters) {
params.customAuthParams.parameters = [];
}
// Add an API key to the headers
params.customAuthParams.parameters.push({
in: 'header',
name: 'X-API-Key',
value: process.env.CUSTOM_API_KEY,
});
}
return params;
};
// Apply the auth modifier to all tools from a specific toolkit
const tools = await composio.tools.get(
'default',
{
toolkits: ['custom-api'],
},
{
beforeExecute: authModifier,
}
);
Data Transformation Pipeline
// Create a pipeline of modifiers
const pipeModifiers = (...modifiers) => {
return ({ toolSlug, toolkitSlug, result }) => {
return modifiers.reduce((modifiedResult, modifier) => {
return modifier({ toolSlug, toolkitSlug, result: modifiedResult });
}, result);
};
};
// Individual transformation steps
const addMetadata = ({ toolSlug, toolkitSlug, result }) => {
if (result.successful) {
result.data.metadata = { source: toolkitSlug, tool: toolSlug };
}
return result;
};
const formatDates = ({ toolSlug, toolkitSlug, result }) => {
if (result.successful) {
// Convert all dates to a consistent format
Object.keys(result.data).forEach(key => {
if (key.includes('date') || key.includes('time') || key.endsWith('At')) {
if (result.data[key]) {
result.data[key] = new Date(result.data[key]).toLocaleString();
}
}
});
}
return result;
};
const removeNulls = ({ toolSlug, toolkitSlug, result }) => {
if (result.successful) {
// Remove null and undefined values
Object.keys(result.data).forEach(key => {
if (result.data[key] === null || result.data[key] === undefined) {
delete result.data[key];
}
});
}
return result;
};
// Apply the pipeline
const result = await composio.tools.execute(
'GITHUB_GET_REPO',
{
userId: 'default',
arguments: {
owner: 'composio',
repo: 'sdk',
},
},
{
afterExecute: pipeModifiers(addMetadata, formatDates, removeNulls),
}
);
Provider-Specific Modifier Support
Composio SDK supports two types of providers:
- Agentic Providers: Providers that have control over tool execution (e.g., Vercel, Langchain)
- Non-Agentic Providers: Providers that only format tools but don't control execution (e.g., OpenAI)
Technical Differences
The key technical distinction between these provider types affects which modifiers they support:
-
Agentic Providers receive an
ExecuteToolFnparameter in theirwrapTool/wrapToolsmethods, allowing them to embed execution modifiers directly into the wrapped tools. This means they support all three types of modifiers: schema, beforeExecute, and afterExecute. -
Non-Agentic Providers only format tools for external consumption and don't control execution. When tools are wrapped by non-agentic providers, the context is lost, making it impossible to automatically apply execution modifiers. These providers only support schema modifiers.
Here's how the provider implementations differ:
// Non-Agentic Provider (e.g., OpenAI)
class OpenAIProvider extends BaseNonAgenticProvider<OpenAiToolCollection, OpenAiTool> {
// No executeToolFn parameter
override wrapTool(tool: Tool): OpenAiTool {
// Simply formats the tool for OpenAI
return {
type: 'function',
function: {
name: tool.slug,
description: tool.description,
parameters: tool.inputParameters,
},
};
}
}
// Agentic Provider (e.g., Vercel)
class VercelProvider extends BaseAgenticProvider<VercelToolCollection, VercelTool> {
// Receives executeToolFn parameter
wrapTool(tool: Tool, executeTool: ExecuteToolFn): VercelTool {
return tool({
description: tool.description,
parameters: jsonSchema(tool.inputParameters ?? {}),
execute: async params => {
// Can apply modifiers through executeTool
return await executeTool(tool.slug, params);
},
});
}
}
Using Modifiers with Non-Agentic Providers
For non-agentic providers like OpenAI, you can still apply execution modifiers manually using the provider's helper methods:
// Get OpenAI provider instance
const openaiProvider = composio.provider as OpenAIProvider;
// Execute a tool call with modifiers
const result = await openaiProvider.executeToolCall(
'default', // userId
toolCall, // OpenAI tool call object
{ connectedAccountId: 'account_123' }, // Options
{
// Manually apply execution modifiers
beforeExecute: ({ toolSlug, toolkitSlug, params }) => {
console.log(`Executing ${toolSlug}`);
return params;
},
afterExecute: ({ toolSlug, toolkitSlug, result }) => {
result.data.processedAt = new Date().toISOString();
return result;
},
}
);
Tool Router Modifiers (v0.4.0+)
Tool Router sessions support enhanced modifier types that include session context. This is useful for tracking and managing tool execution across different user sessions.
Using Session Modifiers with Tool Router
import { Composio } from '@composio/core';
import { SessionExecuteMetaModifiers } from '@composio/core';
const composio = new Composio();
const session = await composio.create('user_123', {
toolkits: ['gmail', 'slack'],
});
// Use session-specific modifiers
const tools = await session.tools({
modifySchema: ({ toolSlug, toolkitSlug, schema }) => {
// Customize tool schemas
console.log(`Modifying schema for ${toolSlug} from ${toolkitSlug}`);
return schema;
},
beforeExecute: ({ toolSlug, toolkitSlug, sessionId, params }) => {
// Access session ID for tracking
console.log(`[Session: ${sessionId}] Executing ${toolSlug} from ${toolkitSlug}`);
// Add session-specific metadata
params.sessionMetadata = {
sessionId,
timestamp: new Date().toISOString(),
};
return params;
},
afterExecute: ({ toolSlug, toolkitSlug, sessionId, result }) => {
// Transform results with session context
console.log(`[Session: ${sessionId}] Completed ${toolSlug}`);
if (result.successful) {
result.data.sessionId = sessionId;
result.data.executedAt = new Date().toISOString();
}
return result;
},
});
Session-Specific Modifier Benefits
The session-specific modifiers provide several advantages:
- Session Tracking: Access to
sessionIdallows you to track which session executed which tools - User Context: Associate tool executions with specific users through session IDs
- Audit Logging: Log tool executions with session context for compliance and debugging
- Performance Monitoring: Track tool execution performance per session
Getting Meta Tools Directly
You can also fetch tool router meta tools directly using the new method:
import { Composio } from '@composio/core';
const composio = new Composio();
// Get raw meta tools for a session
const metaTools = await composio.tools.getRawToolRouterMetaTools('session_123', {
modifySchema: ({ toolSlug, toolkitSlug, schema }) => {
// Customize meta tool schemas
if (toolSlug === 'composio_authorize_toolkit') {
schema.description = 'Custom description for authorization';
}
return schema;
}
});
// Use the meta tools with your AI framework
console.log('Available meta tools:', metaTools.map(t => t.name));
Type Definitions
// Schema Modifier (supported by all providers)
type TransformToolSchemaModifier = (toolSlug: string, toolkitSlug: string, tool: Tool) => Tool;
// Before Execution Modifier (supported by agentic providers only)
type beforeExecuteModifier = (
toolSlug: string,
toolkitSlug: string,
params: ToolExecuteParams
) => ToolExecuteParams;
// After Execution Modifier (supported by agentic providers only)
type afterExecuteModifier = (
toolSlug: string,
toolkitSlug: string,
result: ToolExecuteResponse
) => ToolExecuteResponse;
// Modifiers Object
interface ExecuteToolModifiers {
beforeExecute?: beforeExecuteModifier;
afterExecute?: afterExecuteModifier;
}
// Provider Options
interface ProviderOptions<TProvider> {
modifySchema?: TransformToolSchemaModifier;
beforeExecute?: beforeExecuteModifier; // Only applied by agentic providers
afterExecute?: afterExecuteModifier; // Only applied by agentic providers
}
// Session-Specific Modifiers (v0.4.0+)
interface SessionExecuteMetaModifiers {
modifySchema?: (context: {
toolSlug: string;
toolkitSlug: string;
schema: any;
}) => any;
beforeExecute?: (context: {
toolSlug: string;
toolkitSlug: string;
sessionId: string;
params: any;
}) => any;
afterExecute?: (context: {
toolSlug: string;
toolkitSlug: string;
sessionId: string;
result: any;
}) => any;
}
// Session Meta Tool Options (v0.4.0+)
interface SessionMetaToolOptions {
modifySchema?: (context: {
toolSlug: string;
toolkitSlug: string;
schema: any;
}) => any;
}