Skip to main content

Advanced: Appointment Booking System

Build a complete appointment booking system with date selection, time slots, confirmations, reminders, and admin management.


What We're Building

A professional booking bot that:

  • 📅 Shows available dates and times
  • 🔒 Prevents double-booking
  • ✅ Sends confirmations
  • ⏰ Sends reminders (24h before)
  • 📊 Admin dashboard for managing bookings
  • ❌ Allows cancellations and rescheduling

Part 1: Service Configuration

Step 1.1: Initialize Services

Create a Startup Trigger flow to set up your services:

  1. Add Startup Trigger
  2. Add Set Variable for services:

Variable: global.services Value:

[
{
"id": "consultation",
"name": "📋 Initial Consultation",
"duration": 30,
"price": 0,
"description": "Free 30-minute discovery call"
},
{
"id": "coaching",
"name": "🎯 Coaching Session",
"duration": 60,
"price": 99,
"description": "One-on-one coaching session"
},
{
"id": "workshop",
"name": "🎓 Workshop",
"duration": 120,
"price": 199,
"description": "2-hour intensive workshop"
}
]
  1. Add Set Variable for business hours:

Variable: global.business_hours Value:

{
"start": 9,
"end": 18,
"days": [1, 2, 3, 4, 5],
"timezone": "America/New_York",
"slot_interval": 30
}
  1. Add Set Variable for blocked dates:

Variable: shared.blocked_dates Value: []

  1. Add Set Variable for all bookings:

Variable: shared.bookings Value: []


Step 1.2: The Booking Command

  1. Add Command Trigger for /book
  2. Add Send Message:
📅 Appointment Booking

What type of appointment would you like to book?
  1. Add For Each node on global.services
  2. Build dynamic buttons from services:

Reply Markup (dynamically generated):

  • 📋 Initial Consultation (Free) | service_consultation
  • 🎯 Coaching Session ($99) | service_coaching
  • 🎓 Workshop ($199) | service_workshop

Part 2: Date Selection

Step 2.1: Handle Service Selection

  1. Add Message Trigger for callbacks matching service_*

  2. Add Transform to extract service ID:

    • Expression: replace(ctx.Input, "service_", "")
    • Save to var.booking_service_id
  3. Add Filter to get service details:

    • Input: global.services
    • Condition: item.id == var.booking_service_id
    • Get first, save to var.booking_service
  4. Add Send Message with service confirmation:

`{{var.booking_service.name}}`
`{{var.booking_service.description}}`

⏱️ Duration: `{{var.booking_service.duration}}` minutes
💰 Price: `{{var.booking_service.price == 0 ? "Free" : "$" + string(var.booking_service.price)}}`

Please select a date:

Step 2.2: Generate Available Dates

  1. Add Transform to generate next 7 business days:
// Generate date buttons for next 7 available days
filter(
map(
range(0, 14),
day -> {
"date": addDays(now(), day),
"dayOfWeek": dayOfWeek(addDays(now(), day)),
"formatted": format(addDays(now(), day), "Mon, Jan 2")
}
),
item -> contains(global.business_hours.days, item.dayOfWeek) &&
!contains(shared.blocked_dates, format(item.date, "2006-01-02"))
)[:7]

Save to flow.available_dates

  1. Add Map to create buttons:

    • Each button: {{item.formatted}} | date_{{format(item.date, "2006-01-02")}}
  2. Add Reply Markup with date buttons (2 per row)


Step 2.3: Handle Date Selection

  1. Add Message Trigger for date_* callbacks

  2. Add Transform to parse selected date:

    • Expression: replace(ctx.Input, "date_", "")
    • Save to var.booking_date
  3. Store formatted date for display:

    • Expression: format(parseDate(var.booking_date), "Monday, January 2, 2006")
    • Save to var.booking_date_display

Part 3: Time Slot Selection

Step 3.1: Generate Available Slots

  1. Add Transform to generate all possible slots:
map(
range(global.business_hours.start, global.business_hours.end),
hour -> [
{"hour": hour, "minute": 0, "display": formatTime(hour, 0)},
{"hour": hour, "minute": 30, "display": formatTime(hour, 30)}
]
) | flatten

Save to flow.all_slots

  1. Add Filter to get booked slots for selected date:

    • Input: shared.bookings
    • Condition: item.date == var.booking_date && item.status != "cancelled"
    • Save to flow.booked_slots
  2. Add Filter to remove booked times:

filter(
flow.all_slots,
slot -> !any(
flow.booked_slots,
booking -> booking.hour == slot.hour && booking.minute == slot.minute
)
)

Save to flow.available_slots

  1. Add Condition: len(flow.available_slots) == 0

If no slots:

😕 Sorry, no available times on `{{var.booking_date_display}}`.

Please select a different date:
[🔙 Back to Dates]

If slots available:

  1. Add Send Message:
📅 `{{var.booking_date_display}}`

Select an available time:
  1. Generate time slot buttons:
    • ⚪ 9:00 AM | time_9_0
    • ⚪ 9:30 AM | time_9_30
    • etc.

Step 3.2: Handle Time Selection

  1. Add Message Trigger for time_* callbacks

  2. Add Transform to parse time:

    • Expression: split(replace(ctx.Input, "time_", ""), "_")
    • Extract: flow.booking_hour = int(parts[0])
    • Extract: flow.booking_minute = int(parts[1])
  3. Format for display:

    • Expression: formatTime(flow.booking_hour, flow.booking_minute)
    • Save to var.booking_time_display

Part 4: Customer Details & Confirmation

Step 4.1: Collect Customer Info

Check if we already have the customer's info:

  1. Add Condition: var.customer_name != nil && var.customer_name != ""

If new customer:

  1. Add Send Message:
Almost done! I need a few details.

What's your name?
  1. Add Ask node:

    • Save to var.customer_name
    • Validation: len(value) >= 2
  2. Add Send Message:

And your phone number? (for appointment reminders)
  1. Add Ask node:
    • Save to var.customer_phone
    • Validation: len(replace(value, " ", "")) >= 10

If returning customer:

Jump to confirmation step.


Step 4.2: Show Booking Summary

  1. Add Transform to create booking ID:

    • Expression: "APT-" + upper(string(now().Unix())[5:])
    • Save to flow.booking_id
  2. Add Send Message:

📋 Booking Summary

🔖 Booking ID: `{{flow.booking_id}}`
━━━━━━━━━━━━━━━━━

📌 Service: `{{var.booking_service.name}}`
⏱️ Duration: `{{var.booking_service.duration}}` minutes
💰 Price: `{{var.booking_service.price == 0 ? "Free" : "$" + string(var.booking_service.price)}}`

📅 Date: `{{var.booking_date_display}}`
🕐 Time: `{{var.booking_time_display}}`

👤 Name: `{{var.customer_name}}`
📞 Phone: `{{var.customer_phone}}`

Please confirm your booking:
  1. Add Reply Markup:
    • ✅ Confirm Booking | confirm_booking
    • 📅 Change Date/Time | change_datetime
    • ❌ Cancel | cancel_booking_flow

Step 4.3: Save the Booking

  1. Add Message Trigger for confirm_booking

  2. Add Transform to create booking object:

{
"id": flow.booking_id,
"service_id": var.booking_service_id,
"service_name": var.booking_service.name,
"date": var.booking_date,
"hour": flow.booking_hour,
"minute": flow.booking_minute,
"user_id": user.id,
"customer_name": var.customer_name,
"customer_phone": var.customer_phone,
"status": "confirmed",
"created_at": now(),
"reminder_sent": false
}
  1. Add Set Variable:

    • Variable: shared.bookings
    • Value: append(shared.bookings, flow.new_booking)
  2. Save booking ID to user's bookings:

    • Variable: var.my_bookings
    • Value: append(var.my_bookings ?? [], flow.booking_id)
  3. Add Edit Message:

✅ Booking Confirmed!

🔖 Your booking ID: `{{flow.booking_id}}`

📌 `{{var.booking_service.name}}`
📅 `{{var.booking_date_display}}` at `{{var.booking_time_display}}`

📧 We'll send you a reminder 24 hours before.

━━━━━━━━━━━━━━━━━
Manage your booking:
/mybookings - View all bookings
/cancel `{{flow.booking_id}}` - Cancel this booking

Part 5: Admin Management

Step 5.1: Admin Dashboard

  1. Add Command Trigger for /admin

  2. Add Guard to check admin status:

    • Expression: user.id == global.admin_id
    • Block Message: "🚫 Admin access required"
  3. Add Send Message:

👑 Admin Dashboard

📊 Stats:
• Total Bookings: `{{len(shared.bookings)}}`
• Today: `{{len(filter(shared.bookings, b -> b.date == today()))}}`
• This Week: `{{len(filter(shared.bookings, b -> isThisWeek(b.date)))}}`
  1. Add Reply Markup:
    • 📅 Today's Schedule | admin_today
    • 📋 All Bookings | admin_all
    • 🚫 Block Date | admin_block
    • ⚙️ Settings | admin_settings

Step 5.2: View Today's Bookings

  1. Add Message Trigger for admin_today

  2. Add Filter:

    • Input: shared.bookings
    • Condition: item.date == format(now(), "2006-01-02") && item.status == "confirmed"
    • Sort by: item.hour * 60 + item.minute
  3. Add Condition: len(flow.today_bookings) == 0

If no bookings:

📅 Today's Schedule

No bookings scheduled for today.

If has bookings:

  1. Add Map to format:
map(
flow.today_bookings,
b -> formatTime(b.hour, b.minute) + " - " + b.service_name + " (" + b.customer_name + ")"
)
  1. Add Send Message:
📅 Today's Schedule (`{{format(now(), "Jan 2")}}`)

`{{join(flow.formatted_bookings, "\n")}}`

Total: `{{len(flow.today_bookings)}}` appointments

Step 5.3: Block/Unblock Dates

  1. Add Message Trigger for admin_block
  2. Add Send Message:
🚫 Block a Date

Enter the date you want to block (YYYY-MM-DD format):
Example: 2024-12-25
  1. Add Ask node:

    • Validation: Pattern match for date format
    • Save to flow.block_date
  2. Add Set Variable:

    • Variable: shared.blocked_dates
    • Value: append(shared.blocked_dates, flow.block_date)
  3. Add Send Message:

✅ Date `{{flow.block_date}}` has been blocked.

No new bookings will be available for this date.

Part 6: Customer Management

Step 6.1: View My Bookings

  1. Add Command Trigger for /mybookings

  2. Add Filter:

    • Input: shared.bookings
    • Condition: item.user_id == user.id && item.status == "confirmed"
    • Sort by date
  3. Add Condition: len(flow.my_bookings) == 0

If no bookings:

📋 My Bookings

You don't have any upcoming appointments.

Book one now: /book

If has bookings:

  1. Add For Each to display bookings:
📋 My Bookings (`{{len(flow.my_bookings)}}`)

`{{range flow.my_bookings}}`
━━━━━━━━━━━━━━━━━
🔖 `{{.id}}`
📌 `{{.service_name}}`
📅 `{{formatDate(.date)}}` at `{{formatTime(.hour, .minute)}}`
Status: `{{.status == "confirmed" ? "✅ Confirmed" : "❌ Cancelled"}}`
`{{end}}`

Manage: /cancel [booking_id]

Step 6.2: Cancel Booking

  1. Add Command Trigger for /cancel

  2. Add Transform to get booking ID:

    • Expression: ctx.Args[0] ?? ""
    • Save to flow.cancel_id
  3. Add Condition: flow.cancel_id == ""

If no ID:

Please specify which booking to cancel:
/cancel APT-XXXXX

Your bookings: /mybookings

If ID provided:

  1. Add Filter to find booking:

    • Condition: item.id == flow.cancel_id && item.user_id == user.id
  2. Add Condition: len(flow.found) == 0

If not found:

❌ Booking not found.

Make sure you're using the correct booking ID.

If found:

  1. Add Send Message:
⚠️ Cancel Booking?

🔖 `{{flow.booking.id}}`
📌 `{{flow.booking.service_name}}`
📅 `{{formatDate(flow.booking.date)}}` at `{{formatTime(flow.booking.hour, flow.booking.minute)}}`

Are you sure you want to cancel?
  1. Add Reply Markup:

    • ✅ Yes, Cancel | do_cancel_{{flow.cancel_id}}
    • ❌ Keep Booking | keep_booking
  2. On confirmation, update booking status:

    • Map over shared.bookings
    • If item.id == flow.cancel_id, set status = "cancelled"

Part 7: Reminder System

Step 7.1: Check for Due Reminders

We'll use a Cron Trigger to check for upcoming appointments every 15 minutes.

  1. Add Cron Trigger:

    • Name: check_reminders
    • Expression: */15 * * * * (Every 15 mins)
  2. Add Filter to find due bookings in shared.bookings:

    • Condition:
    item.status == "confirmed" && 
    !item.reminder_sent &&
    isWithin24Hours(item.date, item.hour, item.minute)
  3. Add For Each over due reminders:

  4. Add Send Message to user (with Chat ID = item.user_id):

⏰ Appointment Reminder

Hi `{{item.customer_name}}`!

You have an appointment tomorrow:

📌 `{{item.service_name}}`
📅 `{{formatDate(item.date)}}`
🕐 `{{formatTime(item.hour, item.minute)}}`

Need to reschedule? Reply with /cancel `{{item.id}}`
  1. Mark reminder as sent:
    • Update item.reminder_sent = true

Complete Flow Diagram


Nodes Used in This Tutorial

CategoryNodes Used
TriggersCommand, Message/Callback, Startup
ActionsSend Message, Edit Message, Answer Callback
LogicCondition, Guard, Switch
FlowAsk, For Each
DataSet Variable, Get, Filter, Map, Transform, Append, Range

Key Patterns Demonstrated

  1. Dynamic Button Generation — Building keyboards from data
  2. Double-Booking Prevention — Filtering available slots
  3. State Management — Tracking booking flow with variables
  4. Admin Guard — Restricting access to admin commands
  5. Scheduled Actions — Reminder system architecture

Next Steps