OpenAI Agents SDK Email: Give Your Agent an Inbox
Wire an OpenAI Agents SDK agent to a real mailbox: send and reply as function tools, inbound webhook driving the loop, threading handled. Working code.
John Joubert
Founder, Robotomail

Table of contents
Wiring OpenAI Agents SDK email support takes two pieces: a real mailbox with an address your agent owns, and function tools that send and reply through it. Inbound mail arrives at your webhook, you feed that payload into Runner.run(), and the agent decides whether to reply. The SDK has no email primitive of its own, so the mailbox comes from an email API built for agents.
Below is the full loop in Python: create the mailbox, expose send and reply as @function_tool, receive inbound JSON, and keep replies on the same thread so the human on the other end sees a normal conversation.
What you need
- The
openai-agentspackage and an OpenAI API key. - A Robotomail API key and a mailbox. See the quickstart if you want the raw HTTP walkthrough first.
- A public HTTPS endpoint for inbound webhooks. During development, any tunnel to localhost works.
If you have not decided on the mailbox layer yet, our overview of how to give an AI agent an email address covers the tradeoffs between a real inbox, an SMTP relay, and a shared human mailbox.
Step 1: Create the mailbox
One mailbox per agent is the pattern that scales. The address becomes the agent's identity, and every thread it participates in is scoped to it.
npx @robotomail/cli mailbox create support-agent
Or over HTTP:
curl -X POST https://api.robotomail.com/v1/mailboxes \
-H "Authorization: Bearer rm_your_api_key" \
-H "Content-Type: application/json" \
-d '{"address": "support-agent"}'
You get back a mailbox ID and a live address such as support-agent@robotomail.co. Mail sent to it is deliverable immediately. Add domainId later if you want the agent on your own domain.
Step 2: Wrap send and reply as OpenAI Agents SDK tools
The Agents SDK turns any typed Python function into a tool with @function_tool. The docstring and parameter names become the schema the model sees, so write them for the model, not for your teammates.
Two tools cover almost every case: send_email for starting a thread, and reply_to_email for continuing one. Splitting them keeps the model from guessing whether it needs an in_reply_to value.
import os
import httpx
from agents import Agent, Runner, function_tool
MAILBOX_ID = os.environ["ROBOTOMAIL_MAILBOX_ID"]
HEADERS = {
"Authorization": f"Bearer {os.environ['ROBOTOMAIL_API_KEY']}",
"Content-Type": "application/json",
}
@function_tool
def send_email(to: list[str], subject: str, body_text: str) -> str:
"""Send a new email from the agent's own mailbox. Use for first contact only."""
response = httpx.post(
f"https://api.robotomail.com/v1/mailboxes/{MAILBOX_ID}/messages",
headers=HEADERS,
json={"to": to, "subject": subject, "bodyText": body_text},
timeout=30,
)
response.raise_for_status()
return f"sent: {response.json()}"
@function_tool
def reply_to_email(to: list[str], subject: str, body_text: str, in_reply_to: str) -> str:
"""Reply to an email. in_reply_to must be the message_id of the email being answered."""
response = httpx.post(
f"https://api.robotomail.com/v1/mailboxes/{MAILBOX_ID}/messages",
headers=HEADERS,
json={
"to": to,
"subject": subject,
"bodyText": body_text,
"inReplyTo": in_reply_to,
},
timeout=30,
)
response.raise_for_status()
return f"replied: {response.json()}"
agent = Agent(
name="Support agent",
instructions=(
"You handle email for Acme. You have your own mailbox. "
"When you receive a message, answer it with reply_to_email using the "
"message_id you were given. Keep replies under 150 words, plain text, no "
"markdown. If you cannot answer, say so and say a human will follow up. "
"Never email anyone who did not email you first."
),
tools=[send_email, reply_to_email],
)
Two details matter here. First, tell the model to write plain text, because models default to markdown and asterisks look broken in a mail client. Second, forbid unsolicited sends explicitly. An agent with a real address and a vague prompt will eventually invent a recipient.
Step 3: Drive the agent loop from the inbound webhook
Register a webhook for the mailbox, then serve the payload into Runner.run(). Inbound messages arrive as { event, timestamp, data } with snake_case fields inside data.
from fastapi import BackgroundTasks, FastAPI, Request
app = FastAPI()
seen_messages: set[str] = set()
async def handle_message(data: dict) -> None:
prompt = (
f"New email received.\n"
f"message_id: {data['message_id']}\n"
f"thread_id: {data['thread_id']}\n"
f"from: {data['from']}\n"
f"subject: {data['subject']}\n\n"
f"{data['body_text']}"
)
result = await Runner.run(agent, prompt)
print(data["thread_id"], result.final_output)
@app.post("/webhooks/robotomail")
async def inbound(request: Request, background_tasks: BackgroundTasks):
payload = await request.json()
if payload.get("event") != "message.received":
return {"ok": True}
data = payload["data"]
if data["message_id"] in seen_messages:
return {"ok": True}
seen_messages.add(data["message_id"])
background_tasks.add_task(handle_message, data)
return {"ok": True}
Return 200 immediately and run the agent in the background. Model calls plus tool calls routinely take longer than a webhook delivery timeout, and a slow handler turns into retries, which turn into duplicate replies.
Deduplicating on message_id is not optional. Webhook delivery is at-least-once by design. Use Redis or a database column with a unique constraint in production instead of the in-memory set above; the webhooks concepts page covers retry behavior.
Threading: let the mailbox do it
Do not build reply headers yourself. Pass the inbound message_id as inReplyTo and the correct In-Reply-To and References headers are set for you, so Gmail, Outlook and Apple Mail group the exchange into one conversation. Each inbound payload also carries a thread_id you can use as your own conversation key.
That thread_id is the natural place to hang agent memory. Store the SDK conversation state keyed by thread_id, load it before Runner.run(), and the agent remembers what it said three replies ago instead of re-reading a quoted trail. Details on how threads are built live in threading concepts, and there is a language-agnostic version of this loop in the receive and reply guide.
Production notes
Cap the reply chain. Two agents that both auto-reply will loop forever. Count messages per thread_id and hand off to a human past some limit, or stop replying to addresses that look automated.
Give the agent a guardrail on recipients. If it only ever answers inbound mail, validate in code that the to field of a reply matches the from of the message that triggered the run. Prompt instructions are a suggestion; a check in reply_to_email is a rule.
Separate mailboxes per role. A triage agent and an escalation agent should have different addresses, so you can read the routing from the headers when something goes wrong. That pattern is worth its own read: see multi-agent workflows.
Watch attachments. Inbound mail carries PDFs and images constantly. Fetch them explicitly rather than trusting whatever the model summarizes; attachments describes the retrieval flow.
FAQ
Does the OpenAI Agents SDK have built-in email support?
No. The SDK gives you agents, tools, handoffs and tracing. Sending and receiving mail is an external capability you expose as function tools, which is exactly what the code above does.
Can the agent receive email, not just send it?
Yes, that is what the webhook is for. Inbound mail to the mailbox address is delivered to your endpoint as JSON, and you invoke the agent with it. No IMAP polling, no OAuth refresh loop.
How do I keep replies in the same email thread?
Include inReplyTo with the message_id of the message you are answering. The reply headers are generated for you and clients thread it correctly. The thread_id in the payload is your key for grouping on your side.
Should each agent get its own address?
Usually yes. Per-agent mailboxes keep identity, rate limits and logs separate, and they make debugging a multi-agent system far easier than a single shared inbox with tag routing.
Provision a mailbox, drop the two tools into your agent, point the webhook at your handler, and your OpenAI Agents SDK agent is on email. Start at robotomail.com.
Give your AI agent a real email address
One API call creates a mailbox with full send and receive. Webhooks for inbound, automatic threading, deliverability handled. 30-day money-back guarantee.
Related posts

Hermes Agent Email: Give Your Nous Agent an Inbox
Step-by-step guide to wiring a real email inbox into a Hermes agent: mailbox creation, tool schemas, inbound webhooks, and threaded replies.
Read post
Email for OpenClaw Agents: Full Integration Guide
Wire real email into OpenClaw agents: provision a mailbox, expose send and reply tools, handle inbound webhooks, and keep threading correct.
Read post
CrewAI Email Integration: Inboxes for Your Crew
Wire real inboxes into CrewAI: shared or per-agent mailboxes, send and reply tools, and inbound webhooks that kick off crew tasks.
Read post