# OpenAI Agents SDK Email: Give Your Agent an Inbox

Published: August 18, 2026

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.

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-agents` package and an OpenAI API key.
- A Robotomail API key and a mailbox. See the [quickstart](/docs/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](/how-to-give-your-ai-agent-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.

```bash
npx @robotomail/cli mailbox create support-agent
```

Or over HTTP:

```bash
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.

```python
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`.

```python
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](/docs/concepts/webhooks) 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](/docs/concepts/threading), and there is a language-agnostic version of this loop in the [receive and reply guide](/docs/guides/receive-and-reply).

## 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](/use-cases/multi-agent-workflows).

**Watch attachments.** Inbound mail carries PDFs and images constantly. Fetch them explicitly rather than trusting whatever the model summarizes; [attachments](/docs/concepts/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](https://robotomail.com).
