Email reply threading with multiple agents on one domain breaks in predictable ways — and the failure mode is subtle enough to ship to production unnoticed. A reply arrives, your webhook fires, but it lands on the wrong agent. Or threads fork. Or In-Reply-To resolves correctly but your agent has no memory of the conversation because thread state is keyed to the wrong identifier.
This post works through the specific mechanics: how Reply-To, From, Message-ID, In-Reply-To, and References interact when multiple agents operate under one domain, and what you need to implement to keep routing and threading correct.
Why sharing a domain complicates reply routing
The From header identifies the sender. The Reply-To header tells mail clients where to send the reply. When both point to the same address, there's no ambiguity. The problem starts when multiple agents send from addresses that share a domain — or worse, when outbound and inbound routing paths diverge.
Consider a workspace with three agents: scheduler, billing, and support. They all share the same registered domain. Their outbound messages go out from addresses like scheduler.acme@send.mails.ai. Inbound replies come back through scheduler.acme@in.mails.ai. If you don't set Reply-To explicitly, the mail client uses From — and depending on how the sending infrastructure rewrites that header, replies may arrive at an address your inbound pipeline doesn't watch, or at an unmonitored address that silently drops them.
The minimal correct pattern: every outbound message from every agent must carry an explicit Reply-To that maps to that agent's inbound address. Letting the MUA derive the reply target from From is acceptable when you control both ends of a single-agent system. In multi-agent setups, it's a liability.
Threading headers: what they actually do
Three headers carry threading context in SMTP. Email clients and servers use them to group messages into conversations.
Message-ID — a globally unique identifier the sending MTA assigns when the message leaves the queue. Format: <local-part@domain>. It's the primary key for threading. If your agents generate their own Message-ID values (some SDKs allow this), use a structure like <uuid.agentname@yourdomain.com> — embedding the agent name makes manual debugging significantly faster.
In-Reply-To — set by the replying client to the Message-ID of the message being replied to. A direct parent pointer. A well-behaved MUA sets exactly one value here.
References — the full ancestry chain. Each reply appends the previous References list plus the In-Reply-To value. So a thread of depth 5 has 5 entries in References. Gmail's threading algorithm uses References to handle cases where In-Reply-To is missing or malformed; Outlook primarily uses Subject with a Re: prefix, but falls back to these headers.
When an agent receives a reply and needs to continue the thread, it must set both In-Reply-To (the Message-ID of the message it's replying to) and References (the full chain). Missing either causes threading to break in some clients — replies appear as new conversations.
sequenceDiagram
participant A as Agent scheduler
participant MTA as Outbound MTA
participant U as User inbox
participant IH as Inbound handler
A->>MTA: Send msg [Message-ID: abc123 Reply-To: scheduler.acme@in.mails.ai]
MTA->>U: Delivered
U->>IH: Reply [In-Reply-To: abc123 References: abc123 To: scheduler.acme@in.mails.ai]
IH->>A: Webhook event reply.received [thread_id source_message_id]
Address disambiguation across agents
The From address is what your outbound MTA uses for envelope routing and DKIM signing. The Reply-To address is what routes the human's reply back to you. These serve different purposes and should be set independently.
For a multi-agent setup, the pattern is:
From: The agent's display address. Used for DKIM alignment, bounce handling, SPF evaluation.Reply-To: The agent-specific inbound address. Must be monitored. Must be unique per agent.
The uniqueness requirement for Reply-To is the load-bearing piece. If scheduler and billing both set Reply-To to a shared support@yourdomain.com, you've lost the ability to route replies by agent at the header level. You'd have to parse the original message's subject or body to infer which agent should handle the reply — fragile, and unnecessary.
If your infrastructure assigns each agent a unique inbound address (e.g., scheduler.acme@in.mails.ai versus billing.acme@in.mails.ai), set Reply-To to that address explicitly on every outbound send. Never rely on the MTA to infer it.
Thread state keying
Thread state in your agent's memory should be keyed to thread_id, not to Message-ID. Here's why: a user might reply to two different messages in the same conversation, creating a fork. The thread_id your infrastructure assigns groups all messages in a conversation — it's the right grain for state. Message-ID is the right grain for a single message lookup.
When a reply.received event arrives, extract:
thread_id— to load conversation statesource_message_id— to know which specific message was replied to, if the thread has branchesdata.from.address— to know who replieddata.extracted_text— the reply body, already parsed
Storing your agent's outbound Message-ID values and mapping them back to thread_id lets you handle cases where a user replies to an older message mid-thread — a common UX pattern that breaks agents tracking only the most recent message.
Webhook routing for multiple agents
With a shared domain, inbound webhook delivery must be agent-aware. The webhook endpoint receives an event with agent_id as a top-level field. Your router uses this to dispatch to the correct handler.
import { mails } from "@mailsai/sdk";
const hello = mails.agent("hello");
hello.onReply((event) => {
const { thread_id, source_message_id, agent_id, data } = event;
const replyText = data.extracted_text;
const sender = data.from.address;
// Load thread state keyed to thread_id
// Dispatch to correct handler based on agent_id
routeToHandler(agent_id, thread_id, replyText, sender);
});
Note: hello.onReply registers a handler for reply.received events on the hello agent specifically. Each agent you create gets its own .onReply and .onMessage handlers. The SDK handles dispatch; you don't need to inspect agent_id to separate events between agents when you've registered handlers per-agent.
Where agent_id becomes critical is if you're building a shared webhook receiver — a single HTTP endpoint that receives events for all agents (a common pattern when deploying multiple agents in one service). In that case, agent_id is your routing key.
The From/Reply-To split in practice
Here's the concrete problem this split solves. Suppose your billing agent sends an invoice notification. The From header is billing.acme@send.mails.ai — the outbound address your MTA uses. The user hits Reply. Without an explicit Reply-To, their client addresses the reply to billing.acme@send.mails.ai, an outbound-only address that likely has no MX record pointing to your inbound pipeline.
The reply bounces, routes to a catch-all that drops it, or arrives at a generic inbox your agent doesn't watch. You never get the reply.received event.
With Reply-To: billing.acme@in.mails.ai explicitly set, the user's reply arrives at the inbound pipeline, triggers the webhook, and your agent gets the event with full threading context intact.
For agents built on Mails.ai's email infrastructure, this split is handled by design — each agent has distinct send and inbound addresses, and threading headers are set on client.messages.reply() calls automatically. The Re: prefix, In-Reply-To, and References are set server-side based on the original message ID, so you don't have to reconstruct the header chain manually.
Domain-level DKIM and SPF when agents share a domain
Authentication adds another layer of complexity. If multiple agents share a domain, they also share DKIM keys and SPF records. That's usually fine — DKIM signs at the domain level, and a valid signature from scheduler.acme@send.mails.ai and billing.acme@send.mails.ai both pass as long as the d= tag matches the signing domain.
What matters for multi-agent correctness:
SPF alignment: The
Return-Path(envelope sender) domain must match theFromdomain for strict alignment, or be a subdomain for relaxed. Shared sending infrastructure often uses a fixedReturn-Pathdomain. Verify this matches your DMARC policy.DKIM selector per agent: Not required, but useful for debugging. If you can assign a distinct DKIM selector per agent (or per agent group), you can correlate signing failures to specific agents without parsing message headers manually.
Reply-Todoesn't affect authentication:Reply-Tois a routing header, not an authentication signal. SPF evaluates theMAIL FROMenvelope address. DKIM signs headers and body. DMARC checks alignment betweenFromdomain and the SPF/DKIM domains. None of them look atReply-To.
For a deeper treatment of authentication setup, the SPF/DKIM/DMARC guide for automated senders covers the alignment requirements in detail.
Handling thread forks and out-of-order replies
Users reply to old messages. They forward threads. They CC new people mid-conversation. Each creates a fork or introduces a participant your agent hasn't seen.
Thread fork: a user replies to message #2 when message #4 already exists. The In-Reply-To on the new reply points to Message-ID of message #2. If your agent loads state from thread_id and processes messages in arrival order, the fork is usually harmless — you just see a reply that references a non-terminal node. Store the full References chain so you can reconstruct which branch the user is on.
New CC participant: the reply.received event's data.from.address will be the original sender. New CC participants appear in data.to or data.cc. If your agent tracks participants, add them on each event rather than only on the first message.
Out-of-order arrival: SMTP doesn't guarantee delivery order. Two replies sent seconds apart might arrive reversed. Use data.date from the event (the RFC 2822 Date header) for temporal ordering, not arrival timestamp. Build your state machine to handle append-and-reorder rather than append-only.
Cross-agent handoffs within one thread
A common pattern: a triage agent handles first contact, then hands off to billing or support based on intent. The user expects to stay in the same email thread.
To continue the thread from a different agent, the new agent needs:
- The original
Message-IDto setIn-Reply-To - The full
Referenceschain - The correct
Reply-Tofor the new agent's inbound address
This means your state store must carry threading headers across agent boundaries, not just conversation text. A minimal handoff payload looks like:
type HandoffContext = {
thread_id: string;
last_message_id: string; // source_message_id from the event
references_chain: string[]; // accumulated Message-IDs
original_sender: string;
subject: string;
};
The receiving agent uses last_message_id as in_reply_to_message_id on its first reply, reconstructs References from references_chain, and sets its own Reply-To. The user sees a continuous thread. Without the references_chain, the handoff reply appears as a new conversation in many clients.
For inbound email parsing and classification that feeds these handoff decisions, the classification intent (available on first-contact message.received events) gives you a structured signal — schedule_meeting, ask_question, request_info — to decide which agent should own the thread without parsing free text.
Frequently Asked Questions
Why does Reply-To matter more than From for agent routing?
From is what your outbound MTA signs and delivers from. For infrastructure reasons (bounce handling, DKIM alignment), it often points to a sending-only address with no MX record for inbound. Reply-To overrides where human clients send their replies. If Reply-To is missing, clients fall back to From — and if that address has no inbound pipeline, replies are lost. Explicit Reply-To per agent is the only reliable way to route replies in a multi-agent setup.
Can two agents share the same Reply-To address and use subject parsing to route replies?
You can, but it's fragile. Subject lines get modified by mail clients (Re: Re: accumulation, trimming), users change subjects, and forwarded messages lose subject context entirely. Reply-To routing by address is deterministic. Subject-based routing is a heuristic that fails in edge cases. Use address routing as the primary signal.
How do I preserve threading when handing off between agents?
Store the full References chain and the last outbound Message-ID in your thread state, and pass them in the handoff payload. The receiving agent sets In-Reply-To to the last Message-ID and reconstructs the References header from the stored chain. Without this, the handoff reply appears as a new thread in most mail clients.
Does DKIM break if agents on the same domain use different Reply-To addresses?
No. Reply-To is not signed by DKIM. DKIM signs specific headers (From, Subject, To, Date, and optionally others) plus the message body. Reply-To changes don't invalidate the DKIM signature, and they don't affect SPF or DMARC evaluation. You can set any valid address in Reply-To without authentication consequences.
What happens to threading when a user replies from a different email client?
Most modern clients (Gmail, Outlook, Apple Mail) set In-Reply-To and References correctly. Where it breaks: some mobile clients strip References and only set In-Reply-To; some older enterprise clients use subject-line threading only. Build your thread matching to try In-Reply-To first, then References, then fall back to thread_id from your own state if both are absent. Never rely exclusively on a single header.
How should I key thread state in a multi-agent system?
Key by thread_id, not by Message-ID or by agent. thread_id is stable across the entire conversation regardless of which agent sends or receives a given message. Within a thread, use source_message_id to identify specific messages when you need to handle branched replies or audit specific exchanges.
Related guides: Email Threading Across Provider Boundaries Explained, Reply-To vs From in Email: Route Agent Replies Right, Inbound Email Parsing: Webhooks, Classification & Events and Email OAuth Token Management for AI Agents.
Explore the product: Inbound email API, Sender reputation, Email classification, All features and Pricing.
