P-12 · Reminder escalation (day 3 / 5 / 7)
SOP:
Payroll_Processing.md§6 / Step 2.0 — timeline / Internal SLAActors: System — thecheck-pending-submissionsworker job. Payroll senders (or the approver) on the receiving side; Payroll Executive / Payroll Lead for escalations. Pre-state: A month waiting on the client:REQUESTED/INTAKE_IN_PROGRESS,ACTION_REQUIREDwith a question waiting, orREVISION_REQUESTEDafter our revision message. Post-state: The reminder for that day sent (or, with automatic messages off, a staff task saying it is due); at day 7 of a silent intake, staff (and possibly the Payroll Lead) escalated.
There is no portal link in a reminder any more, and no token is minted or expired. Reminders go through Chatwoot like every payroll message: by email to each payroll sender on their thread for the month (started from the firm-wide payroll mailbox chosen in Settings › Integrations), or their own chat when they have no email address.
0. Prerequisites
- A month waiting on the client. The client's days count from the month's request, not the opening (stage review S1-03, 28 Oct 2026): from the first
payroll.client_notice_sentaudit row with reasonpayroll.cycle_open.confirmation_sent. To exercise intake reminders without waiting, move that row back (or, for a month with no such row and no "client was not told" task,opened_at):sqlUPDATE audit_events SET occurred_at = now() - interval '3 days' WHERE target_id = '<cycleId>' AND action = 'payroll.client_notice_sent' AND reason = 'payroll.cycle_open.confirmation_sent'; - Worker running. Automatic sends are on by default:
MSG_AUTO_REPLY_ENABLEDis on unless set tofalse, and each company has its own switch on Settings → Payroll. With either off, every reminder below is held and becomes a staff task instead.
1. Steps
1.1 Trigger
check-pending-submissions runs hourly (0 * * * *). A company is dealt with only from 10:00 in its own time zone (the entity's timezone; Singapore when none is on file). Each day's reminder is recorded once, so later runs that day do nothing more. A reminder that falls due overnight waits for the first run at or after 10:00 local.
1.2 Intake — the client has sent nothing (SILENT)
On day 3, 5 and 7 after the month's request reached someone the client gets the month's list again, filled in from what we know (the first-month explanation is not repeated):
Just checking in: we haven't received anything for your {Month YYYY} payroll yet. Please send:
- … (what this company sends every month, or the standard list)
…what we already know about the month, what is still needed, when salaries go out… Just reply to this email with the files. (chat: "Just send them here.")
1.3 Intake — the client has sent something (PARTIAL)
Instead of "we're still missing the rest", the reminder moves the month on:
| Where the closing question stands | What the day-3/5/7 reminder does |
|---|---|
| Not asked yet | Asks it automatically: "So far for {Month} we have: …", what is still needed, then "Is anything else coming for your {Month YYYY} payroll? Reply "yes" if there's more coming, or "no" if that's everything." |
| Asked, still open | A short nudge on the same thread: "Just checking: is anything else coming for your {Month YYYY} payroll? Reply "no" if that's everything, or send the rest." — skipped if the client has written there since |
| Asked, lapsed unanswered | Asks it again ("Just checking: is anything else coming …"), with what has arrived since |
| Answered "more is coming" | "You mentioned more is coming for your {Month YYYY} payroll: please send the rest when you can." with what is still missing |
| Answered anything else | The plain reminder: "Thanks for what you've sent so far for your {Month YYYY} payroll. Please send anything else that's still to come." Used for an unclear reply (after the one re-ask) and for a "no" from a sender who could not be verified (stage review S2-10). What the answer meant is the answer handler's own record of the reply it read; a clear "yes" after the re-ask counts as "more is coming". |
A "no, that's everything" confirms intake and moves the month out of intake, so it is never reminded again. The client then gets "Thanks, we have everything we need for your {Month YYYY} payroll: …" by email (also when staff confirm intake or it is confirmed from the review inbox).
1.4 Waiting on a question (ACTION_REQUIRED)
Counted from the newest staff Ask client still waiting. "Just a reminder — we're still waiting on the items we asked you for so we can finish your {Month YYYY} payroll." goes only on the thread(s) the question was asked on. It stops once the client has answered the question or written anything on that thread. No question asked ⇒ no reminder.
1.5 After a revision request (REVISION_REQUESTED)
Counted from our revision message ("here is what we need"), not from the client's request, and sent to the approver it answered. No reminder before we have sent that message, or once the approver has written back.
1.5b A company counting back from its due day (since 29 Oct 2026)
When the company has chosen reminder days on Settings → Payroll → Client reminders (or in onboarding), the intake reminders in 1.2 and 1.3 go on those days instead: for example, with inputs due by the 23rd and "1 day before" and "on the due day" chosen, on the 22nd and the 23rd at 10:00, and not on day 3. The audit ledger records them as D1 and D0. From the day after the due day (the 24th) no more reminders go, and the escalation in 1.6 starts then rather than on day 7. A month whose request went on or after its due day falls back to days 3 / 5 / 7. The question and revision reminders (1.4, 1.5) are not affected.
1.6 Day 7 — escalation of a stalled intake
From day 7, on every run (independent of the reminders):
- staff are escalated (
CYCLE_SILENT_NO_STANDING_INSTRUCTION, "Contact the client directly"), once per cycle; - the Payroll Lead too (
CYCLE_SILENT_POL_ESCALATION) when no baseline fallback applies, intake is only partial, or the pay-by date is inside its risk buffer; - a genuinely silent month whose client has standing authority and a baseline roster falls back to that baseline, with a notice to the client.
When the client's days start (stage review S1-03, 28 Oct 2026):
- From the first month request that reached someone. Not from the opening: the month is already at Intake in progress before any message is tried.
- No such request yet and the "The client was not told the month opened" task (
PAY_CYCLE_OPEN_NOTICE_UNSENT) is open: no clock. No reminder, no day-7 escalation, no fallback; the task is what staff act on. - Staff cleared that task (they told the client themselves): the days count from when they cleared it.
- A month with neither on record (opened before requests were recorded, 17 Oct 2026) counts from
opened_at, as before. - The task open again after a request did go (a resend that failed): the days count from the first request, but the fallback is never applied while the task is open; staff and the Lead are escalated instead, the staff task saying "the client was not told the month opened".
Not "genuinely silent" (stage review S2-03): a joiner or leaver carried in from an earlier month, or held, on the month. Last month's figures are then known to be wrong for that person, so the fallback is not applied, however long the silence; the Payroll Lead's escalation names them ("… cannot proceed on a baseline: the joiner Tan Mei Ling, carried from an earlier month, is still unresolved, so last month's figures are known to be wrong for that person."). The reminders still treat the month as silent (the client sent nothing).
Day 7 is the last automatic reminder. After it, staff can still send one from the Work page (Client reminder › Send a reminder now), see Ask the client.
1.7 Not sent
A reminder held (automatic messages off) or not delivered becomes a REMINDER_DELIVERY_FAILED staff task on the month, in plain words, naming the day — for example "… It has not been sent — review the message and send it with "Send reminder"." A person with an email address is never moved to chat because no payroll mailbox is set; the task says so instead. Sending it from the Work page clears the task.
2. Verification
Audit ledger
cycle.reminder_fired (intake; reason = W3 / W5 / W7)
cycle.stage_reminder_fired.action_required.w{3,5,7} (ACTION_REQUIRED)
cycle.stage_reminder_fired.revision_requested.w{3,5,7} (REVISION_REQUESTED)
payroll.silence_reminder_sent (the reminder that went, on its thread)
payroll.closing_question.nudged (PARTIAL: nudge on the open question)
payroll.client_notice_sent (on the cycle: label, who it reached, the words; what client-notices reads)
cycle.reminder_delivery_failed (not sent — with the reason)
cycle.silence_escalated / cycle.silence_escalated_to_polAPI
GET /ops/cycles/<cycleId>/client-notices lists each reminder that went, with the words each person was sent and automatic: true.
Web UI
The Work page's Client reminder section shows when the next reminder falls due (day 3, 5, 7 of waiting, or, for a company counting back from its due day, e.g. "the day before the due day"), and A reminder is waiting to be sent when one was held.
3. Negative & edge cases
- Before 10:00 local — the company is skipped this run; the reminder goes on the first run at or after 10:00.
- Re-run the sweep — the same window never fires twice (ledger guard).
- Client wrote on the thread since the question — no nudge / no "still waiting" reminder.
- Approval reminders are not here:
check-pending-approvals(hourly at:15, also from 10:00 local) nudges the approver every 2 days until the pay-by date, each with a fresh approval link, and names the date we need approval by ("We need it by {date} to pay salaries on time.").
Next
If the client now sends everything, the month proceeds through P-04 and beyond.