OpenAI Agents SDK
Give an OpenAI Agents SDK agent email tools and invoke it from signed incoming-mail events.
The OpenAI Agents SDK turns ordinary Python functions into model tools. CarlyEmail's Python SDK handles the email API, while a signed webhook starts the agent on each new message.
1. Install and create an inbox
pip install carlyemail openai-agents fastapi svix uvicorn
npx carlyemail signup --human-email you@example.com --username assistant
npx carlyemail verify 123456
export OPENAI_API_KEY=sk-...
export CARLYEMAIL_API_KEY=ce_us_...
export CARLYEMAIL_INBOX=assistant@carlyemail.com
export CARLYEMAIL_WEBHOOK_SECRET=whsec_...
2. Give the agent email tools
Python
import os
from agents import Agent, function_tool
from carlyemail import CarlyEmail
carly = CarlyEmail()
INBOX = os.environ["CARLYEMAIL_INBOX"]
@function_tool
def list_messages(limit: int = 20) -> list[dict]:
"""List recent messages in the agent's own inbox."""
return carly.messages.list(INBOX, limit=limit)["messages"]
@function_tool
def read_message(message_id: str) -> dict:
"""Read one message in full, including its body."""
return carly.messages.get(INBOX, message_id)
@function_tool
def reply(message_id: str, text: str) -> dict:
"""Reply in the same thread. This sends real email and cannot be undone."""
return carly.messages.reply(INBOX, message_id, {"text": text})
inbox_agent = Agent(
name="Inbox",
instructions=(
"Read the message and thread before acting. Never invent facts. "
"Draft rather than send when intent or authority is ambiguous."
),
tools=[list_messages, read_message, reply],
)
Only expose the operations the agent needs. Pair that model-visible list with an
inbox-scoped API key, so a missing permission still returns
403 if a tool or guardrail is later misconfigured.
3. Invoke it when mail arrives
Python
import json
from agents import Runner
from fastapi import FastAPI, Request, Response
from svix.webhooks import Webhook, WebhookVerificationError
app = FastAPI()
@app.post("/hooks/carlyemail")
async def on_email(request: Request):
raw = await request.body()
try:
event = Webhook(os.environ["CARLYEMAIL_WEBHOOK_SECRET"]).verify(
raw, request.headers
)
except WebhookVerificationError:
return Response("Invalid signature", status_code=400)
message = event.get("message")
if event.get("event_type") != "message.received" or not message:
return Response(status_code=204)
await Runner.run(
inbox_agent,
"Handle this newly received email. Reply if appropriate.\n\n"
+ json.dumps(message),
)
return Response(status_code=204)
uvicorn app:app --host 0.0.0.0 --port 8000
npx carlyemail webhook https://your-agent.example/hooks/carlyemail \
--events message.received
The event's thread_id is the stable key to pass to your own session store when
the agent needs private memory between messages. CarlyEmail preserves the real
email headers when reply is called.
Handing off between agents
Email maps naturally onto SDK handoffs: a triage agent reads and routes, while specialists share the reply tool.
Python
billing = Agent(name="Billing", instructions="Answer billing questions.", tools=[reply])
support = Agent(name="Support", instructions="Answer product questions.", tools=[reply])
triage = Agent(
name="Triage",
instructions="Read new mail and hand off to the right specialist.",
tools=[list_messages, read_message],
handoffs=[billing, support],
)
Warning
Email content is untrusted model input. Verify the raw body, deduplicate by
event_id, and start with draft-only permissions when a human should approve
outbound mail.