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

How a compliance reminder reaches its owner

The scheduler picked a move — due now, overdue, or escalate. Now the reminder Lambda has to figure out who to send it to, on what channel, at what time of day, and with what context attached. Get any of those wrong and the reminder is worse than no reminder: a 2am Slack ping, a generic “something is due,” a notification to somebody who left the company three months ago. Four small guardrails sit between the move and the actual reminder.

Key takeaways

  • Owner resolution: per-task override beats per-control-area default beats fallback to the configured admin.
  • Slack DMs are the default; email is the fallback if no Slack ID is configured.
  • Quiet hours and holiday calendars defer reminders to the next available business hour.
  • Every reminder ships with the task, due date, what proof to keep, a link to the row, and a Done button.
  • Escalation reminds the named target instead of (or alongside) the owner; the owner stays in the loop.

Four guardrails on every dispatch

Four guardrails between the chosen move and the dispatched reminder A horizontal flow diagram. On the far left, a "Move chosen" box: the scheduler emitted an event with the task, the control area, and one of three sending moves — due now, overdue, or escalate. Four guardrail gates sit in a row to the right, each drawn as a vertical bar. Gate 1: Resolve owner — looks up the per-task owner column first; if blank, falls back to the per-control-area owner from the rules doc; if still blank, falls back to the configured admin. Returns a Slack member ID if one is set, otherwise an email address. Gate 2: Quiet hours — checks the local time against the rules-doc quiet-hours window (default 6pm to 8am); if outside business hours, defers the dispatch to the next available business minute via an EventBridge Scheduler one-off rule. Gate 3: Holiday calendar — checks the date against the configured holiday list; if it's a holiday, defers to the next non-holiday business day. Gate 4: Final compose — formats the reminder message from the voice-doc template for the chosen move and control area, attaches the task context (name, due date, what proof to keep, link to the task row), adds a Done button. After all four gates pass, the reminder ships via the Slack Web API DM for the resolved Slack ID, or via SES outbound for the resolved email. A note at the bottom: every dispatch is logged to DynamoDB so the next tick knows the reminder has fired. Move chosen due now, overdue, or escalate Gate 1 Resolve owner task override? area default? admin fallback? prefer Slack ID, fall back to email Gate 2 Quiet hours local time in business window? if not, defer via Scheduler Gate 3 Holiday calendar date in holiday list? if yes, defer to next business day Gate 4 Compose + context voice template for area + task, due, proof, link, Done button Dispatch — Slack DM (preferred) or SES outbound email (fallback) chat.postMessage for Slack · SES SendRawEmail for inbox every dispatch logged to DDB ct-reminders — the next tick won’t duplicate Every gate is a deterministic check — no model calls, no surprise behavior on a Tuesday in April.
Fig 4. Four guardrails between the move and the dispatched reminder. Resolve the owner. Honor quiet hours. Skip holidays. Compose with full context. Then ship via Slack or email and log the dispatch so the next tick doesn’t duplicate.

Gate 1: resolve the owner

Three places the reminder Lambda looks for the owner of a task, in order. First, the task list’s per-task owner_email column — if a row has a specific person assigned, that person owns it regardless of the control-area default. Second, the per-control-area default in the rules doc (“all safety tasks default to the office manager”). Third, the configured admin fallback — the person who set up the tracker and gets every unowned reminder. The fallback should never fire in steady state; if it does, the weekly digest names every task that hit the fallback so the rules doc can be updated.

Once the dispatch knows which person to remind, it looks up their delivery preference. The voice doc maps each owner to a Slack member ID if one is set, otherwise to an email address. Slack is preferred because reminders feel like work-context messages, and a Slack DM with a Done button is more useful than an email link. Email is the fallback so nobody falls through the cracks.

Gate 2: quiet hours

The scheduler itself runs at 8am local time, so the first time a move fires it’s already in business hours. But escalations and overdue reminders that result from a tick can fire later in the day. And one-off computed dispatches (a second-window reminder that takes effect the same day) can land outside the configured window.

Gate 2 reads the rules doc’s quiet-hours setting (default 6pm to 8am, configurable per business). If the current local time is in the quiet window, the dispatch creates a one-off EventBridge Scheduler rule that fires at the next business-hour minute and exits without sending. The Scheduler invokes the same dispatch Lambda with the same payload at the deferred time, where Gate 2 will let it through.

Gate 3: holiday calendar

The rules doc lists the holidays you observe — either a static list (“Christmas Day, New Year’s Day, Independence Day...”) or a reference to a Google Calendar that holds them. Gate 3 checks the current local date against that list and, if it’s a configured holiday, defers the dispatch to the next non-holiday business day.

The list is on purpose — the tracker won’t auto-detect a country’s public holidays for you. The failure modes are very different. A holiday you forgot to add fires a reminder that lands on a closed laptop. A holiday in the list that’s no longer observed just delays a reminder by one business day, which is fine. The trade-off favors keeping the list explicit.

Gate 4: compose with full context, then ship

The voice doc has one Slack message template per control area: a short message with placeholders for the task name, due date, what proof to keep, and a link to the task row. The dispatch Lambda fills the placeholders, attaches a “Done” button, and ships the message via the Slack Web API chat.postMessage call. The bot token itself lives in Secrets Manager.

For email fallback, the same template is wrapped in a small HTML email with the same fields and a link that, when clicked, hits a Function URL that records the completion — the email equivalent of the Slack button.

An escalate move adds a second recipient: the escalation target named in the rules doc for that control area. The owner is still reminded (the escalation isn’t a substitute for the original owner’s reminder — both go out), but the manager now sees it too. The escalate template is slightly different: it includes the previous reminder dates and how many days the task has been overdue, so the manager has the audit trail at hand.

Every dispatch — Slack or email, owner or escalate — writes a row to ct-reminders in DynamoDB. The next day’s tick reads that row and knows not to remind the same window again.

Why the guardrails exist

None of these gates are exotic. They’re the kind of small care a thoughtful human would take if they were sending the reminders themselves — check who actually owns this, don’t ping at 11pm, skip the day everyone’s off, include enough context that the recipient doesn’t have to ask a follow-up question. Putting them in code as four small sequential gates makes them part of the design, not a feature you’re trusting the writer of any one reminder to remember.

Next post: how evidence gets captured once an owner has acted on the reminder — how the tracker records the task done, files the note or photo as proof, and rolls the cycle forward to the next due date.

All posts