Back to the build library
Healthcare & clinics
Advanced

Clinic Waitlist and Cancellation Fill

A ranked standby waitlist and a tiered offer engine that refills a canceled slot in minutes without double-booking it.

Industry
Healthcare & clinics
Build effort
Advanced
Typical time to launch
3–5 weeks
On this page
The problemWhat gets builtHow it runsAutomations in detailWhat changesBefore you start
Want this built for you?

Bring your stack to a call and we will scope it against how your business actually runs.

Book a call
01

The problem

A patient cancels at four in the afternoon for a nine o'clock appointment the next morning. The front desk notes it, maybe tells the provider, and the hour disappears. Nobody has time to work backwards through a waitlist that lives in a notebook, a spreadsheet tab and three people's memories, and even if they did, calling twelve patients to find one who can come at nine tomorrow is forty minutes of phone work for an hour of revenue. So the slot goes empty. Meanwhile there are patients who have been waiting six weeks for that exact appointment and would have driven in gladly if anyone had asked them.

Every practice knows this is happening and most cannot quantify it, which is part of the problem. The vacated hour does not appear anywhere as a loss. It shows up as a provider with a gap, a schedule that looks busy on paper, and a waiting list that keeps growing while the calendar has holes in it. A practice running eight providers and losing two unfilled slots per provider per week is losing the equivalent of a provider's half-day every single week, permanently, and the capacity it needs to fix the waiting list is sitting right there unused.

Doing it manually fails for structural reasons, not effort reasons. By the time a human gets to the call the window has shrunk. Calling down a list sequentially means each call takes two minutes and the eighth patient hears about the slot sixteen minutes later than the first. Texting everyone at once creates a race that ends in a double-booking and two annoyed patients. And there is no memory: the patient who was offered three slots last week at times they cannot do gets offered a fourth, decides the practice is not listening, and stops answering. This build solves all four, structured preferences so candidates are matched not guessed, tiered offer waves with exclusive windows so the race is controlled, a single atomic claim so the slot cannot be taken twice, and a fairness layer so the same patients are not carpet-bombed.

02

What gets built

Pipeline and stages

Two objects, which is why this is an advanced build. The waitlist roster lives on contacts with structured preference fields and smart lists. Each vacated appointment becomes its own card in a pipeline called Slot Fill, so the fill attempt has its own lifecycle and its own clock.

  • Slot Vacated: a cancellation or reschedule has freed time; nothing offered yet.
  • Candidates Matched: the eligible, ranked candidate set has been built and sized.
  • Offer Wave 1: the top-ranked few hold an exclusive window.
  • Offer Wave 2: the window lapsed; a wider band has been offered.
  • Open Broadcast: offered to every eligible candidate at once, first claim wins.
  • Claimed: Pending Confirm: a patient has claimed and the slot is locked to them.
  • Booked: confirmed on the provider's calendar.
  • Filled: terminal success; the visit happened or is imminent.
  • Unfilled: terminal; the slot passed empty, with a reason recorded.
  • Withdrawn: terminal; the original patient reinstated, or the provider released the time.

Calendars

  • Provider service calendars: one per provider per visit-type group, with real durations, buffers, per-day caps and room or equipment constraints. Short-notice fill only works if the calendar actually knows how long each appointment type takes; a generic thirty-minute slot will get filled with a visit that needs fifty.
  • Short-Notice Eligible: a calendar configuration flag per visit type. Some appointment types cannot be offered at short notice because they need preparation, an interpreter, a pre-visit step or equipment setup. This is a clinical decision recorded per visit type, and it is the single most important guardrail in the build.
  • Standby Hold: a short internal hold placed on the slot the moment it is claimed, so staff looking at the calendar see it as taken rather than open while confirmation is pending.
  • Waitlist Consult: 10 minutes, round robin, for patients who want to talk through their availability rather than fill in a form.

Custom fields on the waitlist roster

  • waitlist_status: active, paused, removed.
  • visit_type_code: internal code for what the patient is waiting for. Never a readable reason for visit.
  • provider_preference: named provider, any provider in group, or no preference.
  • location_preference
  • earliest_acceptable_date and latest_useful_date
  • days_available: per-weekday flags, not free text.
  • time_window_preference: morning, afternoon, either.
  • notice_required_hours: the minimum warning this patient needs. The field that makes the whole thing work: a patient who needs twelve hours should never be offered a two-hour slot.
  • same_day_ok: explicit yes or no.
  • waitlist_priority_score: computed, see workflow 1.
  • clinical_priority_band: set by a clinician only, never by the patient and never computed.
  • waitlist_added_date, days_waiting
  • offers_sent_count, offers_accepted_count, offers_declined_count, offers_ignored_count
  • last_offer_sent_at, offer_cooldown_until
  • no_show_history_flag
  • interpreter_needed, accessibility_needs_on_file
  • patient_consent_sms, patient_consent_email, consent_captured_at, consent_language_version, consent_source
  • preferred_channel, preferred_language, portal_activated, timezone

Custom fields on the Slot Fill card

  • slot_id, slot_start, slot_end, slot_duration_minutes
  • slot_provider, slot_location, slot_visit_type_code, short_notice_eligible
  • vacated_at, vacated_reason: patient canceled, patient rescheduled, no-show converted, provider released.
  • lead_time_hours: recomputed continuously; drives the whole wave strategy.
  • offer_wave, candidates_matched_count, offers_outstanding
  • claim_token: single-use, slot-specific, per-candidate.
  • claimed_by, claimed_at, confirmed_at
  • fill_outcome: filled, unfilled, withdrawn.
  • unfilled_reason: no eligible candidates, no claims, claim lapsed, lead time too short, not short-notice eligible.
  • time_to_fill_minutes

Tags

  • waitlist-active, waitlist-paused, waitlist-removed
  • short-notice-ok, same-day-ok
  • offer-pending, offer-accepted, offer-declined, offer-expired, offer-cooldown
  • fill-won, fill-lost
  • offer-fatigue-watch: applied at three ignored offers.
  • no-show-risk, interpreter-needed
  • consent-sms-yes, consent-sms-revoked, consent-email-yes, consent-email-revoked

No tag and no field name states a reason for visit, a condition or a procedure. visit_type_code is a code precisely so that nothing readable can be merged into a message by accident.

Forms

  • Waitlist Request: patient-facing. Structured only: which days, which time windows, how much notice you need, same day acceptable yes or no, provider preference, location, interpreter needed. No free-text clinical box. The visit type is set by staff from the booking context, not typed by the patient.
  • Offer Claim: the tokenized page a patient lands on from an offer. Shows date, time, location, duration and a single claim button. It does not show the reason for visit. One token, one slot, one use.
  • Availability Update: a short form the patient can reach any time to change their windows or pause themselves. Linked from every offer, because the cheapest way to stop annoying someone is to let them fix their own preferences.
  • Short-Notice Eligibility Review: internal, completed once per visit type by a clinician, recording whether that type can be offered at short notice and the minimum lead time it requires.
  • Communication Consent: patient-facing, per channel, versioned.

BO's role and guardrails

  • BO may: add a patient to the waitlist by walking them through the structured questions, read back and update their availability, explain how the offer process works, confirm whether an offer is still open, send a claim link, confirm a claim went through, process a decline, pause a patient's waitlist entry, and handle opt-outs.
  • BO may not: state or restate the reason for the visit, name the visit type in readable form, give preparation instructions, discuss anything clinical, set or override clinical_priority_band, promise a slot, quote any price, or confirm a booking that the claim engine has not locked.
  • Guardrails: BO never confirms a slot from conversation alone. Confirmation comes only from the claim engine in workflow 4, so two patients talking to BO at once cannot both be told yes. Any clinical question or urgency language is handed to a human immediately. BO's messages carry no clinical content in an SMS body or an email subject line. When a patient asks to be bumped up the list, BO records the request and routes it to a clinician; it never adjusts priority itself.

Numbers and channels

  • A dedicated scheduling number, separate from marketing, registered as transactional traffic, with two-way replies monitored during clinic hours. Short-notice offers are worthless on an unmonitored number.
  • Individual sends only. Every offer goes out as a separate one-to-one message. No group SMS thread, no shared recipient list, no visible CC. A group message that reveals a dozen patients to each other is a disclosure, not a convenience.
  • Voice fallback: for the top-ranked candidates on high-value slots, or any patient with no consented digital channel, an outbound call task placed by a person. On very short lead times a call beats a text.
  • Patient portal for anything beyond date, time, location and duration: preparation instructions in particular live there and nowhere else.
03

How it runs, step by step

  1. A patient cannot be offered the appointment they need inside the useful window, so staff add them to the waitlist. They complete Waitlist Request at the desk, by portal link, or by talking to BO, and the structured preferences are recorded: crucially notice_required_hours and same_day_ok.
  2. Workflow 1 computes waitlist_priority_score from clinical band, days waiting, how often this patient has accepted offers before, and whether they have ignored several. The score is recomputed nightly so the list stays honest as people wait longer.
  3. A cancellation lands. The calendar frees the slot, and workflow 2 creates a Slot Fill card with slot_start, slot_provider, slot_visit_type_code, slot_duration_minutes and lead_time_hours.
  4. Eligibility gates run first. If short_notice_eligible is no for that visit type, or lead_time_hours is below the minimum that type requires, the card moves straight to Unfilled with a reason and a task to consider using the time for admin or overbooking relief. The system does not try to fill a slot that should not be filled.
  5. The candidate set is built: matching visit type, matching provider preference, matching location, the weekday and time window in their availability, inside their date range, notice_required_hours satisfied by the actual lead time, not in cooldown, not paused, consented on a channel. The set is ranked by waitlist_priority_score.
  6. The wave strategy is chosen by lead time. Comfortable lead time means a narrow first wave with an exclusive window. Tight lead time compresses the waves. Very short lead time skips straight to an open broadcast, because there is no time for politeness.
  7. Wave one goes to the top three candidates as individual messages, each with its own single-use claim_token, and each holding an exclusive window: typically twelve minutes. The message carries the practice name, the date, the time, the location and a link. Nothing else.
  8. A patient taps the link and lands on Offer Claim. They press claim. The claim engine attempts an atomic lock on the slot.
  9. The first valid claim wins. The slot is locked, a Standby Hold is placed on the calendar immediately, the card moves to Claimed: Pending Confirm, and every other outstanding token for that slot is invalidated in the same operation. Patients who tap a now-dead link see a plain message that the slot has gone and an offer to stay on the list: never an error page.
  10. Confirmation runs. The appointment is written to the provider's calendar with the correct duration and buffers, confirmed_at is set, and the card moves to Booked. The patient gets a confirmation that again carries only date, time, location and a portal link.
  11. Preparation instructions, if the visit type has any, are delivered in the portal only, with a doorbell message saying there are instructions to read. Preparation text frequently reveals the procedure, so it never goes in a text message.
  12. Because the appointment is imminent, the reminder ladder is compressed: a single confirmation now and one reminder at the sensible interval for the remaining lead time, instead of the standard three-touch sequence firing all at once.
  13. If wave one's window lapses with no claim, wave two opens to the next band of candidates with a shorter window, then an open broadcast to everyone eligible. Each wave recomputes eligibility against the shrinking lead_time_hours, so candidates who needed more notice than now remains drop out automatically.
  14. If the slot passes unfilled, the card moves to Unfilled with a reason, every outstanding token dies, and the attempt feeds the fill-performance digest. Offer counters and cooldowns update on every candidate who was contacted, so the fairness layer carries forward into the next cancellation.
04

Automations in detail

1. Waitlist Intake, Scoring and Nightly Re-Rank

Trigger: Waitlist Request submitted, or staff add a patient manually; plus a nightly scheduled re-score of every active entry.

Actions in order:

  1. Write the structured preferences. Reject the entry back to staff if visit_type_code, notice_required_hours or days_available is missing. An entry without those three cannot be matched and will only generate bad offers.
  2. Apply waitlist-active, plus same-day-ok and short-notice-ok where the patient said yes.
  3. Compute waitlist_priority_score from four inputs: clinical_priority_band as the dominant weight, days_waiting, an acceptance-history component rewarding patients who have claimed before, and a responsiveness component that gently demotes patients with three or more ignored offers. Clinical band always outranks the behavioral components. A clinically urgent patient is never demoted for being slow to answer a text.
  4. Send a plain confirmation: “Hi {{contact.first_name}}, you're on the waitlist at {{practice_name}}. If something opens up that matches what you told us, we'll text you a link: first to claim it gets it. Update what works for you any time: {{availability_link}}.” It sets the expectation that offers are a race, which prevents most of the complaints.
  5. Nightly: recompute days_waiting and waitlist_priority_score for every active entry. Expire entries past latest_useful_date by applying waitlist-paused and sending one check-in: “Hi {{contact.first_name}}, still want to stay on the {{practice_name}} waitlist? Confirm or update here: {{availability_link}}.” No answer in 7 days moves them to waitlist-removed with a task for staff review.
  6. Nightly hygiene: flag entries whose days_waiting has passed the practice's threshold without a single matching offer. That pattern almost always means their stated availability cannot be matched by the real schedule, and the fix is a Waitlist Consult call, not another month of waiting.

Exit: entry removed, or patient booked and no longer waiting.

2. Slot Vacated Detection, Eligibility Gates and Candidate Matching

Trigger: an appointment is canceled, rescheduled away, or released by a provider; or a no-show is converted to a vacated slot.

Actions in order:

  1. Create the Slot Fill card at Slot Vacated. Write slot_id, slot_start, slot_end, slot_duration_minutes, slot_provider, slot_location, slot_visit_type_code, vacated_at, vacated_reason. Compute lead_time_hours.
  2. Gate A: reinstatement grace. If vacated_reason is patient rescheduled and the reschedule is still in flight, wait 10 minutes before offering. Nothing is more damaging than giving away a slot the original patient is still mid-way through moving.
  3. Gate B: short-notice eligibility. If short_notice_eligible is no for this visit type, move to Unfilled with reason not short-notice eligible, notify the provider that the time is free, and stop. This gate is clinician-owned and non-negotiable.
  4. Gate C: minimum lead time. If lead_time_hours is below the minimum recorded for this visit type, move to Unfilled with reason lead time too short, and create a task offering the time for admin, catch-up or an overbooked patient.
  5. Gate D: quiet hours. If the offer would send outside the configured window, hold it. For a slot more than 14 hours out, queue to the next window. For a slot inside the window where the only possible contact is now, hold it and create a staff task. A human may call; automation does not text patients at midnight to fill a nine o'clock.
  6. Build the candidate set. Filter on: visit_type_code match, provider_preference satisfied, location_preference match, slot_start weekday present in days_available, time_window_preference match, slot_start inside the date range, lead_time_hours at or above notice_required_hours, same-day flag satisfied if the slot is today, offer_cooldown_until in the past, waitlist_status active, at least one consented channel, and slot_duration_minutes adequate for the visit type.
  7. Rank by waitlist_priority_score descending, then by days_waiting descending as the tiebreak. Write candidates_matched_count and move to Candidates Matched.
  8. If the count is zero, move to Unfilled with reason no eligible candidates, notify the provider, and log the slot's attributes. A pattern of zero-candidate slots at a particular time is a waitlist-composition problem worth fixing.
  9. Choose the wave strategy from lead_time_hours and hand to workflow 3.

Exit: handed to workflow 3, or terminal Unfilled, or Withdrawn if the original patient reinstates.

3. Tiered Offer Wave Engine

Trigger: stage becomes Candidates Matched.

Conditions: the three strategies:

  • Comfortable, lead time over 48 hours: wave 1 to the top 3 with a 30-minute exclusive window; wave 2 to the next 10 with 20 minutes; broadcast after that.
  • Tight, lead time 6–48 hours: wave 1 to the top 3 with a 12-minute window; wave 2 to the next 8 with 10 minutes; broadcast after that.
  • Urgent, lead time under 6 hours: skip the waves entirely, broadcast immediately to every eligible candidate, and in parallel create voice-call tasks for the top 3. At this lead time a phone call is the highest-yield channel and the text is the backstop.

Actions in order:

  1. Generate a distinct single-use claim_token for each candidate in the wave, bound to this slot_id and this contact, with an expiry set to the window length.
  2. Send individually, one message per candidate, never as a group. SMS: “Hi {{contact.first_name}}, an earlier appointment just opened at {{practice_name}}: {{slot_day}} {{slot_time}} at {{slot_location}}, {{slot_duration}} minutes. First to claim gets it. Your link: {{claim_link}}. Expires in {{window_minutes}} min. Not for you? Reply NO and we'll keep you on the list.” Email subject line: “An earlier appointment is available at {{practice_name}}”. No visit type, no reason, no preparation detail, no provider specialty, in either.
  3. Increment offers_sent_count and set last_offer_sent_at on every candidate contacted. Apply offer-pending. Write offers_outstanding and offer_wave on the card. Move the card to the matching offer stage.
  4. Wait the window length, or exit immediately on a successful claim.
  5. On window expiry with no claim: invalidate every token from that wave, apply offer-expired to those contacts, increment offers_ignored_count for non-responders and offers_declined_count for explicit declines, recompute lead_time_hours, and re-filter the remaining candidates against the new shorter lead time: patients whose notice_required_hours no longer fits drop out rather than being offered something they cannot use.
  6. Open the next wave. If the strategy has escalated because lead time has fallen, switch strategies mid-flight: a Tight slot that is now under 6 hours goes straight to broadcast.
  7. Broadcast wave: all remaining eligible candidates, individual sends, 1-second stagger to avoid carrier throttling, window set to whatever lead time allows. Copy adds urgency honestly: “Hi {{contact.first_name}}, this just opened for {{slot_day}} {{slot_time}} and it's open to everyone on the list now: {{claim_link}}. First claim gets it.”
  8. Decline handling: an inbound NO invalidates that patient's token, applies offer-declined, and sets offer_cooldown_until to a short cooldown so they are not immediately re-offered a similar slot an hour later. It never removes them from the waitlist.

Exit: a claim is locked, the broadcast window expires, or slot_start is reached.

4. Claim, Atomic Lock and Double-Book Prevention

Trigger: a claim is submitted from Offer Claim, or BO receives an explicit claim in conversation.

This is the workflow that makes the build safe. Every path runs through it; nothing else may book the slot.

Actions in order:

  1. Validate the token: exists, matches this slot_id, matches this contact, not expired, not already used. Any failure routes to the already-gone path at step 6.
  2. Attempt the lock. Check the card is still in an offer stage and claimed_by is empty. If both hold, write claimed_by and claimed_at in a single operation and move the card to Claimed: Pending Confirm. The stage transition is the lock; nothing downstream books without it.
  3. Invalidate everything else in the same step. Kill every other outstanding token for this slot_id, clear offer-pending from those contacts, and set offers_outstanding to zero. This must happen in the same action block as the lock, not after a wait. A gap here is exactly where a double-booking appears.
  4. Place the Standby Hold on the provider's calendar immediately, before any confirmation message goes out, so a staff member booking manually in the next thirty seconds sees the slot as taken.
  5. Verify the fit before writing the real appointment: duration still adequate, no conflicting booking created in the interim, provider still available, slot_start still in the future. Any failure releases the lock, notifies staff, and reopens the card to Open Broadcast rather than silently booking something wrong.
  6. Already-gone path. A patient arriving on a dead token sees a plain page: “This appointment has already been claimed. You're still on the waitlist and we'll let you know next time something opens. Update your availability here: {{availability_link}}.” Send the same as a reply if they texted. No error screens, no silence. A losing claimant who is told clearly stays on the list; one who hits a broken page stops answering.
  7. BO path. If a patient claims in conversation, BO calls this workflow and waits for its verdict before saying anything. BO never confirms from its own view of the record.
  8. On a successful lock, hand to workflow 5.

Exit: lock held and confirmed, or lock released and the card reopened.

5. Confirmation, Preparation Delivery and Reminder Compression

Trigger: stage becomes Claimed: Pending Confirm.

Actions in order:

  1. Write the appointment to the provider's calendar with the real duration and buffers. Set confirmed_at, compute time_to_fill_minutes from vacated_at, release the Standby Hold into the booking, move the card to Booked.
  2. Update the contact: offers_accepted_count plus one, apply fill-won, remove waitlist-active, set waitlist_status to paused for this visit type so they do not receive offers for an appointment they now have.
  3. Send confirmation: “You're booked, {{contact.first_name}}, {{slot_day}} {{slot_time}} at {{slot_location}} with {{practice_name}}. Everything you need to know, including anything to do beforehand, is in your secure portal: {{portal_link}}.” Date, time, location, link. Nothing else.
  4. Preparation branch. If the visit type has preparation requirements, publish them to the portal and send a doorbell only: “There are a couple of things to read before your visit. They're in your portal: {{portal_link}}.” Preparation text routinely reveals the procedure, so it never appears in an SMS body or an email subject line.
  5. Interpreter and access branch. If interpreter_needed or accessibility_needs_on_file is set, create a staff task immediately with the remaining lead time shown. On a short-notice fill this is the step most likely to fail, so it gets its own alert rather than relying on someone noticing.
  6. Compressed reminder ladder, chosen by remaining lead time: over 48 hours, reminders at 24 hours and 2 hours. 6–48 hours, one reminder at 2 hours. Under 6 hours, no further reminder at all. The confirmation was the reminder. The standard three-touch sequence is explicitly suppressed so a patient who books for tomorrow morning does not get four messages tonight.
  7. Notify the provider and the front desk that the slot filled, with the patient identifier and the time to fill. Internal channel only, access-controlled.
  8. Late cancellation branch. If this newly booked appointment is itself canceled, create a fresh Slot Fill card and run the whole engine again, with the original claimant's cooldown respected.

Exit: appointment reaches slot_start; card moves to Filled.

6. Unfilled Slot Fallback

Trigger: the broadcast window expires with no claim, or slot_start arrives with the card still in an offer stage.

Actions in order:

  1. Invalidate every outstanding token. Clear offer-pending from all contacted candidates. Move the card to Unfilled and write unfilled_reason.
  2. Branch: candidates existed but nobody claimed. Create a task to call the top 3 by phone if there is still usable lead time. A voice call converts short-notice slots far better than a text and the system should not give up while a human could still fill it.
  3. Branch: no eligible candidates. Log the slot's attributes (provider, weekday, time window, visit type) into the unfilled-pattern report. Repeated zero-candidate Thursday mornings is an actionable finding about who you are putting on the waitlist.
  4. Notify the provider and practice manager that the time is now genuinely free, early enough to be useful for admin, catch-up or relieving an overbooked clinic.
  5. Update every contacted candidate's counters and set a modest offer_cooldown_until, so a patient who ignored one offer is not hit again twenty minutes later by the next cancellation.
  6. Feed time_to_fill_minutes, wave reached and unfilled_reason into the digest in workflow 9.

Exit: terminal.

7. Fairness, Cooldown and Offer-Fatigue Control

Trigger: runs continuously as a gate on workflow 3, plus a weekly scheduled review.

Actions in order:

  1. Hard caps per contact: no more than 3 offers in a rolling 7 days and no more than 1 offer in any rolling 4 hours, regardless of how many slots open. A candidate at cap is skipped silently and the next-ranked candidate is used. The cap exists because the fastest way to destroy a waitlist is to make being on it feel like spam.
  2. Cooldowns: an explicit decline sets a short cooldown; an ignored offer sets a shorter one; a claim that lost the race sets none at all, because that patient did everything right and should get the next one.
  3. Fatigue watch: at 3 consecutive ignored offers, apply offer-fatigue-watch and send one preference check rather than another offer: “Hi {{contact.first_name}}, we've sent a few openings that didn't suit. Tell us what actually works and we'll only send those: {{availability_link}}. Or reply PAUSE to pause.”
  4. At 5 consecutive ignored offers, move to waitlist-paused and create a task for a human call. Continuing to text someone who has ignored five offers teaches them to ignore the sixth, which is the one that mattered.
  5. Starvation check, weekly: list active entries with high days_waiting and zero offers received. Create a Waitlist Consult task for each. These are the patients the algorithm is quietly failing, and they will never complain until they leave.
  6. Equity check, weekly: report offer volume by clinical_priority_band and by days_waiting decile. If the behavioral components of the score are crowding out clinical band or long waiters, reweight. Review the weighting with a clinician, not just with whoever is looking at the dashboard.

Exit: runs permanently.

8. Consent, Quiet Hours and Content Guardrail

Trigger: waitlist entry created with no consent, Communication Consent submission, or any inbound opt-out keyword.

Actions in order:

  1. Capture. Record patient_consent_sms and patient_consent_email separately with consent_captured_at, consent_language_version and consent_source, storing the exact wording shown rather than a summary. A waitlist entry with no consented channel is explicitly a phone-only entry, flagged as such, and is never silently skipped.
  2. Separation. Scheduling consent is recorded separately from marketing consent. A patient opted out of campaigns still gets their cancellation offers; a patient who opts out of scheduling texts is moved to phone-only rather than dropped off the list.
  3. On opt-out. Set the channel flag to no, apply the revoked tag, invalidate any outstanding token on that channel, remove the contact from active waves, flag the entry phone-only, and send one confirmation: “You're unsubscribed from texts from {{practice_name}}. You're still on the waitlist: we'll call you instead.”
  4. Quiet hours. All offers are held to the configured window in the patient's timezone. A slot whose only viable offer time falls in quiet hours becomes a staff call task, not an automated 2am text. Decide the morning cutover time deliberately, opening the window at 7am to fill an 8am slot is reasonable; 5am is not.
  5. Content guardrail. Before any send, verify the body and subject line against an allow-list of merge fields: first name, practice name, slot day, slot time, location, duration, claim link, availability link, portal link, window length. visit_type_code, slot_visit_type_code, preparation text, provider specialty and any clinical field are blocked outright. A failing send is blocked and raised as an internal alert, never silently rewritten.
  6. Group-send guardrail. The engine refuses any send with more than one recipient on the message. Offers are always one-to-one, always a per-contact token. There is no configuration that permits a group thread.

Exit: runs permanently as the guard layer.

9. Daily Fill Performance Digest

Trigger: scheduled, each weekday before the first clinic block, plus a weekly roll-up.

Actions in order:

  1. Summarize yesterday: slots vacated, slots filled, fill rate, median and worst time_to_fill_minutes, and the wave at which each fill happened.
  2. List every Unfilled card with its unfilled_reason, grouped so patterns are visible rather than scattered.
  3. Flag today's at-risk gaps: slots already vacated for today or tomorrow still in an offer stage, so a human can pick up the phone while there is still time.
  4. List offer-fatigue-watch contacts, starved long-waiters, and any waitlist entry rejected for missing required preference fields.
  5. Send internally only, to the practice manager and scheduling lead, on an access-controlled channel. It contains patient identifiers.
  6. Weekly, add: fill rate by provider, by weekday and by time window; broadcast-reached share, which tells you whether the narrow first wave is working; zero-candidate slot patterns; and offer-to-claim conversion by wave.

Exit: never.

05

What changes

The canceled hour stops being a loss and becomes a transaction. A slot that frees up at four in the afternoon is offered, claimed and confirmed before anyone at the desk has finished the sentence about it, and the patient who gets it is the one the ranking says should, not whoever happened to be at the top of a paper list. Over a quarter the visible change is a schedule with fewer holes and a waitlist that shortens, and the invisible change is that nobody is spending forty minutes on the phone to fill one appointment.

The second change is that the waitlist becomes a real asset instead of a backlog. Structured availability means you can answer questions you could not before: which time windows you genuinely cannot staff, which providers are oversubscribed, how many people are waiting for something your schedule never produces. The zero-candidate and starvation reports turn a list of frustrated people into a capacity-planning input.

The third change is narrower but it is the one that keeps the build out of trouble: patients stop being spammed. Caps, cooldowns and the fatigue check mean a waitlist offer keeps its signal value, so when the message does arrive people still open it.

Metrics to watch

  • Fill rate: filled divided by vacated, excluding cards that failed the short-notice eligibility gate, because those were never fillable and including them flatters the number.
  • Median and 90th-percentile time_to_fill_minutes, split by lead-time band. The 90th percentile is where the manual phone work still lives.
  • Wave at fill: the share filled in wave 1, wave 2 and broadcast. Mostly wave 1 means the ranking is good; mostly broadcast means the matching logic is too loose or the top candidates are not reachable.
  • Offer-to-claim conversion by wave and by channel, plus voice-task conversion on urgent slots.
  • Unfilled reasons as a distribution: no eligible candidates versus no claims versus lead time too short. Each one has a different fix and lumping them together hides all three.
  • Zero-candidate slot patterns by provider, weekday and time window.
  • Double-booking incidents: target zero. Any non-zero value is a defect in workflow 4, not a tuning problem, and it is the one number that should trigger an immediate stop.
  • Claim-race losers who stayed on the list versus those who went quiet afterwards. If losers churn, the already-gone message needs work.
  • Offer load per patient: mean and maximum offers per contact per week, and how often the cap bit.
  • Waitlist health: active entries, median days_waiting, starved entries, paused rate, and opt-out rate on the scheduling channel.
  • Guardrail health: blocked sends caught by the content check and any attempted multi-recipient send. Both should be zero in steady state, and both are worth an alert rather than a dashboard tile.
06

Before you start

Prerequisites

  • Accurate appointment durations and buffers on every visit type. Short-notice fill writes real bookings into real calendars; if the durations are wrong, the system will cheerfully book a fifty-minute visit into a thirty-minute gap. Fix this before anything else.
  • A clinician-completed short-notice eligibility review per visit type, recording whether it can be offered at short notice and the minimum lead time it needs. Signed off, not assumed.
  • A cancellation event the platform can actually see. If cancellations are recorded only in a separate practice management system with no outbound notification, the engine has nothing to trigger on and that integration is the real first task.
  • Agreed wave sizes, window lengths and lead-time bands. The values in this guide are sensible starting points, not findings; tune them against your own conversion data after a month.
  • Agreed ranking weights, with clinical_priority_band dominant and set only by clinicians.
  • A monitored two-way inbox during clinic hours, and an honest decision about what the engine does outside them.
  • Role-based access so the digest, the waitlist roster and the fill cards are visible only to the roles that need them.

Integrations

  • Calendar sync (Google Calendar or Outlook) two-way, with conflict detection. One-way sync is not sufficient for this build: the claim engine has to be able to see a booking made elsewhere thirty seconds ago.
  • Your practice management or scheduling system, for cancellation events in and confirmed bookings out. Decide which system is the source of truth for the schedule and make the other follow it; two systems both believing they own the calendar is how slots get double-booked.
  • Patient portal for preparation instructions and anything beyond date, time, location and duration.
  • Reporting export for the fill-performance roll-up. Exports carry patient data and get the same handling as the system.

Numbers and messaging registration

  • Provision a dedicated scheduling number and complete A2P 10DLC brand and campaign registration in the United States, describing the campaign accurately as transactional appointment scheduling. This build depends on messages arriving within minutes, so filtering is fatal. A filtered offer looks identical to an ignored one in the data, and you will tune the wrong thing.
  • Keep the scheduling number and campaign separate from marketing so opt-outs do not cross over in either direction.
  • Broadcast waves send many individual messages in a short burst. Check your per-second and per-minute throughput limits, stagger sends by about a second, and confirm your provider will not rate-limit a thirty-recipient broadcast into uselessness. Test this with a real burst before launch, not with three test contacts.
  • Automatic immediate opt-out handling on recognized keywords, with the required opt-in, help and opt-out wording present where the carrier rules say.
  • Quiet hours in the patient's local time, with the morning cutover chosen deliberately. Set the per-patient daily cap globally across all programs, not per workflow.
  • Use one stable claim-link domain. Rotating shorteners get filtered, and a filtered claim link in a twelve-minute window is a lost slot.

Protected health information and consent

A cancellation offer looks like the most harmless message in healthcare, and it is one of the easier ones to get wrong, because the useful details are exactly the disclosing ones.

  • Keep PHI out of SMS bodies and email subject lines. Date, time, location, duration, practice name and a link are enough to claim a slot. The reason for visit, the visit type in readable form, the provider's specialty where the specialty is effectively a diagnosis, and preparation instructions all stay out. Preparation text is the trap worth naming twice: instructions about fasting, medication holds or what to bring frequently reveal the procedure, so they belong in the portal with a neutral doorbell pointing at them.
  • Never send a group message. Every offer is one-to-one with its own token. A group SMS thread or a visible recipient list discloses to each patient that the others are patients of the practice, and on a broadcast wave that can be thirty people at once. The engine should refuse multi-recipient sends structurally rather than relying on anyone remembering.
  • Apply minimum necessary to the already-gone page too. It confirms the slot is taken; it does not say who took it, and it does not restate what the appointment was for.
  • Capture consent explicitly, per channel, and version it. Record the exact wording shown, the version, timestamp, method and source. Keep scheduling consent separate from marketing consent and honor revocation of either without destroying the other. Keep revocation events as history. A patient who opts out of scheduling texts should move to phone-only on the waitlist, not quietly fall off it.
  • Put a signed business associate agreement in place before any patient data flows. The platform acts as a business associate when it handles patient data for the practice, and so does every downstream vendor: the SMS carrier or aggregator, the email infrastructure, the calendar provider, which in this build holds appointment times and patient names and is therefore squarely in scope. Any voice or transcription service used for the call-task fallback, and any warehouse or spreadsheet you export the fill report into. Identify each, confirm each is covered, and remember that an agreement with the platform does not extend to an integration you connected yourself.
  • Treat the waitlist roster as a sensitive dataset in its own right. A list of people waiting for a particular visit type at a particular practice can be disclosing even without a diagnosis attached, which is exactly why visit_type_code is a code. Restrict access by role, set retention periods, and prune entries that are no longer live rather than keeping a years-long list because it might be useful.
  • Keep the audit trail complete. Every offer sent, token issued, token invalidated, claim attempted, lock taken and lock released should be attributable with a timestamp. When two patients both believe they claimed the same slot, the trail is the only thing that will settle it.
  • Watch the guardrail alerts rather than muting them. The content check and the multi-recipient refusal are the controls that stop a well-meaning template edit from turning a helpful offer into a disclosure six months from now.

All of the above describes operational practice, not legal advice. Which requirements apply to your practice, how consent must be worded for your jurisdiction, what retention periods apply, which vendors need agreements and in what form, how state privacy and telecoms law interact with federal requirements, and whether a given offer counts as a treatment communication or as marketing are questions for the practice's own compliance counsel and privacy officer. Have them review the offer templates, the claim page, the consent wording, the vendor chain and the retention settings before the first offer goes out, and again whenever a template or integration changes.

Scope and build cost are set per practice. Book a call at bookings.appbo.io to work through yours.

Build it with us.

Every build on this page is something we configure, test and hand over with the SOPs your team needs to run it.

Book a call