Tools so far have been local Python functions. Today: an agent tool that touches a real service. Composio's toolset.execute_action is the bridge.
from pydantic_ai import Agent
agent = Agent(model)
@agent.tool_plain
def count_inbox_emails() -> int:
"""Return the number of emails currently in the user's Gmail inbox (capped at 10)."""
result = toolset.execute_action(Action.GMAIL_FETCH_EMAILS, {"max_results": 10})
return len(result.get("messages", []))
response = agent.run_sync("How many emails are in my inbox right now?")
print(response.output)The tool is a 3-line function. Inside, it calls Composio. From the agent's perspective, it looks like any other tool.
Right. The model sees count_inbox_emails() -> int with the docstring. It calls when relevant, gets an integer back, and incorporates that into the final answer. Whether the tool is local arithmetic or a real API call is invisible to the model — the contract is just "call this function, get this type back".
What about safety? An agent that can read my inbox is a step up from add(a, b).
Three layers. First: the tool's signature determines what the agent CAN do. count_inbox_emails() reads metadata; it can't send. Want to allow sending? Add a separate tool with explicit parameters. Second: Composio's per-user OAuth scopes constrain what the user has authorised. Third: keep tools narrow — many small purposeful tools beat one giant do_anything_to_gmail(). Today's tool is read-only, capped at 10 messages, single integer output.
Any Composio action becomes an agent tool with a small wrapper:
@agent.tool_plain
def <python_name>(<typed_params>) -> <return_type>:
"""Plain-English docstring for the model."""
result = toolset.execute_action(Action.<COMPOSIO_ACTION>, {<params>})
return <extract_what_you_want>(result)The pattern:
toolset.execute_action(Action.X, {...})| Service | Common actions |
|---|---|
| Gmail | GMAIL_FETCH_EMAILS, GMAIL_SEND_EMAIL |
| Google Calendar | GOOGLECALENDAR_LIST_EVENTS, GOOGLECALENDAR_CREATE_EVENT |
| Google Sheets | GOOGLESHEETS_APPEND_VALUES, GOOGLESHEETS_GET_VALUES |
| Google Tasks | GOOGLETASKS_LIST_TASKS, GOOGLETASKS_INSERT_TASK |
| Google Docs | GOOGLEDOCS_GET_DOCUMENT, GOOGLEDOCS_CREATE_DOCUMENT |
| Google Slides | GOOGLESLIDES_GET_PRESENTATION |
LINKEDIN_GET_MY_INFO |
count_inbox_emails() cannot send email no matter what the model decides.count_inbox_emails, send_quick_reply(to, body)) beat broad ones (do_gmail_thing(action, params)). Narrow tools are easier to audit and easier for the model to use correctly.This lesson's tool is read-only. We're showing the integration without the consequences of bad agent decisions. Real production agents that send/write should ship with output validation, dry-run modes, and per-call confirmations.
One agent run with one tool call = ~2-3 LLM calls + 1 Composio call. Composio calls don't burn LLM quota but do count against your platform quota.
Wrap GMAIL_FETCH_EMAILS (max 10) as a count_inbox_emails() -> int tool. Ask the agent how many emails are in the inbox. Verify the response includes a digit.
Tools so far have been local Python functions. Today: an agent tool that touches a real service. Composio's toolset.execute_action is the bridge.
from pydantic_ai import Agent
agent = Agent(model)
@agent.tool_plain
def count_inbox_emails() -> int:
"""Return the number of emails currently in the user's Gmail inbox (capped at 10)."""
result = toolset.execute_action(Action.GMAIL_FETCH_EMAILS, {"max_results": 10})
return len(result.get("messages", []))
response = agent.run_sync("How many emails are in my inbox right now?")
print(response.output)The tool is a 3-line function. Inside, it calls Composio. From the agent's perspective, it looks like any other tool.
Right. The model sees count_inbox_emails() -> int with the docstring. It calls when relevant, gets an integer back, and incorporates that into the final answer. Whether the tool is local arithmetic or a real API call is invisible to the model — the contract is just "call this function, get this type back".
What about safety? An agent that can read my inbox is a step up from add(a, b).
Three layers. First: the tool's signature determines what the agent CAN do. count_inbox_emails() reads metadata; it can't send. Want to allow sending? Add a separate tool with explicit parameters. Second: Composio's per-user OAuth scopes constrain what the user has authorised. Third: keep tools narrow — many small purposeful tools beat one giant do_anything_to_gmail(). Today's tool is read-only, capped at 10 messages, single integer output.
Any Composio action becomes an agent tool with a small wrapper:
@agent.tool_plain
def <python_name>(<typed_params>) -> <return_type>:
"""Plain-English docstring for the model."""
result = toolset.execute_action(Action.<COMPOSIO_ACTION>, {<params>})
return <extract_what_you_want>(result)The pattern:
toolset.execute_action(Action.X, {...})| Service | Common actions |
|---|---|
| Gmail | GMAIL_FETCH_EMAILS, GMAIL_SEND_EMAIL |
| Google Calendar | GOOGLECALENDAR_LIST_EVENTS, GOOGLECALENDAR_CREATE_EVENT |
| Google Sheets | GOOGLESHEETS_APPEND_VALUES, GOOGLESHEETS_GET_VALUES |
| Google Tasks | GOOGLETASKS_LIST_TASKS, GOOGLETASKS_INSERT_TASK |
| Google Docs | GOOGLEDOCS_GET_DOCUMENT, GOOGLEDOCS_CREATE_DOCUMENT |
| Google Slides | GOOGLESLIDES_GET_PRESENTATION |
LINKEDIN_GET_MY_INFO |
count_inbox_emails() cannot send email no matter what the model decides.count_inbox_emails, send_quick_reply(to, body)) beat broad ones (do_gmail_thing(action, params)). Narrow tools are easier to audit and easier for the model to use correctly.This lesson's tool is read-only. We're showing the integration without the consequences of bad agent decisions. Real production agents that send/write should ship with output validation, dry-run modes, and per-call confirmations.
One agent run with one tool call = ~2-3 LLM calls + 1 Composio call. Composio calls don't burn LLM quota but do count against your platform quota.
Wrap GMAIL_FETCH_EMAILS (max 10) as a count_inbox_emails() -> int tool. Ask the agent how many emails are in the inbox. Verify the response includes a digit.
Create a free account to get started. Paid plans unlock all tracks.