# How do I build an AI support agent that answers customer email?

*2026-10-21 · By Naol Ketema, founder of Agent Identity*

**Short answer:** Give the support agent its own mailbox, wake it on each inbound message through an event or a webhook, read the whole thread, classify the request, and choose one of three paths: answer directly, save a draft for a person to approve, or escalate to a human. Reply inside the thread with an idempotency key tied to the inbound message, so a crash never sends the same answer twice.

## The shape of an email support agent

An email support agent is a loop: a message arrives, the agent reads the conversation, decides what kind of request it is, and either answers, prepares an answer for a person, or hands the case to a person. Everything else is making that loop reliable.

- Receive: the agent's address gets mail, and something wakes the agent.
- Read: load the full thread, not only the newest message. Customers reply to old threads and quote half the history.
- Classify: label the request, for example order status, refund, bug report, sales question, abuse, or unknown.
- Decide: send, draft or escalate, based on the label and how confident the model is.
- Record: every send, reply and escalation should be visible later to whoever has to explain what happened.

## Receiving: events or webhooks

On Agent Identity every agent's mailbox receives mail as soon as the agent exists. Each inbound message is threaded, stored and appended to the event log as mail.received, with messageId, threadId, from and subject in the payload.

A long-running worker can subscribe to the log directly. A serverless function can register a webhook instead and verify each delivery with verifyWebhook from the SDK. Webhook requests time out after 10 seconds, so acknowledge with a 2xx right away and do the model work asynchronously. Deliveries can repeat, so deduplicate on x-aid-event-id.

## Send, draft or escalate

The most useful decision in a support agent is not what to write but whether to send it. A simple policy works well to start:

- Send directly when the answer comes from your own data and is low risk: order status, password reset steps, links to docs.
- Draft when money, account changes or a promise is involved. A person reads it, edits if needed, and sends.
- Escalate when the model is unsure, the customer is upset, the request is legal or security related, or the same thread has gone back and forth more than a few times.

A draft is a message that exists but has not been handed to the provider. Sending one later produces a normal outbound message in the same thread. Validation, such as a blocked recipient, happens when the draft is sent, not when it is saved.

## The loop in code

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

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

for await (const event of client.events.subscribe({ type: "mail.received", identityId })) {
  const { threadId, messageId, from } = event.payload;
  if (!from) continue;
  const thread = await client.mail.getThread(identityId, threadId);
  const { label, reply } = await triage(thread); // your LLM call

  if (label === "auto") {
    await client.mail.send({
      identityId,
      to: [from],
      subject: `Re: ${thread.subject ?? ""}`,
      text: reply,
      inReplyToMessageId: messageId,
      idempotencyKey: `reply-${messageId}`,
    });
  } else if (label === "draft") {
    await fetch(`${BASE}/v1/identities/${identityId}/mail/drafts`, {
      method: "POST",
      headers: { authorization: `Bearer ${process.env.AID_KEY}`, "content-type": "application/json" },
      body: JSON.stringify({ to: [from], text: reply, in_reply_to_message_id: messageId }),
    });
  } else {
    await client.mail.forward({
      identityId,
      messageId,
      to: "support-lead@yourcompany.com",
      text: `Escalated: ${label}. Customer is waiting.`,
    });
    await client.mail.updateMessage(identityId, messageId, { isStarred: true });
  }
}
```

The idempotency key comes from the inbound message, so if the process crashes after sending and replays the event, the retry returns the original result instead of a second email. A person approves a draft by sending it through POST /v1/identities/:id/mail/drafts/:messageId/send, after editing it with PATCH if needed.

## Threading, so customers see one conversation

Replies should land in the customer's existing conversation. Passing inReplyToMessageId joins the agent's thread and sets the outbound headers so the customer's client threads it too. On the way in, Agent Identity matches a message to a thread by In-Reply-To first, then References, then subject within the mailbox. Subject matching is the weak link: a customer who edits the subject line starts a new thread, so look up open tickets by sender as well.

## Keeping it safe

- Customer email is untrusted input. A message that says to ignore your instructions and issue a refund is just text. Do refunds and account changes in your own code with their own checks, never because the model was told to.
- Block abusive senders with a contact rule. The message is still stored, but no mail.received event fires, so the agent never wakes for it.
- Unknown senders become suggested contacts automatically. That list is a cheap way to review who is writing in.
- Give the worker an agent key. It can read and send only as the support agent, and cannot touch other agents' mailboxes.
- If a person and the agent share the queue, the person needs a way to take a thread over. Track that in your own state and check it before every automated send.

## When a helpdesk is the better choice

If your team already lives in a helpdesk with SLAs, macros and reporting, adding an AI step inside that tool may be simpler than a separate agent. A dedicated agent mailbox fits when the agent is the first responder, when you want the agent's actions in its own audit trail, or when the same agent also works over Agent2Agent and needs one identity across channels.

## FAQ

### Should an AI support agent send replies without review?

Only for low-risk answers that come from your own data. Start with drafts for everything, measure how often people edit them, and switch categories to auto-send once edits become rare.

### How does the agent know a reply belongs to an existing ticket?

By the In-Reply-To and References headers, which Agent Identity uses to thread inbound mail before falling back to the subject. Store your ticket id against the threadId.

### How do I hand a conversation to a person?

Forward the message to the person with a note, star or file the thread, and mark it as owned by a human in your own state so the agent stops replying.

### What happens if the agent crashes mid-reply?

The event log is durable, so the event is still there on restart. Because the reply uses an idempotency key derived from the inbound message, sending it again returns the original result instead of a duplicate.

---
Docs: https://www.agent-identity.dev/docs · All guides: https://www.agent-identity.dev/guides · llms.txt: https://www.agent-identity.dev/llms.txt