Processing Tools

Customize tool behavior by modifying schemas, inputs, and outputs

Composio allows you to refine how tools interact with LLMs and external APIs through Processors. These are custom functions you provide to modify data at key stages:

  • before the LLM sees the tool’s definition
  • before Composio executes the tool
  • after Composio executes the tool

Why use Processors?

  • Improve Reliability: Remove confusing parameters or inject required values the LLM might miss.
  • Guide LLMs: Simplify tool schemas or descriptions for better tool selection.
  • Manage Context & Cost: Filter large API responses to send only relevant data back to the LLM, saving tokens.
  • Adapt to Workflows: Transform tool inputs or outputs to match your application’s specific needs.
Python SDK Only

Tool Processors described on this page are currently only available in Composio’s Python SDK. Support for TypeScript is planned for the future.

How Processors Work

Processors are Python functions you define and pass to get_tools within a processors dictionary. The dictionary maps the processing stage ("schema", "pre", "post") to another dictionary, which maps the specific Action to your processor function.

Python
1# Conceptual structure for applying processors
2
3def my_schema_processor(schema: dict) -> dict: ...
4def my_preprocessor(inputs: dict) -> dict: ...
5def my_postprocessor(result: dict) -> dict: ...
6
7tools = toolset.get_tools(
8 actions=[Action.SOME_ACTION],
9 processors={
10 # Applied BEFORE the LLM sees the schema
11 "schema": {Action.SOME_ACTION: my_schema_processor},
12
13 # Applied BEFORE the tool executes
14 "pre": {Action.SOME_ACTION: my_preprocessor},
15
16 # Applied AFTER the tool executes, BEFORE the result is returned
17 "post": {Action.SOME_ACTION: my_postprocessor}
18 }
19)

Let’s look at each type.

Schema Processing (schema)

Goal: Modify the tool’s definition (schema) before it’s provided to the LLM.

Example: Simplifying GMAIL_SEND_EMAIL Schema

Let’s hide the recipient_email and attachment parameters from the LLM, perhaps because our application handles the recipient logic separately and doesn’t support attachments in this flow.

Python
1from composio_openai import ComposioToolSet, Action
2
3toolset = ComposioToolSet()
4
5def simplify_gmail_send_schema(schema: dict) -> dict:
6 """Removes recipient_email and attachment params from the schema."""
7 params = schema.get("parameters", {}).get("properties", {})
8 params.pop("recipient_email", None)
9 params.pop("attachment", None)
10 # We could also modify descriptions here, e.g.:
11 # schema["description"] = "Sends an email using Gmail (recipient managed separately)."
12 return schema
13
14# Get tools with the modified schema
15processed_tools = toolset.get_tools(
16 actions=[Action.GMAIL_SEND_EMAIL],
17 processors={
18 "schema": {Action.GMAIL_SEND_EMAIL: simplify_gmail_send_schema}
19 }
20)
21
22# Now, when 'processed_tools' are given to an LLM, it won't see
23# the 'recipient_email' or 'attachment' parameters in the schema.
24# print(processed_tools[0]) # To inspect the modified tool definition

Preprocessing (pre)

Goal: Modify the input parameters provided by the LLM just before the tool executes.

Use this to inject required values hidden from the LLM (like the recipient_email from the previous example), add default values, clean up or format LLM-generated inputs, or perform last-minute validation.

Example: Injecting recipient_email for GMAIL_SEND_EMAIL

Continuing the previous example, since we hid recipient_email from the LLM via schema processing, we now need to inject the correct value before Composio executes the GMAIL_SEND_EMAIL action.

Python
1def inject_gmail_recipient(inputs: dict) -> dict:
2 """Injects a fixed recipient email into the inputs."""
3 # Get the recipient from app logic, context, or hardcode it
4 inputs["recipient_email"] = "fixed.recipient@example.com"
5 # Ensure subject exists, providing a default if necessary
6 inputs["subject"] = inputs.get("subject", "No Subject Provided")
7 return inputs
8
9# Combine schema processing and preprocessing
10processed_tools = toolset.get_tools(
11 actions=[Action.GMAIL_SEND_EMAIL],
12 processors={
13 "schema": {Action.GMAIL_SEND_EMAIL: simplify_gmail_send_schema},
14 "pre": {Action.GMAIL_SEND_EMAIL: inject_gmail_recipient}
15 }
16)
17
18# Now, when the LLM calls this tool (without providing recipient_email),
19# the 'inject_gmail_recipient' function will run automatically
20# before Composio executes the action, adding the correct email.
21# result = toolset.handle_tool_calls(llm_response_using_processed_tools)
Schema vs. Preprocessing

Think of schema processing as changing the tool’s instructions for the LLM, while pre processing adjusts the actual inputs right before execution based on those instructions (or other logic).

Postprocessing (post)

Goal: Modify the result returned by the tool’s execution before it is passed back.

This is invaluable for filtering large or complex API responses to extract only the necessary information, reducing the number of tokens sent back to the LLM, improving clarity, and potentially lowering costs.

Example: Filtering GMAIL_FETCH_EMAILS Response

The GMAIL_FETCH_EMAILS action can return a lot of data per email. Let’s filter the response to include only the sender and subject, significantly reducing the payload sent back to the LLM.

Python
1import json # For pretty printing example output
2
3def filter_email_results(result: dict) -> dict:
4 """Filters email list to only include sender and subject."""
5 # Pass through errors or unsuccessful executions unchanged
6 if not result.get("successful") or "data" not in result:
7 return result
8
9 original_messages = result["data"].get("messages", [])
10 if not isinstance(original_messages, list):
11 return result # Return if data format is unexpected
12
13 filtered_messages = []
14 for email in original_messages:
15 filtered_messages.append({
16 "sender": email.get("sender"),
17 "subject": email.get("subject"),
18 })
19
20 # Construct the new result dictionary
21 processed_result = {
22 "successful": True,
23 # Use a clear key for the filtered data
24 "data": {"summary": filtered_messages},
25 "error": None
26 }
27 return processed_result
28
29# Get tools with the postprocessor
30processed_tools = toolset.get_tools(
31 actions=[Action.GMAIL_FETCH_EMAILS],
32 processors={
33 "post": {Action.GMAIL_FETCH_EMAILS: filter_email_results}
34 }
35)
36
37# --- Simulate Execution and Postprocessing ---
38# Assume 'raw_execution_result' is the large dictionary returned by
39# executing GMAIL_FETCH_EMAILS without postprocessing.
40# raw_execution_result = toolset.execute_action(Action.GMAIL_FETCH_EMAILS, params={...})
41
42# Apply the postprocessor manually to see the effect (handle_tool_calls does this automatically)
43# filtered_result = filter_email_results(raw_execution_result)
44# print("Filtered Result (much smaller for LLM):")
45# print(json.dumps(filtered_result, indent=2))

By using postprocessing, you can make tool results much more manageable and useful for the LLM, preventing context overflow and focusing its attention on the most relevant information.