Configuring reminder escalation
Set the stage order, change the day thresholds, and prove a schedule change with a boundary test before it reaches real customers.
Last updated 12 August 2026.
Northwind sends three reminders before an invoice reaches collections. This page explains the escalation order, how to change the schedule, and how to test a change before it reaches real customers.
Escalation runs once a day, for every invoice with an open balance and no dispute flag. Each stage is evaluated independently against the invoice's due date, so an invoice can skip a stage entirely if a payment or a dispute is recorded before that stage's day arrives.
Escalation order #
The stage function is the single place this decision is made. Northwind calls it once per invoice, per day, from a scheduled task:
def next_reminder(invoice):
if invoice.days_overdue >= 14:
return Stage.FINAL_NOTICE
if invoice.days_overdue >= 3:
return Stage.OVERDUE
return Stage.DUE
Stage order matters more than stage timing: a customer who pays between the due date and the first reminder never sees a reminder at all, and one who disputes at any point is pulled out of escalation on the next daily run, not retroactively.
Before you enable this
Turning on automatic escalation sends to every account with an open
invoice, including ones already overdue under the old manual
process. Run a dry run first, with DRY_RUN=1, to see
exactly who would be contacted before the first real send.
You should know
Stages are evaluated in the account's own timezone, not the server's. An invoice due on the 30th in Sydney is chased on the 30th in Sydney, even when the scheduler runs from London.
Changing the thresholds #
The day counts in next_reminder are the only numbers most
teams ever need to change. Northwind's own support team moved the
overdue threshold from seven days to three after finding that most
disputes were raised in the first week regardless of when the reminder
arrived, so a later reminder was only delaying the useful signal.
- Lowering a threshold sends reminders sooner but raises the number a support team has to handle.
- Raising the final notice threshold delays collections handoff, which is rarely what a finance team wants.
- Thresholds are read once per run, not cached, so a change takes effect on the next scheduled task.
We assumed a longer overdue window would feel gentler. It mostly just meant the first three days of a genuine dispute went unanswered.
Testing a schedule change #
Every threshold change should ship with a test pinning the boundary day, not just the middle of a stage. The boundary is where an off-by-one silently moves a customer into the wrong stage a day early or a day late:
def test_overdue_boundary_is_day_three():
invoice = make_invoice(days_overdue=3)
assert next_reminder(invoice) is Stage.OVERDUE
def test_day_before_overdue_stays_due():
invoice = make_invoice(days_overdue=2)
assert next_reminder(invoice) is Stage.DUE
Recovery by stage #
The table below is why the overdue threshold moved. It reads left to right as the escalation runs: the earlier a stage catches a payment, the less likely it is to need a person.
| Stage | Paid within 7 days | Needed a call |
|---|---|---|
| Due today | 58% | 3% |
| Overdue (3 days) | 22% | 17% |
| Final notice (14 days) | 7% | 68% |
| Collections handoff | 2% | 91% |
Moving the overdue threshold from seven days to three shifted roughly a fifth of accounts out of the "needed a call" column entirely, which is the whole basis for the change described above.
Further reading: how late-payment prediction works and how a dispute flag pauses escalation.