LOOMAL
How to

How to make an AI agent
reply in-thread.

Correct threading is the difference between an agent that looks like an assistant and an agent that looks like spam. Here's how to get it right.

Email threading relies on two headers: In-Reply-To (the Message-ID of the message being replied to) and References (the full chain of ancestors). Get these right and the reply stitches into the right thread in every major client. Get them wrong and your agent looks like a spam bot starting a new conversation every time.

Most agents get this wrong by default because they use a simple 'send' primitive and forget the headers. This recipe shows both the manual fix and the shortcut.

1. The manual way (any email provider)

When the agent receives a message, save its Message-ID and References headers. When it replies, set In-Reply-To to the saved Message-ID and References to the existing References plus the Message-ID.

Get the subject right too — prefix with 'Re: ' if it isn't already. Some clients use subject matching as a fallback for threading.

manual.ts
// Assuming you have the incoming message's headers:
const incoming = {
  messageId: "<abc123@example.com>",
  references: "<prior@example.com>",
  subject: "Demo follow-up",
};

const reply = {
  to: ["alice@example.com"],
  subject: incoming.subject.startsWith("Re: ")
    ? incoming.subject
    : `Re: ${incoming.subject}`,
  text: "Tuesday at 2pm works — sending an invite now.",
  headers: {
    "In-Reply-To": incoming.messageId,
    "References": `${incoming.references} ${incoming.messageId}`.trim(),
  },
};

2. The Loomal way (one call)

Instead of tracking headers yourself, use the reply endpoint. POST to /v0/messages/:messageId/reply with just the text (and optionally a new subject). Loomal reads the original message's headers, sets In-Reply-To and References correctly, and delivers the reply into the same thread.

The endpoint takes the original messageId as part of the URL. Nothing else to track — the agent knows what it's replying to because it just read the message.

loomal-reply.ts
async function reply(messageId: string, text: string) {
  const res = await fetch(
    `https://api.loomal.ai/v0/messages/${messageId}/reply`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.LOOMAL_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ text }),
    },
  );
  return res.json();
}

// In your agent loop:
for (const msg of unread) {
  const answer = await llm(msg.extractedText);
  await reply(msg.messageId, answer);
}

3. Reply to the right participant(s)

By default /reply sends to the original From address. For replies that should go to everyone on the thread (the 'Reply All' case), include a replyAll: true flag and Loomal addresses the reply to the full recipient list minus the agent itself.

For forwards, use /forward instead — different semantics, new Message-ID, no In-Reply-To linkage.

reply-all.sh
curl -X POST https://api.loomal.ai/v0/messages/$MSG_ID/reply \
  -H "Authorization: Bearer $LOOMAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Confirmed for everyone.",
    "replyAll": true
  }'

FAQ

What if the thread has 50 messages — do I need all Message-IDs in References?

Email clients generally cap References at a reasonable length (most truncate old entries). Loomal handles this for you; manually, keep the first few entries and the last few to preserve threading even when the middle gets dropped.

Can I change the subject when replying?

Yes. Pass a subject field in the reply body. Clients still thread correctly as long as In-Reply-To is set — they primarily rely on headers, not subject matching.

Why does my reply show up as a new thread sometimes?

Usually missing or malformed In-Reply-To. Also check that the Message-ID format has angle brackets (<abc@host>) — some providers strip them and the reply chain breaks.

Give your agent its own identity.

Free tier, 30-second setup.

Last updated: 2026-04-15