Part 3 of 7 · Compliance tracker series ~5 min read

How a control comes due

Once a day, at 8am local time, an EventBridge Scheduler rule fires the scheduler Lambda. The Lambda reads the task list, looks at one row at a time, works out the next time that task is due, 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 an owner can edit it without a deploy.

Key takeaways

  • The scheduler runs once a day via EventBridge Scheduler at 8am local time.
  • Per-task reminder chains live in the rules doc — a monthly check gets 5-before / on-day / 2-after, an annual sign-off gets 30/14/3 before.
  • Four moves per task, every tick: on-track, due now, overdue, escalate.
  • DynamoDB tracks last-reminder and last-done per task so reminders aren’t duplicate spam.
  • The scheduler itself never calls a model. The decision is entirely deterministic.

The decision flow, per task

Decision flow per task on every daily tick A vertical decision flow diagram. At the top, an input box "Task from list" with the row's name, control area, repeat rule, owner, and DynamoDB state. Below that, a step "Compute next_due_date" — the last-done date plus the repeat interval, then days_to_due is today's date in the configured timezone subtracted from that due date. Below that, a check "Already done this cycle?" — if yes, route to "On-track" (do nothing this tick). If no, continue. The next step "Look up reminder chain for task" — pulls the chain from the rules doc; for example a monthly check returns minus-5, 0, plus-2 days around the due date. The next step "Find smallest window threshold not yet crossed" — iterates the chain and picks the first window where days_to_due reached the threshold since the last tick. If none, route to "On-track." If one, look at the DynamoDB state to see if a reminder has already gone out for this window. If no reminder yet for this window, route to "Due now" (or "Overdue" if the due date has already passed). If the smallest crossed window 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 — On-track, Due now, Overdue, Escalate — emits an event to EventBridge with the move and the task context. A note at the bottom: the rules doc holds every threshold; the scheduler's code only enforces them — change a window in the doc and tomorrow's tick uses the new value. Task from list name · area · repeats · owner Step 1 Compute next_due_date last_done + repeat rule Step 2 Already done this cycle? read DDB ct-done table Step 3 Look up reminder chain e.g. monthly → [-5, 0, +2] Step 4 Smallest window crossed? none → on-track final → escalate Step 5 Already reminded this one? read DDB ct-reminders table On-track do nothing Due now first window crossed Overdue due date passed Escalate final window, not done if yes none final due past due The rules doc holds every threshold — change a window and tomorrow’s tick uses the new value.
Fig 3. The scheduler’s decision tree, per task, per daily tick. Five steps decide which of four moves applies. The rules doc holds every threshold; the scheduler only enforces them.

Reminder chains: the timing isn’t magic, it’s in the doc

The rules doc has one short section per task or control area. Each section names the chain in plain prose: “Monthly safety check: remind 5 days before, on the day, and 2 days after. Quarterly data review: 14 and 3 days before, then on the day. Annual policy sign-off: 30, 14, and 3 days before.” The numbers are days relative to the due date when the reminder fires — negative means before, zero means on the day, positive means after. The first reminder is the early nudge. The last one is the escalation point — if the task still isn’t done by then, the escalation target gets reminded too.

The chains exist for a reason. A 30-day annual-sign-off nudge gives the whole team time to read and acknowledge a policy. A 5-day monthly-check nudge is enough to plan the walk-through into the week without being so early it gets ignored. A 2-day-after reminder catches the check that slipped past its date. Different rhythms need different chains; the doc reflects that.

Per-task overrides exist too. The task list has an optional column called chain_override. Type a comma-separated list of day offsets there and the scheduler uses your numbers instead of the control-area default for that one row. This is the right escape hatch for the audit-prep review you want to start chasing two months out.

Four moves, always

Every task, every tick, lands in exactly one of four buckets. The names are simple on purpose.

  • On-track. The due date is more than the first window away, or the task has already been done for the current cycle. Do nothing. Most tasks, most days, are on-track.
  • Due now. The due date just crossed the first window threshold and the task isn’t done yet. Send a fresh reminder with full context. Write a row to the ct-reminders DynamoDB table marking that the first window has fired.
  • Overdue. The due date has passed without the task 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 ct-reminders.
  • Escalate. The final window in the chain passed without the task being done. Send to the escalation target named in the rules doc — usually the owner’s manager — in addition to the owner. Mark the task as escalated in DynamoDB; the next tick keeps escalating daily until somebody marks it done. Bad timing burns reminders; an escalated control is one of the few cases where daily noise is the right answer.

State that makes the decision deterministic

The scheduler reads two DynamoDB tables every tick. ct-reminders records every reminder that’s gone out: (task_id, chain_index, reminded_date, dispatched_via). ct-done records every completion: (task_id, done_date, by_user, cycle_due_date). With those two tables, the move-decision logic is a few dozen lines of Python and zero magic. A given task with a given due date, a given reminder chain, and a given done/reminder history always produces the same move. Re-running the tick produces no extra reminders, because the state in DynamoDB shows what already fired.

Marking a task done is an explicit roll-forward: rows for the old cycle are kept for audit, the last-done date is stamped, and the next due date is computed fresh from the repeat rule. Part 5 covers the done-and-evidence flow in detail.

Why the daily tick uses no model

The scheduler could call a model on the tick to write a smarter reminder, or to decide whether to remind 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 5 days before and the task isn’t done, the nudge fires. A model in that loop introduces variance the team can’t reason about. Second, model calls cost money, and most days most tasks are on-track, so the call would be wasted nine days out of ten.

Bedrock fires elsewhere — on the evidence-reading lane in Part 5, and on the monthly summary mentioned in Part 6. Not on the daily tick. The scheduler itself is plain Python that reads a doc and writes events.

Next post: how a reminder finds the right person, how quiet hours and holidays are honored, and what marking a task done actually does to the cycle.

All posts