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
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-remindersDynamoDB 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