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:
- Add Startup Trigger
- 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"
}
]
- 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
}
- Add Set Variable for blocked dates:
Variable: shared.blocked_dates
Value: []
- Add Set Variable for all bookings:
Variable: shared.bookings
Value: []
Step 1.2: The Booking Command
- Add Command Trigger for
/book - Add Send Message:
📅 Appointment Booking
What type of appointment would you like to book?
- Add For Each node on
global.services - 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
-
Add Message Trigger for callbacks matching
service_* -
Add Transform to extract service ID:
- Expression:
replace(ctx.Input, "service_", "") - Save to
var.booking_service_id
- Expression:
-
Add Filter to get service details:
- Input:
global.services - Condition:
item.id == var.booking_service_id - Get first, save to
var.booking_service
- Input:
-
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
- 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
-
Add Map to create buttons:
- Each button:
{{item.formatted}}|date_{{format(item.date, "2006-01-02")}}
- Each button:
-
Add Reply Markup with date buttons (2 per row)
Step 2.3: Handle Date Selection
-
Add Message Trigger for
date_*callbacks -
Add Transform to parse selected date:
- Expression:
replace(ctx.Input, "date_", "") - Save to
var.booking_date
- Expression:
-
Store formatted date for display:
- Expression:
format(parseDate(var.booking_date), "Monday, January 2, 2006") - Save to
var.booking_date_display
- Expression:
Part 3: Time Slot Selection
Step 3.1: Generate Available Slots
- 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
-
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
- Input:
-
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
- 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:
- Add Send Message:
📅 `{{var.booking_date_display}}`
Select an available time:
- Generate time slot buttons:
- ⚪ 9:00 AM |
time_9_0 - ⚪ 9:30 AM |
time_9_30 - etc.
- ⚪ 9:00 AM |
Step 3.2: Handle Time Selection
-
Add Message Trigger for
time_*callbacks -
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])
- Expression:
-
Format for display:
- Expression:
formatTime(flow.booking_hour, flow.booking_minute) - Save to
var.booking_time_display
- Expression:
Part 4: Customer Details & Confirmation
Step 4.1: Collect Customer Info
Check if we already have the customer's info:
- Add Condition:
var.customer_name != nil && var.customer_name != ""
If new customer:
- Add Send Message:
Almost done! I need a few details.
What's your name?
-
Add Ask node:
- Save to
var.customer_name - Validation:
len(value) >= 2
- Save to
-
Add Send Message:
And your phone number? (for appointment reminders)
- Add Ask node:
- Save to
var.customer_phone - Validation:
len(replace(value, " ", "")) >= 10
- Save to
If returning customer:
Jump to confirmation step.
Step 4.2: Show Booking Summary
-
Add Transform to create booking ID:
- Expression:
"APT-" + upper(string(now().Unix())[5:]) - Save to
flow.booking_id
- Expression:
-
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:
- Add Reply Markup:
- ✅ Confirm Booking |
confirm_booking - 📅 Change Date/Time |
change_datetime - ❌ Cancel |
cancel_booking_flow
- ✅ Confirm Booking |
Step 4.3: Save the Booking
-
Add Message Trigger for
confirm_booking -
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
}
-
Add Set Variable:
- Variable:
shared.bookings - Value:
append(shared.bookings, flow.new_booking)
- Variable:
-
Save booking ID to user's bookings:
- Variable:
var.my_bookings - Value:
append(var.my_bookings ?? [], flow.booking_id)
- Variable:
-
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
-
Add Command Trigger for
/admin -
Add Guard to check admin status:
- Expression:
user.id == global.admin_id - Block Message: "🚫 Admin access required"
- Expression:
-
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)))}}`
- Add Reply Markup:
- 📅 Today's Schedule |
admin_today - 📋 All Bookings |
admin_all - 🚫 Block Date |
admin_block - ⚙️ Settings |
admin_settings
- 📅 Today's Schedule |
Step 5.2: View Today's Bookings
-
Add Message Trigger for
admin_today -
Add Filter:
- Input:
shared.bookings - Condition:
item.date == format(now(), "2006-01-02") && item.status == "confirmed" - Sort by:
item.hour * 60 + item.minute
- Input:
-
Add Condition:
len(flow.today_bookings) == 0
If no bookings:
📅 Today's Schedule
No bookings scheduled for today.
If has bookings:
- Add Map to format:
map(
flow.today_bookings,
b -> formatTime(b.hour, b.minute) + " - " + b.service_name + " (" + b.customer_name + ")"
)
- 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
- Add Message Trigger for
admin_block - Add Send Message:
🚫 Block a Date
Enter the date you want to block (YYYY-MM-DD format):
Example: 2024-12-25
-
Add Ask node:
- Validation: Pattern match for date format
- Save to
flow.block_date
-
Add Set Variable:
- Variable:
shared.blocked_dates - Value:
append(shared.blocked_dates, flow.block_date)
- Variable:
-
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
-
Add Command Trigger for
/mybookings -
Add Filter:
- Input:
shared.bookings - Condition:
item.user_id == user.id && item.status == "confirmed" - Sort by date
- Input:
-
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:
- 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
-
Add Command Trigger for
/cancel -
Add Transform to get booking ID:
- Expression:
ctx.Args[0] ?? "" - Save to
flow.cancel_id
- Expression:
-
Add Condition:
flow.cancel_id == ""
If no ID:
Please specify which booking to cancel:
/cancel APT-XXXXX
Your bookings: /mybookings
If ID provided:
-
Add Filter to find booking:
- Condition:
item.id == flow.cancel_id && item.user_id == user.id
- Condition:
-
Add Condition:
len(flow.found) == 0
If not found:
❌ Booking not found.
Make sure you're using the correct booking ID.
If found:
- 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?
-
Add Reply Markup:
- ✅ Yes, Cancel |
do_cancel_{{flow.cancel_id}} - ❌ Keep Booking |
keep_booking
- ✅ Yes, Cancel |
-
On confirmation, update booking status:
- Map over
shared.bookings - If
item.id == flow.cancel_id, setstatus = "cancelled"
- Map over
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.
-
Add Cron Trigger:
- Name:
check_reminders - Expression:
*/15 * * * *(Every 15 mins)
- Name:
-
Add Filter to find due bookings in
shared.bookings:- Condition:
item.status == "confirmed" &&
!item.reminder_sent &&
isWithin24Hours(item.date, item.hour, item.minute) -
Add For Each over due reminders:
-
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}}`
- Mark reminder as sent:
- Update
item.reminder_sent = true
- Update
Complete Flow Diagram
Nodes Used in This Tutorial
| Category | Nodes Used |
|---|---|
| Triggers | Command, Message/Callback, Startup |
| Actions | Send Message, Edit Message, Answer Callback |
| Logic | Condition, Guard, Switch |
| Flow | Ask, For Each |
| Data | Set Variable, Get, Filter, Map, Transform, Append, Range |
Key Patterns Demonstrated
- Dynamic Button Generation — Building keyboards from data
- Double-Booking Prevention — Filtering available slots
- State Management — Tracking booking flow with variables
- Admin Guard — Restricting access to admin commands
- Scheduled Actions — Reminder system architecture
Next Steps
- Multi-Language Bot → — Add language support
- Common Patterns → — Reusable solutions