AI Agent for Appointment-Based Businesses: Salons, Spas, and Professional Services
Build an AI scheduling agent that handles appointment booking, cancellations, reminders, rebooking, and waitlist management for salons, spas, and service-based small businesses.
Why Appointment Scheduling Is the Perfect AI Use Case
For salons, spas, massage therapists, and professional service firms, the phone rings constantly with the same request: "Can I book an appointment?" Staff spend hours each day on scheduling tasks that follow predictable rules — checking availability, matching services to providers, sending confirmations. An AI agent handles these interactions instantly, freeing staff to focus on the clients who are already in the chair.
This tutorial walks through building a complete scheduling agent with booking, cancellation, reminder, rebooking, and waitlist capabilities.
Data Model for Scheduling
A solid scheduling agent starts with a clear data model. We need to represent services, providers, time slots, and appointments.
from dataclasses import dataclass, field
from datetime import datetime, date, time, timedelta
from enum import Enum
from typing import Optional
import uuid
class AppointmentStatus(Enum):
CONFIRMED = "confirmed"
CANCELLED = "cancelled"
COMPLETED = "completed"
NO_SHOW = "no_show"
WAITLISTED = "waitlisted"
@dataclass
class Service:
id: str
name: str
duration_minutes: int
price: float
category: str
providers: list[str] # provider IDs who can perform this service
@dataclass
class TimeSlot:
provider_id: str
start: datetime
end: datetime
is_available: bool = True
@dataclass
class Appointment:
id: str = field(default_factory=lambda: str(uuid.uuid4()))
client_name: str = ""
client_phone: str = ""
service_id: str = ""
provider_id: str = ""
start_time: Optional[datetime] = None
end_time: Optional[datetime] = None
status: AppointmentStatus = AppointmentStatus.CONFIRMED
notes: str = ""
reminder_sent: bool = False
Availability Engine
The availability engine is the heart of the scheduling agent. It must check provider schedules, account for existing appointments, respect buffer times between appointments, and handle lunch breaks.
class AvailabilityEngine:
def __init__(self):
self.appointments: list[Appointment] = []
self.provider_schedules: dict[str, dict] = {}
self.buffer_minutes: int = 15 # gap between appointments
def set_provider_schedule(
self, provider_id: str, day: str,
start: time, end: time, lunch_start: time, lunch_end: time
):
if provider_id not in self.provider_schedules:
self.provider_schedules[provider_id] = {}
self.provider_schedules[provider_id][day] = {
"start": start, "end": end,
"lunch_start": lunch_start, "lunch_end": lunch_end,
}
def get_available_slots(
self, provider_id: str, target_date: date,
duration_minutes: int
) -> list[TimeSlot]:
day_name = target_date.strftime("%A").lower()
schedule = self.provider_schedules.get(provider_id, {}).get(day_name)
if not schedule:
return []
day_start = datetime.combine(target_date, schedule["start"])
day_end = datetime.combine(target_date, schedule["end"])
lunch_start = datetime.combine(target_date, schedule["lunch_start"])
lunch_end = datetime.combine(target_date, schedule["lunch_end"])
existing = sorted(
[a for a in self.appointments
if a.provider_id == provider_id
and a.start_time and a.start_time.date() == target_date
and a.status == AppointmentStatus.CONFIRMED],
key=lambda a: a.start_time,
)
slots = []
current = day_start
duration = timedelta(minutes=duration_minutes)
buffer = timedelta(minutes=self.buffer_minutes)
while current + duration <= day_end:
slot_end = current + duration
# Skip lunch
if current < lunch_end and slot_end > lunch_start:
current = lunch_end
continue
# Check conflicts with existing appointments
conflict = False
for appt in existing:
appt_start = appt.start_time - buffer
appt_end = appt.end_time + buffer
if current < appt_end and slot_end > appt_start:
conflict = True
current = appt.end_time + buffer
break
if not conflict:
slots.append(TimeSlot(
provider_id=provider_id,
start=current, end=slot_end
))
current += timedelta(minutes=30) # 30-min increments
return slots
Agent Tools for Booking Operations
We expose the scheduling engine to the agent through function tools that handle each booking operation.
See AI Voice Agents Handle Real Calls
Book a free demo or calculate how much you can save with AI voice automation.
from agents import Agent, Runner, function_tool
engine = AvailabilityEngine()
SERVICES = {
"haircut": Service("s1", "Haircut", 30, 35.0, "hair", ["p1", "p2"]),
"color": Service("s2", "Color Treatment", 90, 120.0, "hair", ["p1"]),
"massage": Service("s3", "Swedish Massage", 60, 85.0, "body", ["p3"]),
}
@function_tool
def check_availability(
service_name: str, preferred_date: str,
preferred_provider: str = ""
) -> str:
"""Check available appointment slots for a service on a given date."""
service = SERVICES.get(service_name.lower())
if not service:
return f"Service '{service_name}' not found. Available: {list(SERVICES.keys())}"
target = date.fromisoformat(preferred_date)
providers = [preferred_provider] if preferred_provider else service.providers
results = []
for pid in providers:
slots = engine.get_available_slots(pid, target, service.duration_minutes)
for slot in slots[:5]:
results.append(f"{pid}: {slot.start.strftime('%I:%M %p')}")
return "\n".join(results) if results else "No availability on that date."
@function_tool
def book_appointment(
client_name: str, client_phone: str,
service_name: str, provider_id: str, slot_time: str
) -> str:
"""Book an appointment for a client."""
service = SERVICES.get(service_name.lower())
start = datetime.fromisoformat(slot_time)
end = start + timedelta(minutes=service.duration_minutes)
appt = Appointment(
client_name=client_name, client_phone=client_phone,
service_id=service.id, provider_id=provider_id,
start_time=start, end_time=end,
)
engine.appointments.append(appt)
return f"Booked: {service.name} with {provider_id} at {start.strftime('%I:%M %p')} on {start.strftime('%B %d')}. Confirmation ID: {appt.id[:8]}"
@function_tool
def cancel_appointment(confirmation_id: str, reason: str = "") -> str:
"""Cancel an existing appointment by confirmation ID."""
for appt in engine.appointments:
if appt.id.startswith(confirmation_id):
appt.status = AppointmentStatus.CANCELLED
appt.notes = f"Cancelled: {reason}"
return f"Appointment {confirmation_id} cancelled. Would you like to rebook?"
return "Appointment not found. Please check your confirmation ID."
Waitlist Management
When preferred slots are taken, the agent should offer waitlist placement rather than losing the booking entirely.
waitlist: list[dict] = []
@function_tool
def join_waitlist(
client_name: str, client_phone: str,
service_name: str, preferred_date: str
) -> str:
"""Add a client to the waitlist for a fully booked date."""
waitlist.append({
"client_name": client_name,
"client_phone": client_phone,
"service": service_name,
"date": preferred_date,
"added_at": datetime.now().isoformat(),
})
return (
f"{client_name} added to the waitlist for {service_name} "
f"on {preferred_date}. We will call if a slot opens up."
)
Assembling the Scheduling Agent
scheduling_agent = Agent(
name="Salon Scheduling Agent",
instructions="""You are a friendly scheduling assistant for a salon and spa.
1. When a client wants to book, ask which service they need and their preferred date.
2. Use check_availability to find open slots, then present the top 3 options.
3. Once the client picks a slot, collect their name and phone, then book_appointment.
4. If no slots are available, offer to join_waitlist.
5. For cancellations, ask for the confirmation ID and the reason.
6. Always confirm the final details before booking or cancelling.
7. Mention the service price when presenting options.""",
tools=[check_availability, book_appointment, cancel_appointment, join_waitlist],
)
FAQ
How do I send automated appointment reminders?
Run a background scheduler (using APScheduler or a cron job) that queries appointments 24 hours before their start time. For each appointment where reminder_sent is False, send an SMS or email, then set the flag to True. The agent itself does not need to handle this — it is a separate async process.
What happens if two people try to book the same slot simultaneously?
In production, wrap the booking operation in a database transaction with a row-level lock on the time slot. If the slot was already claimed between the availability check and the booking attempt, return an error and offer the next available slot. This is a standard optimistic concurrency pattern.
Can the agent handle multi-service bookings like "haircut and color"?
Yes. Extend the book_appointment tool to accept a list of service IDs, sum the durations, and find a contiguous block of availability. The agent instructions should tell it to ask whether the client wants to combine services with the same provider or split across providers.
#AppointmentScheduling #SmallBusiness #AIAgent #BookingSystem #Python #AgenticAI #LearnAI #AIEngineering
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.