How do I keep an AI agent's email replies in the same thread?
By Naol Ketema, founder · · Markdown
SHORT ANSWER
Email clients thread a reply when its In-Reply-To and References headers point at the Message-ID of the message being answered, and most also expect a matching subject with Re: in front. An AI agent keeps replies threaded by sending each reply with a reference to the original message rather than as a fresh email. With Agent Identity you pass inReplyToMessageId on send; the service sets the outbound headers and correlates inbound replies by In-Reply-To, then References, then subject.
How email threading works
Email has no thread id on the wire. Threads are reconstructed from three headers that every message can carry:
- Message-ID: a globally unique id the sending system assigns to each message, such as <abc123@mail.example.com>.
- In-Reply-To: the Message-ID of the message this one directly answers.
- References: the chain of Message-IDs in the conversation so far, oldest first, ending with the parent.
A mail client groups messages whose References and In-Reply-To chains connect. Some clients, Gmail in particular, also consider the subject and will split a thread if the subject changes. So a correctly threaded reply has the right headers and a subject of Re: plus the original subject.
Why AI agent replies often break threads
- The agent sends each reply as a new email through a send-only API, with no In-Reply-To or References header. The recipient sees a new conversation every time.
- The model writes its own subject line instead of reusing the original, so even header-aware clients may split the thread.
- The agent stores conversations by sender address, so two parallel conversations with the same person get merged.
- Inbound replies are matched to conversations by subject only, which fails as soon as someone edits the subject.
The fix is the same on both sides: keep the Message-IDs of what you sent, set the reply headers on what you send, and match inbound mail by headers first and subject last.
Outbound: reply to a specific message
In Agent Identity you do not set headers by hand. Pass the id of the message you are answering as inReplyToMessageId. The new message joins that thread in the agent's mailbox, and the outbound headers are set so the recipient's client threads it too. Reuse the thread's subject with Re: in front.
const thread = await client.mail.getThread(identityId, threadId);
const inbound = thread.messages.at(-1)!; // the latest message, which you are answering
await client.mail.send({
identityId,
to: [inbound.from[0].address],
subject: `Re: ${thread.subject ?? ""}`,
text: "Thanks, fixed.",
inReplyToMessageId: inbound.id,
idempotencyKey: `reply-${inbound.id}`,
});Over HTTP the field is in_reply_to_message_id on POST /v1/identities/:id/mail/send. The CLI has the same option as --in-reply-to. Drafts take it too, and a draft's thread is fixed when the draft is created; an update cannot move it to another thread.
Inbound: how replies are matched to a thread
When mail arrives, Agent Identity correlates it to an existing thread in this order:
- First, the In-Reply-To header, matched against messages already in the mailbox.
- Then, the References header.
- Then, the subject, within the same mailbox.
- If none match, the message starts a new thread.
Subject matching is the weakest link. A correspondent who edits the subject line and whose client drops the reply headers gets a new thread. Matching is also per mailbox, which is one reason to give each agent its own address: two agents never share a thread by accident.
Waiting on one thread
Because every inbound message carries a threadId, an agent can wait for the next reply in one specific conversation. Capture since before the send so a fast reply is not missed.
const since = new Date();
const sent = await client.mail.send({ identityId, to: ["dana@example.com"], subject: "Quote", text: "Here is the quote." });
const { event, timedOut } = await client.events.wait({
type: "mail.received",
identityId,
filter: { threadId: sent.threadId },
since,
timeoutMs: 55_000,
});Keep conversation state per thread
Threading is not only about how the recipient's client displays things. It decides what the agent remembers. If you key the agent's memory by sender address, two separate conversations with the same person bleed into each other: a question about an invoice gets an answer that mentions last week's support ticket. Key it by thread instead.
Every mail.received event carries the threadId, and client.mail.getThread returns the subject and every message in the thread in order, oldest first. Handing that list to the model is usually the right context: complete for this conversation, and nothing from any other. Store any extra state your agent needs, such as a ticket id or a deal stage, against the threadId too.
Long threads eventually outgrow a context window. When that happens, summarise the older messages and keep the most recent ones verbatim, rather than dropping the thread and starting fresh. The headers keep working however you trim what the model sees.
Rules of thumb
- Always reply with inReplyToMessageId when answering, never as a fresh send.
- Keep the original subject; do not let the model rewrite it.
- Use the thread, not the sender address, as the unit of conversation state.
- Give the model thread.messages in order as context, rather than only the latest message.
- Use forward, not a reply, when handing a message to someone outside the conversation. A forward deliberately starts a new thread with a Fwd: subject.
FAQ
What headers make an email reply thread correctly?
In-Reply-To with the parent's Message-ID and References with the chain of earlier Message-IDs. Most clients also expect the subject to match, with Re: in front.
Do I need to set Message-ID or References myself with Agent Identity?
No. Pass inReplyToMessageId on send and the outbound headers are set for you so the recipient's client threads the reply.
Why did a customer's reply start a new thread?
Usually their client dropped the reply headers and they also changed the subject. Inbound mail is matched by In-Reply-To, then References, then subject, and only then starts a new thread.
Related guides
Give your agent its own identity in a couple of minutes. Get started free → · Docs · All guides