How a deadline comes due
Once a day, at 8am local time, an EventBridge Scheduler rule fires the checker Lambda. The Lambda reads the calendar, looks at one row at a time, computes the days remaining until the due date, 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 a team member can edit it without a deploy.
Key takeaways
- The checker runs once a day via EventBridge Scheduler at 8am local time.
- Per-type lead-time chains live in the rules doc — payroll gets 5/2/1, tax filings get 30/14/7/2, license renewals get 60/30/14/7.
- Four moves per deadline, every check: clear, first reminder, follow-up, escalate.
- DynamoDB tracks last-reminder and done state per deadline so reminders aren’t duplicate spam.
- The checker itself never calls a model. The decision is entirely deterministic.
The decision flow, per deadline
Lead-time chains: 5/2/1 isn’t magic, it’s in the doc
The rules doc has one short section per type. Each section names the chain in plain prose: “Payroll: remind at 5, 2, and 1 days. Tax filings: 30, 14, 7, 2. License and permit renewals: 60, 30, 14, 7. Insurance payments: 21, 7, 2.” The numbers are days remaining when the reminder fires. The first number is the first reminder. The last number is the escalation point — if the deadline hasn’t been marked done by then, the escalation target gets reminded too.
The chains exist for a reason. A 30-day tax-filing reminder gives the bookkeeper time to pull the numbers together. A 60-day license-renewal reminder leaves room for a slow government office. A 5-day payroll reminder is the “don’t forget to submit this” nudge before the cutoff, and the 1-day step is the last-chance ping before payday is at risk. Different obligations have different mechanics; the chains reflect that.
Per-deadline overrides exist too. The calendar sheet has an optional column called chain_override. Type a comma-separated list of days there and the checker uses your numbers instead of the type default for that one row. This is the right escape hatch for the annual report you know needs a 90-day head start because it has to go to the board first.
Four moves, always
Every deadline, every check, lands in exactly one of four buckets. The names are simple on purpose.
- Clear. The due date is more than the first lead time away, or this cycle has been marked done. Do nothing. Most deadlines, most days, are clear.
- First reminder. The due date just crossed the first lead-time step and this cycle isn’t done yet. Send a fresh reminder with full context. Write a row to the
dr-remindersDynamoDB table marking that the first step has fired. - Follow-up. A subsequent step crossed without the cycle being done. Send a follow-up that names the previous reminder’s date so the owner doesn’t feel like they’re seeing it for the first time. Write the new reminder to
dr-reminders. - Escalate. The final step in the chain crossed without the cycle being done. Remind the escalation target named in the rules doc — usually the owner’s manager — in addition to the owner. Mark the deadline as escalated in DynamoDB; the next check will keep escalating daily until somebody marks it done. Bad timing burns reminders; an escalated deadline is one of the few cases where daily noise is the right answer.
State that makes the decision deterministic
The checker reads two DynamoDB tables every check. dr-reminders records every reminder that’s gone out: (deadline_id, step_index, sent_date, dispatched_via). dr-done records every completion: (deadline_id, cycle_due_date, done_date, by_user). With those two tables, the move-decision logic is a few dozen lines of Python and zero magic. A given deadline with a given due date, a given lead-time chain, and a given done/reminder history always produces the same move. Re-running the check produces no extra reminders (because the state in DDB shows what already fired).
Marking a cycle done is an explicit reset of both tables for that deadline: rows for the old cycle are kept for audit, the due date rolls forward by the repeat interval, and a fresh chain starts against the new due date. Part 5 covers the completion flow in detail.
Why the daily check uses no model
The checker could call a model on the check to write a smarter reminder message, or to decide whether to remind at all. It doesn’t. Two reasons. First, the daily check should be the one part of the system that is utterly predictable — if the rules doc says remind at 14 days and the cycle isn’t done, the reminder fires. A model in that loop introduces variance the team can’t reason about. Second, model calls cost money, and most days most deadlines are clear, so the call would be wasted nine days out of ten.
Bedrock fires elsewhere — on the inbox parsing lane in Part 2, and on the monthly summary mentioned in Part 6. Not on the daily check. The checker itself is plain Python that reads a doc and writes events.
Next post: how a reminder finds the right owner, how quiet hours and holidays are honored, and what marking a deadline done actually does to the chain.
All posts