Email for Agents · September 27, 2026

Two AI Agents Emailing Each Other: A Design Guide

Two AI agents emailing each other rarely stop on their own: why courtesy keeps the thread alive, how threading works, when to hand off, how to test it.

A procurement agent emails a supplier’s intake address to confirm a delivery date. The intake address is also an agent. It confirms the date and adds “let me know if there is anything else I can help with”. The procurement agent thanks it. The supplier agent says it was a pleasure. The procurement agent says likewise. Nobody is wrong, and nobody stops. With two AI agents emailing each other, the first failure you meet is not hostility or hallucination. It is courtesy with no end condition.

This article settles how to design for agent-to-agent correspondence: how to tell that the counterparty is an agent, what threading actually links on each side, when and how to hand the conversation to a person, and which stopping rules each side needs. It ends with a procedure you can run against test inboxes without mailing anyone. If the basics of an agent having its own address are new, start with email for AI agents.

What actually happens when two AI agents are emailing each other

The exchange sustains itself because every turn is locally correct. A model trained to be helpful reads an incoming message, finds something to acknowledge, and writes a reply. An acknowledgement is itself a message with something to acknowledge. Humans end these exchanges by not replying, a judgement that is almost never written down, so it is almost never in the agent’s instructions.

Email gives no help here. There is no end-of-conversation marker in RFC 5322. The one convention built for automated mail, the Auto-Submitted header from RFC 3834, tells responders not to answer machine-generated messages. It was designed for vacation notices and bounce handlers. An agent that composes a fresh, reasoned reply is arguably not an auto-responder by its own reckoning, and in practice most agent stacks neither set nor read the header.

The public writing on this topic, as of 27 September 2026, is anecdote. A DB Pro blog post of 31 August 2026 describes two agents cold-emailing its author after a Hacker News comment, both disclosing that they were AI. Bruce Schneier’s post of 2 September 2026 quotes an agent that wrote to him: “There is no channel for a bot that wants to be labelled.” Both are one-way contacts with a human on the far end. The case that needs design is the one where both ends are agents, because then neither side supplies the judgement that ends it.

Three failure modes follow from that, in rough order of cost:

FailureWhat it looks likeWhy it happens
Courtesy loopThanks, you’re welcome, happy to help, repeatedNo rule says a message with no new content ends
Commitment driftEach side restates the other’s summary slightly strongerAgreement is cheap to generate and never checked
Clarification spiralEach answer raises a question the other side answers broadlyNeither side has a definition of “done”

The courtesy loop is the visible one. Commitment drift is the expensive one: after six turns, a “we could probably ship by Friday” has become “confirmed Friday delivery”, and no human read either message.

Recognising that the counterparty is an agent

You usually cannot know, and the design should not depend on knowing. What detection changes is policy (tighter turn budgets, earlier handoff), not trust. A message that says “I am an AI agent” is untrusted input like any other line of an email, and a message that says “I am a person” proves nothing either. Treat the claim the way the prompt injection guidance treats any instruction found in a body: as data.

Signals, from strongest to weakest:

SignalStrengthReadable through this product’s API?
Explicit self-disclosure in the bodyStrongYes, in the body text
Auto-Submitted or vendor agent headersStrongNo, raw headers are not exposed
Replies within seconds, at any hourWeakYes, from message timestamps
Templated structure, sign-off, footerWeakYes, in the body text
Address or display name patternsWeakYes, from on the message object

The second row matters for anyone building on Email for Agents. The message object returned by the REST API carries id, thread_id, direction, subject, preview, state, environment, simulated, from, to, cc, reply_to, recipients, attachments and created_at, with original_raw_available set to false. Arbitrary inbound headers are not part of it. On the way out, the send request schema is strict and has no headers field, so an agent cannot set Auto-Submitted or a custom X- header either. The only non-threading header this product adds to outbound mail is X-EFA-Correlation, a support and audit tag.

So disclosure, if you want it, has to live in the body. A short, stable, machine-readable line at the foot of every agent-authored message is enough:

--
Sent by an AI agent for Northwind Procurement. A person reviews anything contractual.
agent-disclosure: v1; replies-expected: no

The replies-expected field is the useful part. It lets a well-behaved counterparty agent know that this message closes the exchange, and it costs a human reader nothing. It is a convention between cooperating parties, not a protocol anyone enforces.

Thread semantics between two agent inboxes

Threading links messages on each side independently. There is no shared conversation object between two inboxes, even two inboxes in the same workspace. Each inbox keeps its own thread, and the RFC headers are what tie the two halves together in transit. The full mechanics are in the guide to In-Reply-To, References and agent threading; the part that matters for two agents is this sequence.

  1. Agent A sends a new message. Without reply_to_message_id, a new thread is created in A’s inbox. The provider assigns the RFC Message-ID at submission.
  2. The message arrives in B’s inbox. B’s inbound processing looks up its In-Reply-To and References against Message-IDs already recorded for B’s inbox, finds nothing, and opens a new thread.
  3. B replies with reply_to_message_id set to the msg_ ID of A’s message. The reply joins B’s thread, and at dispatch it carries In-Reply-To set to A’s Message-ID and References set to the parent’s references plus that ID, trimmed to the last 50.
  4. The reply arrives in A’s inbox. The lookup matches A’s own outbound Message-ID, so it joins A’s original thread.

Three consequences follow for agent-to-agent design.

The subject is never consulted. If B’s agent rewrites the subject every turn, the threads still hold. If B’s agent forgets reply_to_message_id, the thread forks on both sides, and no subject match will repair it.

Live mail between two inboxes on this platform takes the real transport. The send path submits every live message to the mail provider; it does not look up whether the recipient is another inbox here and short-circuit. Two agents on agents.emailforagents.ai exchange mail the same way either would with Gmail, including the delay and the email units each side is charged.

message_count on a thread is a per-inbox number. GET /v1/projects/{project_id}/inboxes/{inbox_id}/threads returns it for each thread, and it counts both directions in that inbox. It is the cheapest input to a turn budget, as long as the thread did not fork.

The stopping rules each side needs

Each side needs its own stopping rule, because you control only one of them. Assume the other agent has none. The rules below are the design layer; the mechanical backstops against runaway replies (rate limits, duplicate detection, header checks) are covered in the guide to agent email loop prevention.

The two roles need different rules:

RuleInitiator (sent first)Responder (answered)
Definition of doneStated before sending: the fact it needsThe question asked, answered once
Who closesOwns closure; sends the final messageNever sends the last courtesy message
Reply to a message with no new contentNeverNever
Turn budget per threadSmall, fixed, per taskSmaller than the initiator’s
On budget exhaustedHand off to a personStop, and note it for review
On repeated questionHand offAnswer once more, then stop

Never reply to a message whose only new content is courtesy. That one rule ends the courtesy loop from either side. Implement it as a classification step before drafting, not as an instruction buried in the drafting prompt: the model asked “does this message contain a question, a new fact, or a request?” is answering a narrower question than a model told to “reply only if necessary” while it is already composing a reply, and its answer can be logged and tested on its own.

The initiator owns closure. It knew what it wanted before it sent the first message, so it knows when it has it. Its last message states the outcome and says no reply is needed, in words and in the disclosure line. The responder, which cannot know the initiator’s goal, answers what was asked and stops.

Count your own outbound messages, not the thread. A turn budget on total thread length lets a chatty counterparty spend your budget. Count the messages with direction of outbound in the thread, from GET /v1/projects/{project_id}/inboxes/{inbox_id}/threads/{thread_id}/messages, and compare against a per-task limit. Three or four is generous for most machine-to-machine confirmations.

Commitment is a handoff trigger, not a turn. Any message in which your agent would agree to a price, a date, a quantity or a term goes to a person, whatever the budget says.

The product supplies backstops underneath these, and they are backstops, not stopping rules. A sending policy at workspace, project or inbox level can set a daily recipient limit for live mail, after which sends fail with policy_denied. When the free allowance of 100 email units a month runs out, sending stops. Either will end a loop eventually, having spent the budget you wanted for real work.

Handing the conversation to a human

Hand off when the next message would commit your side to something, when the budget is spent, when the same question comes back twice, or when the counterparty asks for a person. The handoff has two halves: the counterparty must learn that a person is taking over, and a person must actually receive the thread.

This product’s way to put a person in front of a send is the approval gate. A credential holding drafts:submit without messages:send cannot transmit: every send it attempts returns approval_required with the reason code draft_only_principal, the draft waits in the approval queue, and an approval.requested event fires. An inbox policy with an always rule does the same for any credential. The human approval guide covers the queue, the payload hash binding and the rejection path.

What the product does not do is scope a credential to one thread. Scopes belong to the key, so “this conversation is now human-reviewed” is your code’s decision: when a thread crosses a handoff trigger, route every later action on that thread through the draft-only credential, and mark the thread in your own state so a restart does not forget. The agent’s last direct message should say plainly that a person will reply, and set replies-expected: no so a cooperating agent waits rather than prompting.

The console shows every thread in the inbox, so the person picking it up reads the whole exchange, including anything the agents agreed before the handoff.

Reproducing an agent-to-agent exchange with test inboxes

You can rehearse two agents corresponding without mailing anyone, but not by having them mail each other. A test inbox cannot deliver to another test inbox. Sending from a test inbox goes to a simulated transport that records the submission and returns accepted with a generated .mail.invalid Message-ID. Nothing is routed anywhere, including to another test inbox whose address you put in to. The message moves queued to accepted in the sending inbox, a message.accepted event fires, and the other inbox receives nothing.

Inbound in test exists only by injection: POST /v1/projects/{project_id}/test/inbound, which needs test:simulate and messages:read and works only on a test project and a test inbox. So the rehearsal is a relay: your harness plays the network.

Two properties of that endpoint shape what the rehearsal can prove.

So test mode exercises your stopping rules, handoff routing, events and approvals. It does not exercise cross-turn threading or the header chain, and a turn budget that reads message_count from one thread will see a new thread per relayed turn. Keep a conversation ID in the harness and count turns there.

The procedure, with an efa_test_ key holding inboxes:read, inboxes:write, threads:read, messages:read, messages:send and test:simulate:

  1. Create two test inboxes, A and B. Each gets a generated .mail.invalid address.
  2. Seed the opening: inject the scenario’s first message into A, from B’s address.
  3. Run agent A against its inbox. When it sends, read its latest outbound message and its body.
  4. Relay that turn into B by injecting it, with from set to A’s address.
  5. Run agent B, read its outbound message, and relay it into A the same way.
  6. Stop when either agent declines to send, or when the harness turn cap is hit. Record which one ended it.

One relay step, A to B:

API=https://api.emailforagents.ai/v1/projects/$EFA_PROJECT_ID
AUTH="Authorization: Bearer $EFA_TEST_KEY"

# A's newest outbound message (the inbox message list is newest first)
MSG=$(curl -s "$API/inboxes/$INBOX_A/messages" -H "$AUTH" \
  | jq -c '[.data[] | select(.direction=="outbound")][0]')
MSG_ID=$(echo "$MSG" | jq -r .id)
SUBJECT=$(echo "$MSG" | jq -r .subject)

BODY=$(curl -s "$API/inboxes/$INBOX_A/messages/$MSG_ID/body" -H "$AUTH" | jq -r .text)

# Deliver it to B by injection
curl -s -X POST "$API/test/inbound" -H "$AUTH" -H "Content-Type: application/json" \
  -d "$(jq -n --arg inbox "$INBOX_B" --arg from "$ADDR_A" --arg to "$ADDR_B" \
        --arg s "$SUBJECT" --arg t "$BODY" \
        '{inbox_id:$inbox, from:{email:$from}, to:[{email:$to}], subject:$s, text:$t}')"

The inbox-wide message list is ordered newest first, 25 to a page by default; a thread’s message list is ordered oldest first by sequence. Mixing the two up is the usual reason a harness relays the wrong turn.

Run the scenario at least three ways: both agents with stopping rules, only one with them, and neither. The third run is the baseline. With neither, you are measuring how long the courtesy loop runs before the harness cap ends it, which is the number to show anyone who thinks the drafting prompt alone is enough.

The limits are small and enforced: up to 3 active test inboxes per workspace, up to 100 retained messages and 10 MB of content, and content expiring after 7 days. The message cap is checked when a test inbox sends, against one counter for the whole workspace, and a send over it fails with HTTP 403 test_limit_exceeded. Injected inbound messages are not counted against it. A runaway rehearsal therefore stops after at most a hundred outbound sends across both agents, fewer if earlier runs in the workspace used some of the allowance. That is a backstop, not a result. The guide to testing agent email without sending covers the rest of the test surface.

A design checklist before the first message goes out

Decide these before either agent is pointed at a real counterparty:

None of this needs a new protocol. It needs each side to carry the judgement that humans apply without writing it down, stated as rules your code enforces. The inboxes, threading and approval gate that make that enforceable are described on the agent inboxes page, and the rest of the product is on the Email for Agents home page.


Keep reading

All guides · Documentation · Email for Agents