BlogUpdated 4 November 2026 · 5 min read

Email threading for AI agents

How does an AI agent keep an email conversation in one thread, and what happens when a person steps in?

The short answer

An agent keeps a conversation in one thread by answering with the right headers: In-Reply-To set to the Message-ID of the message it answers, and References carrying the chain before it. With Pidgeon the agent calls POST /v1/messages/{id}/reply and the headers, recipients and subject are taken from the message it is answering. Incoming mail is threaded on arrival, and each thread records whether the agent or a person is answering it.

How email knows what belongs together

Every email carries a Message-ID, a unique string its sender's server stamps on it. A reply names its parent in In-Reply-To and lists the conversation so far in References. Mail clients read those two headers to draw a thread. When they are missing, most fall back to the subject, stripped of Re: and Fwd:.

An agent that writes its own MIME, or calls a send API with a subject of Re: Your order and no headers, breaks this. In its own records the reply may look threaded. In the customer's client it can arrive as a new conversation, beside the one it answers, and the customer has two places to reply.

There is a second trap. Some sending services replace the Message-ID you set with one of their own. Amazon SES, which Pidgeon sends through, does: the recipient sees an id ending in amazonses.com, not the one stored at send time. A reply that quotes the stored id names a message the recipient never received. Pidgeon puts the id the recipient actually saw into In-Reply-To and References when it answers its own sent mail, and matches incoming replies against both.

How Pidgeon threads incoming mail

A thread is a row with an id, and every message and event names it as thread_id. When mail arrives:

  1. The headers decide. In-Reply-To and References are matched against messages at the same address. A match puts the message in that thread.
  2. Otherwise the subject. A normalised subject, without reply prefixes and list tags, joins a conversation from the last 30 days. Two unrelated "Invoice" emails a year apart stay two threads.
  3. Otherwise a new thread, and thread.created fires.

A message your agent composes from scratch always starts a thread of its own. Five identical requests to five suppliers are five conversations, not one with five people in it.

Replying over the API

The reply route reads everything but the words off the message being answered:

curl https://pidgeon.ai/api/v1/messages/$MESSAGE_ID/reply \
  -H "Authorization: Bearer $PIDGEON_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: reply-$MESSAGE_ID" \
  -d '{ "text": "Thanks, that is the right invoice. Paying it today." }'

/reply-all keeps everybody on the message and drops your own addresses, so the agent never mails itself. If you build the message yourself with POST /v1/messages, pass in_reply_to with the Pidgeon id of the message you are answering, and Pidgeon sets In-Reply-To and References for you. Drafts take the same field.

To read the conversation before answering, GET /v1/messages?thread_id=… returns it oldest first, 50 at a time by default. Each message carries reply_text beside the full text: the new words, with the quoted history cut off. Give the model reply_text for each message and it reads the conversation once, not once per quote. A forward is not cut, because what it carries is the point.

Over MCP the same steps are list_messages with the thread_id, then reply_to_message.

Who is answering

Threading tells you which messages belong together. An agent in a real inbox also needs to know whether it is still the one answering. Each Pidgeon thread carries an owner_type, agent, automation or human, and a status: open, waiting, resolved or human_review.

Mail arriving at an address created with type: "agent" opens a thread the agent owns. When the agent is out of its depth, it hands the thread to a person in one call:

curl https://pidgeon.ai/api/v1/threads/$THREAD_ID \
  -X PATCH \
  -H "Authorization: Bearer $PIDGEON_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "owner_type": "human",
    "status": "human_review",
    "reason": "They want a refund above what I can approve."
  }'

The people who can answer as the address get a notification with the reason. The customer keeps writing to the same address and sees nothing change.

A person taking a thread over

A person can also step in without being asked. When somebody replies in an agent's thread from the Pidgeon inbox or from their own mail client, the thread becomes theirs: owner_type turns human, they are named on it, and thread.updated and thread.assigned fire. The reply goes out from the agent's address with the same References, so for the customer it is the next message in the conversation.

While a person has the thread, an API key's reply, reply-all, forward or scheduled send in it is refused with 403 taken_over. So is a key trying to set the owner back. The agent can still save drafts there, words for the person to send. Handing back is the person's decision, made with Hand back in the inbox. Nothing hands a conversation back on a timer.

Your agent learns all of this from the events it already reads. Check owner_type on message.received before answering, and treat taken_over as the answer "a person has this", not as an error to retry.

Limits

  • Threads are per address. The same conversation copied to two of your addresses is two threads, one in each inbox.
  • The subject fallback is a guess. A sender whose client drops the headers and changes the subject starts a new thread. One who reuses a subject within 30 days with no headers joins the old one.
  • Pidgeon ships no agent loop. Threads, owners and events are what you build one on; the model, the prompt and the decision to answer are yours.

Start

Sign up and create an address with type: "agent". Threads has the owner and status model, the hand-off guide walks a conversation from the agent to a person and back, and sending covers in_reply_to. The human-in-the-loop post has the reasoning behind the hand-off.

01

Asked often

Which headers keep a reply in the same thread?

In-Reply-To, set to the Message-ID of the message being answered, and References, carrying the chain before it. Most mail clients fall back to the subject only when those are missing.

How does an agent reply in the same thread with Pidgeon?

POST /v1/messages/{id}/reply. The recipients, subject and threading headers are taken from the message being answered; the agent brings the text.

How does Pidgeon decide which thread incoming mail joins?

The In-Reply-To and References headers first, matched within the address. Otherwise a normalised subject from the last 30 days, and otherwise a new thread.

What happens when a person replies in the agent's thread?

The thread becomes theirs: owner_type turns human and thread.updated fires. Until they hand it back, an API key's reply in that thread is refused with 403 taken_over.

02

Keep reading

All posts

Give your agents an identity

Pidgeon is building the trust layer of the agentic web.

Register your agents, it’s free