mahimaivoice playground
DemosAboutContributeCookbook↗
World of Voice Agents · Built by Mahimai ↗
awesome-voice-apps ↗● ConversationalX
← all demos
healthcare · surface: clinic-scheduler

Clinic scheduler

Books a doctor appointment by voice, finds open slots, and handles reschedules. Clinics and GP offices that want to automate appointment booking without a phone tree.

source ↗
NO LIVEKIT CREDENTIALSPaste your LiveKit URL, key and secret to connect the scope to a live room.
SCOPE · clinic-scheduler○ STANDBY
NO SIGNAL · PRESS CONNECT
STATUS
idle
DURATION
··
RISK
··
TRANSCRIPT

transcript will stream here

CANVAS · clinic-schedulerwaiting
agent-mounted UIappears here on connect

Books a doctor appointment by voice, finds open slots, and handles reschedules. Clinics and GP offices that want to automate appointment booking without a phone tree.

agent may draw
ListCard
REQUIRED CREDENTIALS
livekit_urllivekit_api_keylivekit_api_secretLiveKit: ✗ add your keys
Run the worker locally:
uv run python agent.py dev
then connect: the agent joins room clinic-scheduler.
build writeup

How to build a clinic scheduling voice agent

by Mahimai

A clinic phone line does one thing all day: find an open slot, confirm it with the caller, and move it when plans change. This agent does exactly that, holding the slot inventory in memory and mirroring it on screen.

The stack is OpenAI for all three roles: STT, gpt-4o-mini, and TTS on the shimmer voice. Turn-taking is LiveKit's inference VAD and turn detector.

The slots are generated from the next weekdays, computed from today, so the dates never go stale:

def _build_slots() -> list[dict]:
    days: list[datetime.date] = []
    day = datetime.date.today()
    while len(days) < 4:
        day += datetime.timedelta(days=1)
        if day.weekday() < 5:  # Monday-Friday
            days.append(day)
    ...

When the caller asks what is open, find_slots filters that inventory by an optional day or doctor and publishes the matches as a List on screen. Omit both filters and they see everything; a filter that matches nothing publishes an empty List rather than leaving stale rows up.

The interesting part is the state. One booking at a time: book_appointment refuses a second one instead of overwriting, and points the caller at reschedule.

existing = context.userdata.get("booking")
if existing is not None:
    return (
        f"You already have an appointment on {existing['date']} at "
        f"{existing['time']}. To change it, ask to reschedule."
    )

Booking moves the slot out of the open list; reschedule and cancel put the freed slot back. The put-back re-sorts by the s1..s6 id, so a freed early slot reappears in its natural position instead of drifting to the end:

remaining = [s for s in available if s["id"] != new_slot_id]
remaining.append(_freed_slot(booking))
remaining.sort(key=lambda s: int(s["id"][1:]))

Every mutation re-publishes the List, and cancel unmounts the booking Card, so the screen always matches the inventory. A list that disagrees with state is the most visible bug.

Build it from an empty folder in the full walkthrough, or talk to the finished agent at https://playground.mahimai.ca/demos/clinic-scheduler.

Read the full walkthrough→