1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
8 small wins to finish your path
GitHub and Gmail assistant with tested modifiers
Put it together: an agent that may use two exact tools for one user, with modifiers that label its GitHub issues and strip email bodies, and tests for those modifiers.
from composio import after_execute, before_execute
@before_execute(tools=["GITHUB_CREATE_AN_ISSUE"])
def label_agent_issues(tool, toolkit, params):
params["arguments"].setdefault("labels", []).append("created-by-agent")
return params
@after_execute(toolkits=["gmail"])
def keep_only_subjects(tool, toolkit, response):
messages = response["data"].get("messages", [])
response["data"] = {"subjects": [m.get("subject") for m in messages]}
return responseimport json
from composio import Composio
from openai import OpenAI
from modifiers import keep_only_subjects, label_agent_issues
composio = Composio(allow_tracking=False) # reads COMPOSIO_API_KEY
client = OpenAI() # reads OPENAI_API_KEY
TOOLS = ["GITHUB_CREATE_AN_ISSUE", "GMAIL_FETCH_EMAILS"]
MODIFIERS = [label_agent_issues, keep_only_subjects]
def run(user_id: str, task: str) -> str:
tools = composio.tools.get(user_id, tools=TOOLS)
messages = [{"role": "user", "content": task}]
for _ in range(5):
response = client.chat.completions.create(model="gpt-4.1-mini", messages=messages, tools=tools)
reply = response.choices[0].message
if not reply.tool_calls:
return reply.content
messages.append(reply)
results = composio.provider.handle_tool_calls(user_id=user_id, response=response, modifiers=MODIFIERS)
for call, result in zip(reply.tool_calls, results):
messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})
return "Stopped after 5 steps."- The model sees exactly two tools, fetched for the user who asked.
- Every call goes through
handle_tool_callswith the modifiers, so an agent issue is always labelled and email bodies never reach the model. - Results are sent back in the order of the tool calls; the loop stops after 5 steps.
- The user must have connected GitHub and Gmail first, as in lesson 2.
Hover or tap a piece to see what it is and which lesson built it.
Follow a tool call
Running agent.py acts on real accounts, so it is not run here. The modifiers are what your customer is trusting, and they are plain functions:
from modifiers import keep_only_subjects, label_agent_issues
def test_agent_issues_are_labelled():
params = {"arguments": {"title": "Refund failed for A-1002"}}
out = label_agent_issues.apply("github", "GITHUB_CREATE_AN_ISSUE", params, "before_execute")
assert out["arguments"]["labels"] == ["created-by-agent"]
def test_other_tools_are_untouched():
params = {"arguments": {"text": "hello"}}
out = label_agent_issues.apply("slack", "SLACK_SEND_MESSAGE", params, "before_execute")
assert out == {"arguments": {"text": "hello"}}
def test_gmail_bodies_never_reach_the_model():
response = {"successful": True, "error": None, "data": {"messages": [{"subject": "Invoice", "body": "card 4111 1111 1111 1111"}]}}
out = keep_only_subjects.apply("gmail", "GMAIL_FETCH_EMAILS", response, "after_execute")
assert out["data"] == {"subjects": ["Invoice"]}
assert "4111" not in str(out)pytest -q -p no:warningsThe tests call apply exactly as the SDK does, with no key and no network: agent issues are always labelled, other tools are untouched, and a card number in an email body never reaches the model.
Beyond tools and modifiers
| Topic | What it is for |
|---|---|
| Triggers | Events from connected apps, like a new email, over a WebSocket or webhooks. |
| MCP | Serving a session's tools to an MCP client with create(..., mcp=True). |
| Your own OAuth apps | Custom auth configs with your branding and scopes. |
| The sandbox | Processing large tool results in a session-scoped sandbox. |
| Other providers | Anthropic, LangChain, CrewAI, OpenAI Agents SDK and more. |
| TypeScript SDK | The same concepts for TypeScript apps. |
You understood something today that you didn't yesterday.