A WhatsApp follow-up, end to end
When a WhatsApp follow-up falls due, the engine tick composes a message from the reason recorded on it and hands it to whatsapp.dispatch. Inside the 24-hour window that message goes as written; outside it, only the approved template the workspace nominated. A refusal is recorded with its reason and the commitment stays visible rather than vanishing.
The stages#
- Trigger — a follow-up on the
whatsappchannel reaches its due time and the engine tick picks it up. - User or external event — none. The reason is already a record; nothing has arrived from outside.
- Authentication and workspace resolution — the queue runs inside the owning workspace before any rule or setting is read.
- Ingest — the row names the Person, the channel, the due time and the reason the commitment exists.
- Canonical record — the follow-up is already canonical; the message it produces will join the Person's existing thread.
- Reasoning — the message is composed from the reason plus what Connect knows about the relationship, so it reads as a continuation rather than a fresh approach.
- Knowledge, memory and rules — the same bounded Knowledge, memory tiers and channel standing instructions that govern any reply.
- Autonomy and approval —
dispatchprices the message on Meta's category and the narrowest applicable rule decides: send, queue for approval, hold as a draft, or refuse. A marketing template counts as sales outreach whatever it says. - Action through Meta — consent, do-not-contact, any block directive and the window are checked; inside the window the written message goes, outside it the nominated template, and nothing at all if that template is missing or unapproved.
- Result — a returned message id records the send with evidence; a refusal — the window, an unapproved template, the hourly cap — is recorded with its reason.
- Relationship, timeline and memory — the message joins the Person's history whether or not a reply follows.
- Audit, usage and Needs You — the decision is in the audit trail, the send is metered on a customer plan, and an unkept commitment becomes something a person can see.
Stage by stage, with the failures#
| Stage | What you see | What changes | What can fail |
|---|---|---|---|
| Due | The row moves to due on #/follow-ups | Nothing outbound yet | A workspace whose runtime is switched off does no draining |
| Composed | A message drafted against the thread | A draft exists | A reason too thin to write from — the message reads generic |
| Autonomy applied | Sent, queued, or kept | The decision, with its rule | ask_before_send means nothing goes until somebody acts |
| Compliance checked | Nothing, when it passes | Nothing | A STOP, a suppression or a block — the commitment closes rather than retrying |
| Meta asked | The message in the chat, if it goes | State uncertain → sent when Meta returns a message id | The window has closed and no approved template is nominated; or the hourly cap for the connection type is reached |
| Recorded | The outcome on the conversation and in the log | Timeline, audit, usage | Nothing fails silently here — a refusal is a recorded decision |
What happens to a refused follow-up#
It is not retried against the same refusal and it is not deleted. The refusal and its reason are recorded, and the commitment remains visible as unfinished business — which is what lets somebody pick it up on a channel that is open, or decide it no longer matters.
Three refusals deserve different responses. A window refusal means the message was fine and the moment was wrong: nominate an approved template, or move the commitment to another channel. A rate-limit refusal means nothing is wrong at all — Meta is saying slow down, and waiting is the whole fix. A consent or suppression refusal means the message should not be sent at all: close the commitment rather than rerouting it.
The general principle — that an agent must not quietly abandon something it undertook to do — is argued in Preventing a voice agent from promising a call-back it cannot keep, and it applies here for the same reason.
Proving it worked#
- The row is no longer due — it either completed or is recorded as refused with a reason.
- If it sent, the message is on the customer's thread with an acknowledgement behind its sent state.
- If it was held, there is an item in Needs You naming the person and the reason.
- The decision log carries the rule that produced whichever of those happened.
A follow-up that silently vanished satisfies none of these, and that combination — nothing sent, nothing queued, nothing recorded — is the one worth investigating rather than explaining.
Questions#
Does a follow-up wake the workspace up?
No. Draining happens while the workspace runtime is on. A workspace switched off does not send, and the commitments are still there when it is switched back on.
Can I approve a held follow-up later than its due time?
Yes, and it sends when you approve it. Whether Meta still accepts it is a separate question — a window that had closed by then refuses it just the same, because the check runs at send time and not at approval time.
Has a WhatsApp follow-up ever executed against Meta in production?
No. No number is connected in production, so the due-time composition, the category decision and the refusal paths are what run there; the send itself is proven against a stubbed Meta in test_whatsapp_platform_v1.
Will Connect send the same follow-up twice?
No. A commitment is acted on once and then completed or recorded as refused. Two similar messages usually means two rows, which is worth looking at on the follow-ups screen.