Part 3 of 7 · Deadline reminder series ~5 min read

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

Decision flow per deadline on every daily check A vertical decision flow diagram. At the top, an input box "Deadline from calendar" with the row's name, type, next due date, owner, and DynamoDB state. Below that, a step "Compute days_to_due" — the next due date minus today's date in the configured timezone. Below that, a check "This cycle already done?" — if yes, route to "Clear" (do nothing this check). If no, continue. The next step "Look up lead-time chain for type" — pulls the chain from the rules doc; for example payroll returns [5, 2, 1]. The next step "Find smallest lead time not yet crossed" — iterates the chain and picks the first lead time where days_to_due equals the threshold or fell below it since the last check. If none, route to "Clear." If one, look at the DynamoDB state to see if a reminder has already gone out for this step. If no reminder yet for this step, route to "First reminder" (or "Follow-up" if a previous step has already reminded). If the smallest crossed step is the final one in the chain, route to "Escalate" — remind the escalation target named in the rules doc instead of (or in addition to) the owner. Each terminal box — Clear, First reminder, Follow-up, Escalate — emits an event to EventBridge with the move and the deadline context. A note at the bottom: the rules doc holds every threshold; the checker's code only enforces them — change a lead time in the doc and tomorrow's check uses the new value. Deadline from calendar name · type · due · owner Step 1 Compute days_to_due due_date − today (TZ) Step 2 This cycle already done? read DDB dr-done table Step 3 Look up lead-time chain e.g. payroll → [5, 2, 1] Step 4 Smallest lead time crossed? none → clear final → escalate Step 5 Already reminded this step? read DDB dr-reminders table Clear do nothing First reminder first step crossed Follow-up subsequent step Escalate final step, not done if yes none final first subsequent The rules doc holds every threshold — change a lead time and tomorrow’s check uses the new value.
Fig 3. The checker’s decision tree, per deadline, per daily check. Five steps decide which of four moves applies. The rules doc holds every threshold; the checker only enforces them.

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-reminders DynamoDB 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