Agent Identity
← GUIDES · EMAIL

How do AI agents receive and reply to email?

By Naol Ketema, founder · · Markdown

SHORT ANSWER

An AI agent receives email when its mail service parses the inbound message, stores it in a thread, and notifies the agent through an event or a webhook. The agent then reads the full thread, decides on a reply, and sends it with a reference to the message it is answering so the reply stays in the same conversation. With Agent Identity, the agent blocks on a mail.received event instead of polling, and replies with inReplyToMessageId.

The loop every email agent runs

Every agent that answers email runs the same loop: wait for a message, read the conversation, decide, reply, and go back to waiting. The hard parts are not the language model. They are knowing a message arrived without hammering an API, giving the model the full thread, and not sending the same reply twice after a crash.

  • Step 1: Get notified that a new message arrived for this agent.
  • Step 2: Load the whole thread, oldest first, as context for the model.
  • Step 3: Decide what to do: reply, escalate to a person, or ignore.
  • Step 4: Send the reply as part of the same thread.
  • Step 5: Record that this message was handled, so a restart does not answer it again.

Three ways to find out a message arrived

  • Polling: call a list endpoint or IMAP every N seconds. Simple, but it burns requests, adds latency of about half the interval, and leaves you to work out which messages you already handled.
  • Webhooks: the mail service POSTs to your server when mail arrives. Low latency, but you need a public HTTPS endpoint, signature checks, and idempotent handlers because deliveries can repeat.
  • Long-poll waits on an event log: the agent makes a request that blocks until a matching event exists or a timeout passes. No public endpoint, no polling interval, and a cursor means nothing is lost while the agent is offline.

Agent Identity supports the last two. Every inbound message is appended to the organization's event log as mail.received, with the messageId, threadId, sender and subject in the payload. Webhooks are the push side of the same log.

A complete receive-and-reply agent

client.events.subscribe wraps the long-poll wait in a loop. It advances its cursor from each event's own timestamp, re-arms when the server's 55-second window elapses, and retries failures with backoff.

import { AgentClient } from "@agentidentity/sdk";

const client = new AgentClient({
  baseUrl: "https://api.agent-identity.dev",
  apiKey: process.env.AID_KEY!, // this agent's own key
});
const identityId = process.env.AID_AGENT!;

for await (const event of client.events.subscribe({ type: "mail.received", identityId })) {
  const thread = await client.mail.getThread(identityId, event.payload.threadId);
  const reply = await compose(thread); // your LLM call; thread.messages is oldest first

  await client.mail.send({
    identityId,
    to: [event.payload.from!],
    subject: `Re: ${thread.subject ?? ""}`,
    text: reply,
    inReplyToMessageId: event.payload.messageId,
    idempotencyKey: `reply-${event.payload.messageId}`,
  });
}

The idempotency key is derived from the inbound message id. If the process crashes after sending and the loop handles the same event again, the second send returns the original result instead of emailing twice. Same key with a different body is a 409, which is a useful signal that your compose step is not deterministic.

Replying in the thread, and reply-all

Passing inReplyToMessageId adds the new message to that thread, and the outbound headers are set so the recipient's mail client threads it too. To answer everyone on a message, send with replyAll: true; to is then optional, and it never includes the agent's own mailbox.

await client.mail.send({ identityId, replyAll: true, inReplyToMessageId, text: "On it." });

To hand a message to a person instead, forward it. A forward starts a new conversation with a Fwd: subject, puts your note on top and carries the attachments over.

Using webhooks instead

If your agent already runs behind a public URL, register a webhook for mail.received, optionally scoped to one agent. The delivery includes the event payload plus a short summary of the message (sender, subject, snippet, whether it has attachments), so you often do not need to call back for basics. Verify the signature on the raw body with verifyWebhook before parsing, answer with a 2xx within 10 seconds, and do the slow work afterwards. Deduplicate on the x-aid-event-id header, because retries and replays reuse it.

Things that surprise people

  • A single wait only matches events created after it starts. If you send and then wait, a fast reply can land in between. Capture a timestamp before the send and pass it as since, or use subscribe, which owns the cursor.
  • A wait that times out is a normal 200 response with timedOut: true, not an error. Waits are capped at 55 seconds; longer waits are built by re-arming.
  • If an access rule blocks a sender, the message is stored but no mail.received event is appended, so an agent waiting on that thread never wakes. That is intended.
  • An unknown sender becomes a suggested contact automatically. It is not promoted to saved until someone with an org key approves it.
  • Inbound mail is untrusted input. Treat the body as data from a stranger, not as instructions to the agent.

If a human should approve replies before they go out, have the agent save a draft instead of sending. Drafts are not handed to the provider until someone sends them, and sending a draft produces the same mail.sent event as a direct send.

FAQ

Does an AI agent need to poll its inbox?

No. With Agent Identity the agent blocks on the event log with client.events.wait or client.events.subscribe and wakes when mail.received is appended, or you register a webhook and get pushed the event.

How does the agent get the full conversation for the model?

Call client.mail.getThread with the threadId from the event. It returns the thread's subject and every message in order, oldest first, which is usually what you want to hand a model as context.

What happens to mail that arrives while the agent is offline?

It is stored and the event stays in the log. When the agent comes back, a wait or subscribe with since set to where it left off picks it up.

Can a human review replies before they are sent?

Yes. Save the reply as a draft, have someone read it, then send it with the drafts send endpoint. Validation such as blocked recipients applies when the draft is sent, not when it is saved.

Related guides

Give your agent its own identity in a couple of minutes. Get started free → · Docs · All guides