Skip to content
Learn Agentic AI13 min read0 views

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.

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.

See AI Voice Agents Handle Real Calls

Book a free demo or calculate how much you can save with AI voice automation.

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.

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

Share this article
C

CallSphere Team

Expert insights on AI voice agents and customer communication automation.

Try CallSphere AI Voice Agents

See how AI voice agents work for your industry. Live demo available -- no signup required.