Building a Spa and Wellness Booking Agent: Service Selection and Scheduling
Build an AI booking agent for spas and wellness centers that handles service selection, therapist matching, package recommendations, and real-time availability scheduling.
The Spa Scheduling Challenge
Spa booking is more complex than standard appointment scheduling. Services have variable durations (30 minutes to 3 hours), specific therapists specialize in different treatments, rooms have equipment constraints (hydrotherapy tub vs. massage table vs. facial bed), and many guests want to book multi-service packages with logical sequencing — you do not apply a facial after a body wrap, and you need buffer time between treatments.
An AI booking agent navigates all of these constraints conversationally, guiding guests to the perfect spa experience while maximizing the facility's utilization rate.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
Spa Domain Model
from dataclasses import dataclass, field
from datetime import datetime, timedelta, time
from typing import Optional
@dataclass
class SpaService:
service_id: str
name: str
category: str # massage, facial, body, nail, wellness
duration: timedelta
price: float
description: str
requires_room_type: str # massage_room, facial_room, wet_room, nail_station
buffer_after: timedelta = timedelta(minutes=15)
@dataclass
class Therapist:
therapist_id: str
name: str
specializations: list[str] # service categories they can perform
certifications: list[str] = field(default_factory=list)
rating: float = 4.5
schedule: dict[str, list[tuple[time, time]]] = field(default_factory=dict)
# schedule: {"2026-03-17": [(time(9,0), time(17,0))]}
@dataclass
class SpaRoom:
room_id: str
room_type: str
name: str
bookings: list[dict] = field(default_factory=list)
@dataclass
class SpaPackage:
package_id: str
name: str
services: list[str] # service_ids in recommended order
total_duration: timedelta
price: float # discounted from individual prices
description: str
savings: float
@dataclass
class SpaBooking:
booking_id: str
guest_name: str
guest_phone: str
services: list[SpaService]
therapist: Therapist
room: SpaRoom
start_time: datetime
end_time: datetime
total_price: float
notes: str = ""
Scheduling Engine
The scheduling engine finds available slots by cross-referencing therapist availability, room bookings, and service durations.
flowchart LR
CALLER(["Client"])
subgraph TEL["Telephony"]
SIP["Twilio SIP and PSTN"]
end
subgraph BRAIN["Salon AI Agent"]
STT["Streaming STT<br/>Deepgram or Whisper"]
NLU{"Intent and<br/>Entity Extraction"}
TOOLS["Tool Calls"]
TTS["Streaming TTS<br/>ElevenLabs or Rime"]
end
subgraph DATA["Live Data Plane"]
CRM[("CRM and Notes")]
CAL[("Calendar and<br/>Schedule")]
KB[("Knowledge Base<br/>and Policies")]
end
subgraph OUT["Outcomes"]
O1(["Appointment booked"])
O2(["Reschedule completed"])
O3(["Stylist handoff"])
end
CALLER --> SIP --> STT --> NLU
NLU -->|Lookup| TOOLS
TOOLS <--> CRM
TOOLS <--> CAL
TOOLS <--> KB
NLU --> TTS --> SIP --> CALLER
NLU -->|Resolved| O1
NLU -->|Schedule| O2
NLU -->|Escalate| O3
style CALLER fill:#f1f5f9,stroke:#64748b,color:#0f172a
style NLU fill:#4f46e5,stroke:#4338ca,color:#fff
style O1 fill:#059669,stroke:#047857,color:#fff
style O2 fill:#0ea5e9,stroke:#0369a1,color:#fff
style O3 fill:#f59e0b,stroke:#d97706,color:#1f2937
def find_available_slots(
service: SpaService,
target_date: str,
therapists: list[Therapist],
rooms: list[SpaRoom],
slot_interval: timedelta = timedelta(minutes=30),
) -> list[dict]:
target = datetime.strptime(target_date, "%Y-%m-%d").date()
total_needed = service.duration + service.buffer_after
available_slots = []
# Filter therapists who can perform this service
qualified = [
t for t in therapists
if service.category in t.specializations
]
# Filter rooms of the right type
suitable_rooms = [r for r in rooms if r.room_type == service.requires_room_type]
for therapist in qualified:
day_schedule = therapist.schedule.get(target_date, [])
for shift_start, shift_end in day_schedule:
current = datetime.combine(target, shift_start)
shift_end_dt = datetime.combine(target, shift_end)
while current + total_needed <= shift_end_dt:
slot_end = current + total_needed
# Check therapist is not already booked
therapist_free = True # simplified; check existing bookings
# Check room availability
for room in suitable_rooms:
room_free = all(
not (current < b["end"] and slot_end > b["start"])
for b in room.bookings
)
if therapist_free and room_free:
available_slots.append({
"start": current,
"end": current + service.duration,
"therapist": therapist,
"room": room,
})
break
current += slot_interval
return available_slots
Building the Booking Agent Tools
from agents import Agent, function_tool
spa_services = [
SpaService("SV1", "Swedish Massage", "massage", timedelta(minutes=60),
95.0, "Classic relaxation massage with long flowing strokes",
"massage_room"),
SpaService("SV2", "Deep Tissue Massage", "massage", timedelta(minutes=90),
135.0, "Targeted pressure for chronic tension and knots",
"massage_room", timedelta(minutes=20)),
SpaService("SV3", "Hydrating Facial", "facial", timedelta(minutes=50),
85.0, "Deep cleanse with hyaluronic acid and collagen mask",
"facial_room"),
SpaService("SV4", "Hot Stone Therapy", "massage", timedelta(minutes=75),
125.0, "Heated basalt stones with massage for deep relaxation",
"massage_room"),
SpaService("SV5", "Body Wrap", "body", timedelta(minutes=60),
110.0, "Detoxifying seaweed wrap with full body exfoliation",
"wet_room"),
]
spa_packages = [
SpaPackage("PKG1", "Relaxation Retreat", ["SV1", "SV3"],
timedelta(hours=2, minutes=15), 160.0,
"Swedish massage followed by hydrating facial", 20.0),
SpaPackage("PKG2", "Ultimate Indulgence", ["SV5", "SV2", "SV3"],
timedelta(hours=3, minutes=45), 290.0,
"Body wrap, deep tissue massage, and facial", 40.0),
]
therapists: list[Therapist] = []
rooms: list[SpaRoom] = []
@function_tool
def browse_services(category: str = "") -> str:
filtered = spa_services
if category:
filtered = [s for s in spa_services if category.lower() in s.category]
lines = []
for s in filtered:
duration_min = int(s.duration.total_seconds() / 60)
lines.append(
f"- **{s.name}** ({duration_min} min, ${s.price:.2f})\n"
f" {s.description}"
)
return "\n".join(lines) if lines else "No services in that category."
@function_tool
def browse_packages() -> str:
lines = []
for pkg in spa_packages:
duration_min = int(pkg.total_duration.total_seconds() / 60)
lines.append(
f"- **{pkg.name}** ({duration_min} min, ${pkg.price:.2f} — "
f"save ${pkg.savings:.2f})\n {pkg.description}"
)
return "\n".join(lines)
@function_tool
def check_availability(service_id: str, target_date: str) -> str:
service = next((s for s in spa_services if s.service_id == service_id), None)
if not service:
return f"Service {service_id} not found."
slots = find_available_slots(service, target_date, therapists, rooms)
if not slots:
return f"No availability for {service.name} on {target_date}."
lines = [f"Available slots for {service.name} on {target_date}:"]
for slot in slots[:6]:
lines.append(
f" {slot['start'].strftime('%I:%M %p')} with "
f"{slot['therapist'].name} (rated {slot['therapist'].rating}/5)"
)
return "\n".join(lines)
@function_tool
def book_appointment(
guest_name: str, guest_phone: str, service_id: str,
target_date: str, preferred_time: str, therapist_preference: str = ""
) -> str:
service = next((s for s in spa_services if s.service_id == service_id), None)
if not service:
return f"Service {service_id} not found."
duration_min = int(service.duration.total_seconds() / 60)
return (
f"Booking confirmed for {guest_name}:\n"
f" Service: {service.name} ({duration_min} min)\n"
f" Date: {target_date} at {preferred_time}\n"
f" Price: ${service.price:.2f}\n"
f" Please arrive 15 minutes early to enjoy the relaxation lounge.\n"
f" Confirmation sent to {guest_phone}."
)
@function_tool
def recommend_for_concern(concern: str) -> str:
concern_map = {
"stress": ["SV1", "SV4"],
"tension": ["SV2"],
"skin": ["SV3"],
"detox": ["SV5"],
"pain": ["SV2", "SV4"],
"relaxation": ["SV1", "SV4"],
}
concern_lower = concern.lower()
matched_ids = []
for key, ids in concern_map.items():
if key in concern_lower:
matched_ids.extend(ids)
matched_ids = list(dict.fromkeys(matched_ids))
if not matched_ids:
return "I would recommend starting with a consultation. Could you describe your concern in more detail?"
matched = [s for s in spa_services if s.service_id in matched_ids]
lines = [f"For {concern}, I recommend:"]
for s in matched:
lines.append(f"- {s.name} (${s.price:.2f}): {s.description}")
return "\n".join(lines)
spa_agent = Agent(
name="Spa Booking Agent",
instructions="""You are a spa and wellness booking agent. Help guests
find the right treatments for their needs, check availability, and
book appointments. Ask about any health concerns or preferences first.
Recommend packages when guests want multiple services. Always mention
the 15-minute early arrival recommendation.""",
tools=[browse_services, browse_packages, check_availability,
book_appointment, recommend_for_concern],
)
FAQ
How does the agent handle multi-service bookings that require specific sequencing?
The agent sequences services following spa best practices: exfoliation before wraps, wraps before massages, and facials last (since the guest's face stays product-free during body treatments). The scheduling engine allocates buffer time between services and ensures the same therapist is available for consecutive treatments when possible, reducing transition time and improving the guest experience.
What if a guest has a medical condition that contraindicates certain treatments?
The agent asks about health conditions, pregnancy, and recent surgeries before recommending services. Each service has a contraindications list (for example, hot stone therapy is contraindicated for guests with circulatory conditions). The agent filters these out automatically and explains why certain treatments are unavailable, suggesting safe alternatives instead.
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
How does therapist matching work beyond basic availability?
The agent considers multiple factors: the therapist's specialization match, their rating score, guest preference for male or female therapist, and whether the guest has seen this therapist before (returning guests often prefer continuity). The scheduling engine scores each available therapist and presents the best match first, with alternatives if the guest prefers a different option.
#SpaBooking #WellnessAI #SchedulingAgent #AgenticAI #Python #LearnAI #AIEngineering
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.