Status: Resolved. This postmortem is a historical log. The issue discussed below has been fully fixed.
The WhatsApp broadcast that started talking to itself: Eliminating auto-responder loops
Out-of-office auto-responders and BSP system error messages trigger infinite bot ping-pong loops on WhatsApp campaigns. Meta Cloud API provides no is_bot header, so standard CRMs either loop indefinitely or mute real customers. Here is how we engineered deterministic correspondent triage, context-aware thresholding, and prompt refusal gates in Klaros.
The Incident: A promotional broadcast reaches businesses as well as people, and a business number answers it with its own autoresponder. A CRM that treats that text as a customer message replies to it, the autoresponder replies again, and both sides pay for every turn while the sender’s quality rating takes the complaints.
The Architectural Fix: Replies are classified as a person, a suspected machine or a machine, and only a machine is silenced, which takes two independent signals. After one of our campaign sends those are timing (back within 15 seconds, at least 60 characters) and a known autoresponder phrase. A phrase alone never silences anyone. A prompt guard covers what the classifier misses, and button taps and Flow submissions always count as a person.
The Absence of `Auto-Submitted` Headers on WhatsApp
In conventional messaging protocols, machine-generated traffic carries explicit structural headers:
- SMTP Email: RFC 3834
Auto-Submitted: auto-replied,Precedence: bulk. - Slack API:
bot_idparameter present on message payload. - Telegram Bot API:
User.is_bot: true.
The Meta WhatsApp Cloud API provides no such header. An inbound webhook payload delivers identical fields (from, wa_id, text) whether written by a human typing on a keyboard or generated by an Android out-of-office auto-reply app:
{
"object": "whatsapp_business_account",
"entry": [{
"changes": [{
"value": {
"messaging_product": "whatsapp",
"contacts": [{ "profile": { "name": "Customer" }, "wa_id": "34600000001" }],
"messages": [{
"from": "34600000001",
"id": "wamid.HBgL...",
"timestamp": "1717584000",
"text": { "body": "Thank you for reaching out! We are currently unavailable." },
"type": "text"
}]
}
}]
}]
}
// One signal, never a verdict. Courtesy a person uses ("shukriya", "gracias por tu mensaje")
// was on an earlier version of this list and classified real customers as bots.
const AUTOREPLY_PATTERNS = [
/unknown\s+input\s+type\s+received/i,
/kindly\s+reply\s+with\s+a\s+valid/i,
/thank\s+you\s+for\s+(reaching\s+out|contacting|messaging)\b/i,
/gracias\s+por\s+(escribirnos|contactarnos)/i,
/we\s*(?:['’]\s*re|\s+are)\s+(?:currently\s+)?(?:unavailable|away|closed)/i,
/out\s+of\s+(?:the\s+)?office/i,
/fuera\s+de\s+(?:la\s+)?oficina/i,
/\bauto[-\s]?reply\b|automated\s+(?:response|message|reply)|mensaje\s+autom[áa]tico/i,
/\b(?:office|business|working)\s+(?:hours|timings?)\b/i,
/horario\s+de\s+atenci[óo]n/i,
];
// In classifyCorrespondent: the phrase counts only on a reply to a campaign send, and
// only two independent signals (phrase, timing, exact repeat) make a verdict.
if (signals.length >= 2) return { kind: CORRESPONDENT.AUTOMATED, /* ... */ };
Broadcast Attribution & Interactive Immunity
The broadcast context is read from our most recent outbound message only: whether the campaign ledger (outbound_messages) holds a campaign for it. An earlier version read the conversation’s origin_campaign_id, which is set once and never cleared, so a conversation that a campaign opened months ago kept being treated as a fresh broadcast reply.
Crucially, interactive button taps and WhatsApp Flow completions take unconditional priority (isInteractive = true). No matter how fast an interactive payload arrives, a button press is treated as HUMAN 100% of the time.
Generative LLM Prompt Refusal Guards
Even if an automated text bypasses regex matching, generative AI models must be restrained from entering conversational loops. Klaros adds an explicit AUTO-RESPONDER GUARD to the model’s instructions (src/services/ai-auto-reply.mjs):
AUTO-RESPONDER GUARD: If the incoming message is plainly another business’s automatic reply to a
template message we sent (an out-of-office or away notice, "unknown input type received", a canned
receipt naming THEIR business) AND it asks us nothing, do not reply: return
{"canAnswer": false, "reply": "", "action": "handoff"}. A person being polite ("thank you",
"shukriya", "thanks for reaching out") or asking anything at all is NOT an automatic reply: answer them.
Empirical Verification & Test Proofs
The behaviour described here is covered by tests that run against a real SQLite database built from the production migrations:
# The classifier, with negative controls (people who must never read as machines)
node --test test/uni-cloud-broadcast-autoresponder.test.mjs
# The classifier as the inbound handler actually calls it
node --test test/uni-cloud-correspondent-self-match.test.mjs
Run WhatsApp campaigns without bot loop panic
Connect your own WhatsApp Business Account with zero per-message markup, full consent ledgers, and deterministic loop protection.
Questions people ask about broadcast auto-responder loop suppression
Why do WhatsApp broadcasts trigger auto-responder loops?
When a mass promotional broadcast is sent to thousands of contacts, handsets with out-of-office auto-reply enabled respond automatically. Naive CRMs treat these inbound texts as human messages and trigger auto-replies or AI model responses, starting an infinite messaging loop.
Does Meta WhatsApp Cloud API supply an is_bot header?
No. Unlike email (RFC 3834 Auto-Submitted) or Slack/Telegram APIs, Meta's Cloud API webhook delivers identical message structures regardless of whether the sender is a human or an auto-reply app.
How does Klaros prevent auto-responder loops without silencing human replies?
Klaros classifies each reply as HUMAN, SUSPECTED_AUTOMATED or AUTOMATED, and only AUTOMATED stops replies, which takes two independent signals. For a reply to one of our campaign sends, those are timing (back within 15 seconds and at least 60 characters long) and a known autoresponder phrase such as an out-of-office notice. A phrase alone is never enough, so a customer who writes "thank you for reaching out, what is the price?" is still answered. Quick-reply button taps and WhatsApp Flow submissions always count as a person.
How does zero-markup messaging save costs during mass broadcasts?
Every message in a bot-to-bot loop is a billable message. Klaros adds no per-message markup: you connect your own WhatsApp Business Account and pay Meta’s rates directly, so a loop that is stopped early costs only what Meta charges for the messages already sent.
Written 23 September 2026. Klaros — Built to Remember Your Story. Related: our philosophy, Trengo alternative, Twilio alternative, WATI alternative, reliability notes, all build notes.