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

How a deadline reminder reaches its owner

The checker picked a move — first reminder, follow-up, or escalate. Now the dispatch 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 changed roles three months ago. Four small guardrails sit between the move and the actual reminder.

Key takeaways

  • Owner resolution: per-deadline override beats per-type 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 deadline, type, days remaining, link, and the Done / Snooze / Ack-only buttons.
  • 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 checker's chosen move and the dispatched reminder A horizontal flow diagram. On the far left, a "Move chosen" box: the checker emitted an event with the deadline, the type, and one of three sending moves — first reminder, follow-up, 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-deadline owner column first; if blank, falls back to the per-type 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 type, attaches the deadline context (name, type, days remaining, link to any reference), adds the Done, Snooze, and Ack-only buttons. After all four gates pass, the reminder ships via the Slack incoming webhook 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 check knows the reminder has fired. Move chosen first reminder, follow-up, or escalate Gate 1 Resolve owner deadline override? type 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 type + deadline, days, link, three action buttons Dispatch — Slack DM (preferred) or SES outbound email (fallback) incoming webhook for Slack · SES SendRawEmail for inbox every dispatch logged to DDB dr-reminders — the next check 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 check doesn’t duplicate.

Gate 1: resolve the owner

Three places the dispatch Lambda looks for the owner of a deadline, in order. First, the calendar sheet’s per-deadline owner_email column — if a row has a specific person assigned, that person owns it regardless of the type default. Second, the per-type default in the rules doc (“all tax filings default to the bookkeeper”). Third, the configured admin fallback — the person who set up the system and gets every unowned reminder. The fallback should never fire in steady state; if it does, the weekly digest names every deadline 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 action buttons is more useful than an email link. Email is the fallback so nobody falls through the cracks.

Gate 2: quiet hours

The checker itself runs at 8am local time, so the first time a move fires it’s already in business hours. But follow-ups and escalations that result from a check can fire later in the day. And one-off computed dispatches (a second-step reminder that takes effect at the same time as the first) 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 system 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. One exception: when a deadline is one or two days out, the holiday gate yields, because deferring a last-chance payroll reminder past a holiday could push it past the due date itself.

Gate 4: compose with full context, then ship

The voice doc has one Slack message template per type: a short message with placeholders for the deadline name, type, days remaining, and link to any reference. The dispatch Lambda fills the placeholders, attaches the three action buttons (Done, Snooze, Ack-only), and ships the message via the Slack incoming webhook. The webhook URL itself lives in Secrets Manager.

For email fallback, the same template is wrapped in a small HTML email with the same fields and links that, when clicked, hit a Function URL that records the action — the email equivalent of the Slack buttons.

An escalate move adds a second recipient: the escalation target named in the rules doc for that type. 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 close the deadline now is, so the manager has the full picture at hand.

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

Why the guardrails exist

None of these gates are exotic. They’re the kind of small care a thoughtful person 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 a deadline gets marked done once an owner has acted — how the system records the completion, rolls the due date forward by the repeat interval, and starts a fresh chain for the next cycle.

All posts