Skip to content
Docs

SMS Appointment Reminder System

Appointment no-shows cost the US healthcare system $150B annually, with individual practices losing $200-500 per missed slot. Manual reminder calls — still the primary method at many practices — consume 2-3 staff hours daily, reach only 60-70% of patients (due to unanswered calls and voicemail), and provide no mechanism for patients to confirm, reschedule, or cancel without calling back during business hours. The result is a 25-30% no-show rate that disrupts scheduling, wastes provider time, and delays care for other patients.

SMS reminders address reachability (95%+ open rates within 3 minutes), but one-way SMS without response handling creates its own problems: patients who want to reschedule still need to call the office, and staff still spend time processing those calls manually.

Beluga AI’s server package provides messaging backend abstractions that decouple the reminder logic from the SMS provider (Twilio, Vonage, etc.), enabling provider switching without rewriting application code. The two-way communication architecture uses intent parsing to handle patient responses — confirmations, rescheduling requests, and cancellations — without human intervention for common cases. The scheduler component manages reminder timing (typically 24 hours before appointments) and handles edge cases like same-day appointments that need immediate reminders.

┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Appointment │───▶│ Reminder │───▶│ Messaging │
│ Schedule │ │ Scheduler │ │ Backend │
└──────────────┘ └──────────────┘ └──────┬───────┘
┌──────▼───────┐
│ Patient │
│ (via SMS) │
└──────┬───────┘
┌──────────────┐ ┌──────────────┐ ┌─────▼────────┐
│ Appointment │◀───│ Rescheduling │◀───│ Response │
│ System │ │ Agent │ │ Handler │
└──────────────┘ └──────────────┘ └──────────────┘

The system manages SMS reminders with two-way communication:

package main
import (
"context"
"fmt"
"time"
"github.com/lookatitude/beluga-ai/schema"
"github.com/lookatitude/beluga-ai/server"
_ "github.com/lookatitude/beluga-ai/server/providers/twilio"
)
type AppointmentReminderSystem struct {
messaging server.MessagingBackend
scheduler *ReminderScheduler
}
func NewAppointmentReminderSystem(ctx context.Context) (*AppointmentReminderSystem, error) {
messaging, err := server.NewMessagingBackend("twilio", server.MessagingConfig{
AccountSID: "your-account-sid",
AuthToken: "your-auth-token",
Channel: "sms",
})
if err != nil {
return nil, fmt.Errorf("create messaging backend: %w", err)
}
return &AppointmentReminderSystem{
messaging: messaging,
scheduler: NewReminderScheduler(),
}, nil
}
type Appointment struct {
ID string
PatientName string
PatientPhone string
DateTime time.Time
ProviderName string
Type string
}

Send automated reminders at scheduled times:

func (s *AppointmentReminderSystem) SendReminder(ctx context.Context, appointment Appointment) error {
// Get or create conversation with patient
conversation, err := s.messaging.GetOrCreateConversation(ctx, appointment.PatientPhone)
if err != nil {
return fmt.Errorf("get conversation: %w", err)
}
// Build reminder message
message := s.buildReminderMessage(appointment)
// Send SMS
msg := schema.Message{
Content: message,
Channel: "sms",
}
if err := s.messaging.SendMessage(ctx, conversation.ID, msg); err != nil {
return fmt.Errorf("send reminder: %w", err)
}
return nil
}
func (s *AppointmentReminderSystem) buildReminderMessage(appointment Appointment) string {
return fmt.Sprintf(
"Reminder: You have a %s appointment with %s on %s. Reply 1 to confirm, 2 to reschedule, or 3 to cancel.",
appointment.Type,
appointment.ProviderName,
appointment.DateTime.Format("Mon Jan 2 at 3:04 PM"),
)
}

Process patient responses intelligently:

type ResponseHandler struct {
system *AppointmentReminderSystem
}
func (h *ResponseHandler) HandleResponse(ctx context.Context, conversationID, message string) error {
// Parse response intent
intent := h.parseIntent(message)
switch intent {
case "confirm":
return h.handleConfirmation(ctx, conversationID)
case "reschedule":
return h.handleRescheduling(ctx, conversationID)
case "cancel":
return h.handleCancellation(ctx, conversationID)
default:
return h.sendClarification(ctx, conversationID)
}
}
func (h *ResponseHandler) parseIntent(message string) string {
message = strings.TrimSpace(strings.ToLower(message))
// Simple keyword-based intent recognition
if message == "1" || strings.Contains(message, "confirm") || strings.Contains(message, "yes") {
return "confirm"
}
if message == "2" || strings.Contains(message, "reschedule") || strings.Contains(message, "change") {
return "reschedule"
}
if message == "3" || strings.Contains(message, "cancel") || strings.Contains(message, "no") {
return "cancel"
}
return "unknown"
}
func (h *ResponseHandler) handleConfirmation(ctx context.Context, conversationID string) error {
// Update appointment status to confirmed
appointment := h.getAppointmentByConversation(ctx, conversationID)
if err := h.confirmAppointment(ctx, appointment.ID); err != nil {
return err
}
// Send confirmation message
msg := schema.Message{
Content: "Thank you! Your appointment is confirmed. We look forward to seeing you.",
Channel: "sms",
}
return h.system.messaging.SendMessage(ctx, conversationID, msg)
}
func (h *ResponseHandler) handleRescheduling(ctx context.Context, conversationID string) error {
// Send rescheduling options
msg := schema.Message{
Content: "I can help you reschedule. Please call our office at (555) 123-4567 or visit our website to select a new time.",
Channel: "sms",
}
return h.system.messaging.SendMessage(ctx, conversationID, msg)
}
func (h *ResponseHandler) handleCancellation(ctx context.Context, conversationID string) error {
// Update appointment status to cancelled
appointment := h.getAppointmentByConversation(ctx, conversationID)
if err := h.cancelAppointment(ctx, appointment.ID); err != nil {
return err
}
// Send cancellation confirmation
msg := schema.Message{
Content: "Your appointment has been cancelled. Call us at (555) 123-4567 if you need to reschedule.",
Channel: "sms",
}
return h.system.messaging.SendMessage(ctx, conversationID, msg)
}
func (h *ResponseHandler) sendClarification(ctx context.Context, conversationID string) error {
msg := schema.Message{
Content: "I didn't understand your response. Reply 1 to confirm, 2 to reschedule, or 3 to cancel.",
Channel: "sms",
}
return h.system.messaging.SendMessage(ctx, conversationID, msg)
}

Schedule reminders automatically based on appointment time:

type ReminderScheduler struct {
system *AppointmentReminderSystem
}
func (s *ReminderScheduler) ScheduleReminder(ctx context.Context, appointment Appointment) error {
// Schedule reminder 24 hours before appointment
reminderTime := appointment.DateTime.Add(-24 * time.Hour)
// Calculate delay
delay := time.Until(reminderTime)
if delay < 0 {
// Appointment is less than 24 hours away, send immediately
return s.system.SendReminder(ctx, appointment)
}
// Schedule for later
timer := time.NewTimer(delay)
go func() {
<-timer.C
if err := s.system.SendReminder(context.Background(), appointment); err != nil {
// Log error
}
}()
return nil
}

Send reminders for multiple appointments efficiently:

func (s *AppointmentReminderSystem) SendBatchReminders(ctx context.Context, appointments []Appointment) error {
for _, appointment := range appointments {
if err := s.SendReminder(ctx, appointment); err != nil {
// Log error but continue with other reminders
continue
}
// Rate limit to avoid overwhelming SMS provider
time.Sleep(100 * time.Millisecond)
}
return nil
}

Track reminder delivery and response rates:

import (
"github.com/lookatitude/beluga-ai/o11y"
"go.opentelemetry.io/otel/attribute"
)
func (s *AppointmentReminderSystem) SendReminderWithTracing(ctx context.Context, appointment Appointment) error {
ctx, span := o11y.StartSpan(ctx, "reminder.send")
defer span.End()
span.SetAttributes(
attribute.String("appointment.id", appointment.ID),
attribute.String("appointment.type", appointment.Type),
attribute.String("patient.phone", appointment.PatientPhone),
)
err := s.SendReminder(ctx, appointment)
if err != nil {
span.RecordError(err)
return err
}
return nil
}

Ensure patient data privacy and security:

func (s *AppointmentReminderSystem) buildSecureReminderMessage(appointment Appointment) string {
// Don't include sensitive information in SMS
return fmt.Sprintf(
"Reminder: You have an appointment on %s. Reply 1 to confirm, 2 to reschedule, or 3 to cancel. ID: %s",
appointment.DateTime.Format("Mon Jan 2 at 3:04 PM"),
appointment.ID[:8], // Short ID for reference
)
}

After implementing SMS appointment reminders, the provider achieved:

MetricBeforeAfterImprovement
Manual Reminder Time2-3 hrs/day0 hrs/day100% reduction
No-Show Rate25-30%16%36-47% reduction
Reminder Delivery Rate60-70%92%31-53% improvement
Response Handling0%85%New capability
Revenue Recovery$0/month$22K/month$22K gain
Patient Satisfaction6.5/109.1/1040% improvement