# Hermes Agent Email: Give Your Nous Agent an Inbox

Published: August 14, 2026

Step-by-step guide to wiring a real email inbox into a Hermes agent: mailbox creation, tool schemas, inbound webhooks, and threaded replies.

Setting up Hermes agent email takes three pieces: a mailbox with a real address, two or three tool definitions the model can call, and a webhook that hands inbound mail back to your agent loop. Hermes models from Nous Research are strong at structured tool calling, so the model side is mostly schema design. The infrastructure side is where people get stuck, and that is what this guide covers in depth.

If you just want the short version with the minimum viable code, read [how to give your Hermes agent an email address](/how-to-give-your-hermes-agent-email-address) first. This post assumes you already ran that and now need the operational details: threading, idempotency, tool ergonomics, and what to do when the agent is not the only one in the conversation.

One framing note before code. A Hermes agent email setup is not a mail-merge integration and not an "AI writes your emails" feature. The agent owns an address, receives real mail from real people, and decides what to do. That means you need an inbox, not a send-only API.

## What Hermes actually needs

Hermes runs behind an OpenAI-compatible server in most deployments (vLLM, SGLang, or a hosted endpoint). That gives you the standard `tools` array and `tool_calls` response format, so email becomes three functions:

| Tool | Purpose |
| --- | --- |
| `send_email` | New outbound message to one or more recipients |
| `reply_to_email` | Reply inside an existing thread |
| `list_thread` | Read prior messages before answering |

Keep the surface small. Every extra tool costs you accuracy in the dispatch step, and a model that can send mail with four optional parameters will eventually send mail with the wrong four.

## Step 1: create the mailbox

Fastest path is the CLI:

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

Or from your provisioning code:

```bash
curl -X POST https://api.robotomail.com/v1/mailboxes \
  -H "Authorization: Bearer rm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"address": "hermes-agent"}'
```

You get back a mailbox ID (`mbx_...`) and a live address on our platform domain. Store the mailbox ID next to whatever identity record your agent already has. If you are running one agent per customer or per task, create one mailbox per agent rather than sharing a single address with a routing prefix. Separate mailboxes give you separate threads, separate suppression state, and a clean audit trail. See [mailboxes](/docs/concepts/mailboxes) for the identity model.

Want `hermes@yourcompany.com` instead? Add a custom domain and pass its UUID as `domainId` at creation time. The DNS steps are in the [custom domain guide](/docs/guides/custom-domain).

## Step 2: implement the email functions

Plain Python, no framework. Note that we build the full request URL at the call site, which keeps the HTTP method and path visible together and makes the code easy to grep during an incident.

```python
import os
import requests

RM_KEY = os.environ["ROBOTOMAIL_API_KEY"]
MAILBOX_ID = os.environ["ROBOTOMAIL_MAILBOX_ID"]  # e.g. mbx_shopping

def _headers():
    return {
        "Authorization": f"Bearer {RM_KEY}",
        "Content-Type": "application/json",
    }

def send_email(to, subject, body_text, in_reply_to=None):
    payload = {"to": to, "subject": subject, "bodyText": body_text}
    if in_reply_to:
        payload["inReplyTo"] = in_reply_to

    resp = requests.post(
        f"https://api.robotomail.com/v1/mailboxes/{MAILBOX_ID}/messages",
        headers=_headers(),
        json=payload,
        timeout=20,
    )
    resp.raise_for_status()
    return resp.json()
```

`reply_to_email` is the same call with `inReplyTo` set to the inbound message ID. Do not reimplement it as a second HTTP path. One function, one optional field, less to go wrong.

## Step 3: give Hermes the tool schemas

Descriptions matter more than names here. Hermes follows explicit constraints well, so put your policy in the schema rather than hoping the system prompt holds.

```python
TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "send_email",
            "description": (
                "Send an email from the agent's own mailbox. "
                "Use reply_to_email instead when responding to a message "
                "you received. Never send to more than 3 recipients."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "to": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "Recipient email addresses.",
                    },
                    "subject": {"type": "string"},
                    "body_text": {
                        "type": "string",
                        "description": "Plain text body. No markdown.",
                    },
                },
                "required": ["to", "subject", "body_text"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "reply_to_email",
            "description": (
                "Reply within an existing thread. Requires the message_id "
                "of the message you are replying to."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "message_id": {"type": "string"},
                    "body_text": {"type": "string"},
                },
                "required": ["message_id", "body_text"],
            },
        },
    },
]
```

Then the usual loop:

```python
from openai import OpenAI

client = OpenAI(base_url="http://localhost:8000/v1", api_key="local")

def run(messages):
    while True:
        out = client.chat.completions.create(
            model="hermes",
            messages=messages,
            tools=TOOLS,
        )
        msg = out.choices[0].message
        messages.append(msg)

        if not msg.tool_calls:
            return msg.content

        for call in msg.tool_calls:
            result = dispatch(call.function.name, call.function.arguments)
            messages.append({
                "role": "tool",
                "tool_call_id": call.id,
                "content": result,
            })
```

`dispatch` parses the JSON arguments and calls the Python functions above. Return a short JSON string on success and a short JSON string on failure. Hermes recovers from `{"error": "recipient suppressed"}` far better than from a stack trace.

## Step 4: handle inbound Hermes agent email

Sending is the easy half. The reason to give an agent a mailbox at all is that replies come back.

Register a webhook for the mailbox, and inbound mail arrives as a POST to your endpoint:

```json
{
  "event": "message.received",
  "data": {
    "message_id": "...", "mailbox_id": "...", "mailbox_address": "hermes-agent@robotomail.co",
    "from": "vendor@example.com", "subject": "Re: Order #A-4521", "body_text": "...",
    "thread_id": "...", "received_at": "2026-04-17T10:00:00.000Z"
  }
}
```

Three rules that will save you an outage:

1. **Return 200 immediately, then process.** Enqueue the payload and run the Hermes loop out of band. Model inference plus tool calls can take tens of seconds, which is longer than any sane webhook timeout.
2. **Deduplicate on `message_id`.** Retries happen. A dedupe table keyed on `message_id` is the difference between one reply and five.
3. **Key agent state on `thread_id`.** Load the conversation history for that thread, append the new message, run the loop. Do not try to reconstruct context from the subject line. Details are in [threading](/docs/concepts/threading), and the end-to-end flow is in [receive and reply](/docs/guides/receive-and-reply).

Webhook registration is its own resource with its own fields, documented under [webhooks](/docs/concepts/webhooks). Set it up once per mailbox at provisioning time.

## Guardrails worth adding on day one

Hermes will do what you ask, including the parts you did not think about.

- **Recipient allowlist in the dispatch layer.** Check the `to` array against a list before the HTTP call, not in the prompt. Prompts are advisory, code is not.
- **Per-mailbox send cap.** A loop where the agent replies to its own auto-responder is the classic failure. Count sends per thread per hour and refuse past a threshold.
- **Human loop-in address.** Give the agent a `handoff` tool that emails a person and stops the loop. Cheaper than a clever escalation policy.
- **Plain text by default.** Fewer rendering surprises and better deliverability for conversational mail. See [plain text email](/blog/email-plain-text) for the tradeoff.

If your agent is going to be pulled into multi-step workflows with other agents on other addresses, read [multi-agent workflows](/use-cases/multi-agent-workflows) before you design the thread state, because the "one mailbox per role" pattern is much easier than retrofitting it later.

## FAQ

### Does this work with any Hermes deployment?

Anything exposing an OpenAI-compatible chat completions endpoint with tool calling works, whether you self-host on vLLM or use a hosted inference provider. The email side is plain HTTP and does not care what model you run.

### Can the agent read attachments people send it?

Yes. Inbound attachments are stored and retrievable by ID, which is the right pattern for agents because you avoid dumping binary blobs into the model context. See [attachments](/docs/concepts/attachments).

### Should each Hermes agent get its own address?

Usually yes. Separate mailboxes give you clean threads, per-agent rate visibility, and the ability to revoke one agent without touching the others. The reasoning is spelled out in [why agents need a real inbox](/blog/why-agents-need-real-inbox).

### How do I test before pointing real people at it?

Send between two mailboxes you own and assert on the webhook payloads your handler receives. That exercises threading, dedupe, and reply construction without involving an external recipient. [How to test email delivery](/blog/how-to-test-email-delivery) covers the checks worth automating.

Ready to wire it up? Create a mailbox, register a webhook, and give your Hermes agent an address it actually owns at [robotomail.com](https://robotomail.com).
