# 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.

- **Status:** Available
- **Audience:** both
- **Channels:** whatsapp
- **In the app:** #/follow-ups, #/whatsapp, #/needs-you
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/docs/workflows/whatsapp-follow-up-execution/

## The stages

1. Trigger — a follow-up on the `whatsapp` channel reaches its due time and the engine tick picks it up.
2. User or external event — none. The reason is already a record; nothing has arrived from outside.
3. Authentication and workspace resolution — the queue runs inside the owning workspace before any rule or setting is read.
4. Ingest — the row names the Person, the channel, the due time and the reason the commitment exists.
5. Canonical record — the follow-up is already canonical; the message it produces will join the Person's existing thread.
6. 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.
7. Knowledge, memory and rules — the same bounded Knowledge, memory tiers and channel standing instructions that govern any reply.
8. Autonomy and approval — `dispatch` prices 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.
9. 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.
10. 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.
11. Relationship, timeline and memory — the message joins the Person's history whether or not a reply follows.
12. 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 |

> **Careful** The window is the failure specific to this flow and it is more likely the further out the due date was set. A commitment that genuinely has to land on a date is safer on `email` or `phone`, where no equivalent rule exists.

## 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](/research/callback-promises/), and it applies here for the same reason.

## Proving it worked

1. The row is no longer due — it either completed or is recorded as refused with a reason.
2. If it sent, the message is on the customer's thread with an acknowledgement behind its sent state.
3. If it was held, there is an item in Needs You naming the person and the reason.
4. 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.

## Related

- [Follow-ups on WhatsApp](https://connectbyjbrh.com/docs/whatsapp/whatsapp-follow-up/)
- [Replying on WhatsApp](https://connectbyjbrh.com/docs/whatsapp/replying/)
- [What WhatsApp cannot do here](https://connectbyjbrh.com/docs/whatsapp/whatsapp-limits/)
- [Follow-ups in Connect](https://connectbyjbrh.com/docs/follow-ups/)
- [An inbound WhatsApp message, end to end](https://connectbyjbrh.com/docs/workflows/whatsapp-inbound/)
- [Preventing a voice agent from promising a call-back it cannot keep](https://connectbyjbrh.com/research/callback-promises/)

## What this page is based on

- `backend/app/whatsapp_engine.py` — `tick`, `_due_followups`, the window fallback to the nominated template
- `backend/app/whatsapp.py` — `dispatch`, `_category_for`, `RATE_LIMIT_CODES`
- `docs-source/sources/GENERAL.md` §7 — follow-ups and their channels
- Connect capability registry (docs-source/facts.py) — `WHATSAPP`, `followups`
