How overdue gets detected
Once a day, at 9am local time, an EventBridge Scheduler rule fires the chaser Lambda. The Lambda reads the invoice list, looks at one row at a time, computes how many days past due it is, and decides whether to do nothing or to send a reminder — and if so, which kind. The whole decision is plain Python. No model. No vector retrieval. Every threshold lives in the rules doc, where the bookkeeper can edit it without a deploy.
Key takeaways
- The chaser runs once a day via EventBridge Scheduler at 9am local time.
- Per-terms cadences live in the rules doc — net-30 gets 3/10/21, net-15 gets 2/7/14, due-on-receipt gets 1/7/14.
- Four moves per invoice, every tick: current, first nudge, follow-up, escalate.
- DynamoDB tracks last-send and paid/pause state per invoice so reminders aren’t duplicate spam.
- The chaser itself never calls a model. The decision is entirely deterministic.
The decision flow, per invoice
Cadences: 3/10/21 isn’t magic, it’s in the doc
The rules doc has one short section per set of terms. Each section names the cadence in plain prose: “Net-30: nudge at 3 days late, follow up at 10, escalate at 21. Net-15: 2, 7, 14. Due-on-receipt: 1, 7, 14. Net-60: 7, 21, 45.” The numbers are days past due when the reminder fires. The first number is the friendly nudge. The last number is the escalation point — if the invoice still isn’t paid by then, the account owner gets pulled in.
The cadences exist for a reason. A 3-day net-30 nudge is early enough to feel like a courtesy, not a demand — most invoices that age a few days are just sitting in someone’s approval queue. A 10-day follow-up is firm enough to get noticed without burning the relationship. A 21-day escalation is where a human should be picking up the phone. Different terms imply different patience; the cadences reflect that.
Per-invoice overrides exist too. The invoice sheet has an optional column called cadence_override. Type a comma-separated list of days there and the chaser uses your numbers instead of the terms default for that one row. This is the right escape hatch for the big client you’ve agreed to give a longer leash, or the chronic late-payer who needs a tighter one.
Four moves, always
Every invoice, every tick, lands in exactly one of four buckets. The names are simple on purpose.
- Current. The invoice isn’t past the first cadence step yet, or it has been paid or paused. Do nothing. Most invoices, most days, are current.
- First nudge. The invoice just crossed the first cadence step and there’s no payment yet. Send a friendly reminder with the pay-link. Write a row to the
ic-sendsDynamoDB table marking that the first step has fired. - Follow-up. A later step crossed without payment. Send a firmer reminder that names the previous reminder’s date so the customer doesn’t feel like it’s the first they’re hearing of it. Write the new send to
ic-sends. - Escalate. The final step in the cadence crossed without payment. Tell the account owner named in the rules doc — usually the salesperson or finance lead for that customer — with the full history attached so a human can take it from here. Mark the invoice as escalated in DynamoDB. After escalation the chaser stops auto-sending to the customer; this is the point where a person, not a system, should be in the conversation.
State that makes the decision deterministic
The chaser reads two DynamoDB tables every tick. ic-sends records every reminder that’s gone out: (invoice_id, step_index, send_date, dispatched_via). ic-state records the live status of each invoice: (invoice_id, status) where status is open, paid, paused, disputed, or written-off, plus a paused_until when relevant. With those two tables, the move-decision logic is a few dozen lines of Python and zero magic. A given invoice with a given due date, a given cadence, and a given send/status history always produces the same move. Re-running the tick produces no extra reminders, because the state in DynamoDB shows what already fired.
Marking an invoice paid is an explicit stop: rows for the cadence are kept for audit, and the live state flips to paid so no further reminders go out. Part 5 covers the stop-on-payment flow in detail.
Why the daily tick uses no model
The chaser could call a model on the tick to write a smarter reminder, or to decide whether to send at all. It doesn’t. Two reasons. First, the daily tick should be the one part of the system that is utterly predictable — if the rules doc says nudge at 3 days late and there’s no payment, the nudge fires. A model in that loop introduces variance the team can’t reason about, and chasing money is exactly where surprises are unwelcome. Second, model calls cost money, and most days most invoices are current, so the call would be wasted nine days out of ten.
Bedrock fires elsewhere — on the inbound parsing lane in Part 2, and on the monthly cash-flow summary mentioned in Part 6. Not on the daily tick. The chaser itself is plain Python that reads a doc and writes events.
Next post: how a reminder reaches the customer, how quiet hours and weekends are honored, and how the tone ladder keeps every message polite.
All posts