How the reminder knows when to send
Once an hour, an EventBridge Scheduler rule fires the reminder Lambda. The Lambda reads the list, looks at one appointment at a time, computes the hours remaining, 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 the front desk can edit it without a deploy.
Key takeaways
- The reminder runs once an hour via EventBridge Scheduler so it can catch tight 2-hour and 1-hour windows.
- Per-service schedules live in the rules doc — a dental visit gets 48h/2h, a salon gets 24h/1h, a long consult gets 72h/24h/3h.
- Four moves per appointment, every tick: scheduled, first reminder, second reminder, gap alert.
- DynamoDB tracks last-send and confirm per appointment so reminders aren’t duplicate spam.
- The reminder itself never calls a model. The decision is entirely deterministic.
The decision flow, per appointment
Reminder schedules: 48/2 isn’t magic, it’s in the doc
The rules doc has one short section per service. Each section names the schedule in plain prose: “Dental cleaning: remind at 48 and 2 hours. Salon haircut: 24 and 1 hour. New-patient consult: 72, 24, and 3 hours. Quick follow-up: 24 hours only.” The numbers are hours remaining when the reminder fires. The first number is the early heads-up. The last number is the same-day nudge — the “see you soon” text that catches the customer who forgot.
The schedules exist for a reason. A 48-hour dental reminder gives the customer time to reschedule if the day no longer works, which turns a no-show into a filled slot. A 1-hour salon nudge is the last-chance reminder for a walk-in-style appointment people book on impulse and forget. A 72-hour consult reminder respects that a longer, more valuable appointment deserves more notice. Different services have different rhythms; the schedules reflect that.
Per-appointment overrides exist too. The list sheet has an optional column called schedule_override. Type a comma-separated list of hours there and the reminder uses your numbers instead of the service default for that one row. This is the right escape hatch for the VIP client who asked to be reminded the night before, or the first-timer you want to nudge twice.
Four moves, always
Every appointment, every tick, lands in exactly one of four buckets. The names are simple on purpose.
- Scheduled. The appointment is more than the first window away, or it’s already confirmed or cancelled. Do nothing. Most appointments, most hours, are scheduled.
- First reminder. The appointment just crossed the first window threshold and there’s no reply yet. Send a fresh reminder with full context. Write a row to the
ar-sendsDynamoDB table marking that the first window has fired. - Second reminder. A later window crossed without a confirm. Send a closer-to-the-time nudge that’s shorter and friendlier — “See you at 9 this morning.” Write the new send to
ar-sends. - Gap alert. The customer cancelled, or the appointment is only an hour or two away and still completely unconfirmed. Tell the front desk, in their Slack, so they can call the customer or offer the slot to someone on the waitlist. A gap alert goes to staff, not the customer — it’s the system’s way of saying “this chair might be empty; do something about it now.”
State that makes the decision deterministic
The reminder reads two DynamoDB tables every tick. ar-sends records every reminder that’s gone out: (appt_id, window_index, sent_at, channel). ar-replies records every reply: (appt_id, replied_at, action). With those two tables, the move-decision logic is a few dozen lines of Python and zero magic. A given appointment with a given time, a given reminder schedule, and a given send/reply history always produces the same move. Re-running the tick produces no extra texts (because the state in DDB shows what already fired).
Rescheduling an appointment is an explicit reset of both tables for that booking: rows for the old time are kept for audit, but a new chain starts fresh against the new appointment time. Part 5 covers the reschedule flow in detail.
Why the hourly tick uses no model
The reminder could call a model on the tick to write a smarter message, or to decide whether to send at all. It doesn’t. Two reasons. First, the tick should be the one part of the system that is utterly predictable — if the rules doc says remind at 48 hours and there’s no confirm, the text fires. A model in that loop introduces variance the front desk can’t reason about. Second, model calls cost money, and most hours most appointments are scheduled, so the call would be wasted again and again.
Bedrock fires elsewhere — on the inbound parsing lane in Part 2, and on the weekly staff summary mentioned in Part 6. Not on the hourly tick. The reminder itself is plain Python that reads a doc and writes events.
Next post: how a reminder reaches the customer, how quiet hours and holidays are honored, and what a confirm actually does to the chain.
All posts