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

How a reminder reaches the customer

The reminder picked a move — first reminder, second reminder, or gap alert. 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 6am text, a generic “you have an appointment,” a message to a number the customer changed a year ago. Four small guardrails sit between the move and the actual send.

Key takeaways

  • Channel choice: text if a phone is on file, email if not; a gap alert goes to staff in Slack instead.
  • Text is the default for customers; email is the fallback when no phone is set.
  • Quiet hours and holiday calendars defer reminders to the next sensible hour.
  • Every reminder ships with the service, date, time, staff member, link, and one-tap reply links.
  • A gap alert is the one move that goes to staff, not the customer — so an opening gets filled.

Four guardrails on every dispatch

Four guardrails between the reminder's chosen move and the dispatched message A horizontal flow diagram. On the far left, a "Move chosen" box: the reminder emitted an event with the appointment, the service, and one of three sending moves — first reminder, second reminder, or gap alert. Four guardrail gates sit in a row to the right, each drawn as a vertical bar. Gate 1: Pick channel — for a customer move, uses the phone number if one is set, otherwise the email address; for a gap-alert move, routes to the front-desk Slack instead of the customer. Gate 2: Quiet hours — checks the local time against the rules-doc quiet-hours window (default 9pm to 8am); if outside the window, defers the dispatch to the next allowed minute via an EventBridge Scheduler one-off rule. Gate 3: Holiday calendar — checks the date against the configured holiday list; if it's a closed day, defers to the next open day. Gate 4: Final compose — formats the message from the voice-doc template for the chosen move and service, attaches the appointment context (service, date, time, staff member, link to the booking), adds one-tap Confirm, Reschedule, and Cancel links. After all four gates pass, the message ships via SNS for the resolved phone number, via SES outbound for the resolved email, or to Slack for a gap alert. A note at the bottom: every dispatch is logged to DynamoDB so the next tick knows the reminder has fired. Move chosen first, second, reminder, or gap alert Gate 1 Pick channel customer move? gap to staff? phone on file? prefer text, fall back to email Gate 2 Quiet hours local time in sending window? if not, defer via Scheduler Gate 3 Holiday calendar date in closed list? if yes, defer to next open day Gate 4 Compose + context voice template for service + time, staff, link, reply links Dispatch — text via SNS (preferred), email via SES (fallback), or Slack (gap) SNS Publish for SMS · SES SendRawEmail for inbox every dispatch logged to DDB ar-sends — the next tick won’t duplicate Every gate is a deterministic check — no model calls, no surprise 6am text on a holiday.
Fig 4. Four guardrails between the move and the dispatched reminder. Pick the channel. Honor quiet hours. Skip closed days. Compose with full context. Then ship via text, email, or Slack and log the dispatch so the next tick doesn’t duplicate.

Gate 1: pick the channel

The first thing the dispatch Lambda decides is who the message is even for. A first or second reminder is for the customer. A gap alert is for the front desk. Those go to completely different places, so the channel choice forks here.

For a customer reminder, the dispatch looks up the row’s phone number. If one is set, the reminder is a text — texts get read within minutes and feel personal, which is what you want for an appointment nudge. If no phone is on file, it falls back to email so nobody slips through. For a gap alert, the message goes to a configured Slack channel where staff watch the day’s bookings; it never goes to the customer, because a gap alert is an internal “this slot might open” signal, not something the customer should see.

Gate 2: quiet hours

The reminder runs hourly, so a window can come due at any hour — including the middle of the night. A 48-hour window on a 7am appointment comes due at 7am two days before, which is fine. But a 2-hour window on a 10pm appointment would come due at 8pm, and a tight schedule could line up a send for after the customer’s bedtime.

Gate 2 reads the rules doc’s quiet-hours setting (default 9pm 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 allowed 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. A reminder that can’t be sent before the appointment itself (because the appointment is inside quiet hours) is dropped and logged rather than waking the customer.

Gate 3: holiday calendar

The rules doc lists the days you’re closed — either a static list (“public holidays, the week between Christmas and New Year...”) 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 closed day, defers the reminder to the next open day.

The list is on purpose — the reminder won’t auto-detect a country’s public holidays for you. The failure modes are very different. A closed day you forgot to add sends a reminder for an appointment that can’t happen. A day in the list that you’re actually open just delays a reminder by a 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 message template per service: a short message with placeholders for the service, the date and time, the staff member, and a link to the booking. The dispatch Lambda fills the placeholders, adds three short one-tap links — Confirm, Reschedule, Cancel — and ships the message via SNS for a text or SES for an email. The reply links point at a Function URL that records the customer’s choice; Part 5 covers what each one does.

Text messages are short by nature, so the template is tight: “Hi {name}, reminder: {service} with {staff}, {day} {time}. Confirm: {link} · Reschedule: {link} · Cancel: {link}.” Email gives a little more room for the booking details and a clearer set of buttons, but carries the same fields and the same three links.

A gap-alert move composes differently: it’s a staff message, so it names the customer, the service, the time, and how long until the appointment, plus a note on why it fired (“cancelled” or “2 hours out, still unconfirmed”). The point is to hand the front desk everything they need to decide whether to call the customer or release the slot.

Every dispatch — text, email, or gap alert — writes a row to ar-sends in DynamoDB. The next tick reads that row and knows not to send the same window again.

Why the guardrails exist

None of these gates are exotic. They’re the kind of small care a thoughtful receptionist would take if they were sending the reminders by hand — text the people who text, don’t message at 6am, skip the days you’re closed, include enough detail that the customer doesn’t have to call back to ask what time. 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 message to remember.

Next post: how a customer confirms, reschedules, or cancels once a reminder lands — how the list updates, how the chain resets, and how the front desk sees the change.

All posts