How a freed slot gets noticed
A slot frees up three ways: a customer cancels, staff mark a no-show, or a fresh opening appears on the calendar. Any of those sends an event to the offer engine. The engine reads the waitlist, looks at the freed slot, keeps only the entries that actually fit it, sorts what’s left in the fair order, and decides whether to offer the slot to someone or hand it back. The whole decision is plain Python. No model. No vector search. Every rule lives in the rules doc, where staff can edit it without a deploy.
Key takeaways
- A cancellation, a no-show, or a new opening sends a freed-slot event to the engine.
- Eligibility filters the list first — right service, right party size, date inside the window, matching staff preference.
- Survivors are sorted in fair order: join time by default, lifted by a priority flag.
- Four moves per slot: nobody fits, make an offer, roll on, or hand back to staff.
- The engine never calls a model. The decision is entirely deterministic.
The decision flow, per freed slot
What triggers the engine: three ways a slot frees
The engine doesn’t poll all day — it wakes on an event. Three things send one. A cancellation: the customer cancels through your booking tool, which fires a webhook to a Function URL, or a staffer marks the slot cancelled in the calendar and a small sync Lambda notices. A no-show: staff tap “no-show” at the front desk a few minutes after the appointment time, which frees the rest of that slot. A new opening: someone adds availability — a stylist picks up an extra hour, a table is reconfigured — and that new gap counts as a freed slot too. Each one becomes a wl.slot_freed event carrying the slot’s service, date, time, party capacity, and staff member.
Keeping the engine event-driven matters for both cost and speed. There’s nothing running between slots, so the bill is near zero on a quiet day; and when a slot does free, the first offer goes out in seconds, not on the next poll.
Eligibility: who actually fits this slot
Before anyone is sorted, the list is filtered down to entries that can really take the slot. The rules doc spells out the checks in plain prose, and the engine applies them in order. Service: a colour slot only fits people waiting for a colour. Party size: a two-top doesn’t fit a party of four; a four-top can fit a two if the rules doc allows down-fitting, or not if it doesn’t. Date window: the slot’s date has to fall between the entry’s earliest and latest dates. Staff preference: if someone asked specifically for Jess, they’re only eligible for Jess’s slots. An entry that fails any check is dropped for this slot — it stays on the list for the next one.
This filter is the difference between a useful system and a noisy one. Offering a Tuesday slot to someone who only wants Saturdays, or a kids’ cut to someone waiting for a colour, trains people to ignore the texts. Filtering first means every offer that goes out is one the person could actually say yes to.
Fair order: written down, the same every time
The survivors are sorted by the order in the rules doc. The default is first-come, first-served by the joined timestamp — the person who’s been waiting longest gets first refusal. A priority flag can lift an entry above plain join order: a regular who got bumped by a previous cancellation, a VIP, or someone staff have promised. The doc names exactly how priority interacts with join time (for example, “priority entries first, then by join time within each tier”), so two staffers reading it get the same answer.
The point of writing the order down is fairness you can defend. When a customer asks “why did she get the slot and I didn’t?”, the answer is in the doc and the audit log, not in whoever happened to be at the desk.
Four moves, always
Every freed slot, every time, lands in exactly one of four moves. The names are simple on purpose.
- Nobody fits. The filter left no eligible entry. Leave the slot open and post a note to staff so they can fill it by phone or take a walk-in. Most slots in a thin-list business land here, and that’s fine — the system just got out of the way.
- Make an offer. There’s a top eligible person and no offer is live yet. Send them the slot and start a claim window. Write a row to the
wl-offersDynamoDB table marking the offer as live with its expiry time. - Roll on. The previous person’s window ended or they declined, and there’s a next eligible person who hasn’t been tried. Send the same slot to them with a fresh window. Mark the previous attempt as timed-out or declined in
wl-offers. - Hand back to staff. The eligible list is exhausted, or the per-slot try cap in the rules doc is reached. Return the slot to staff with a short note: how many people were tried, and that the list is spent. The slot stays open for a walk-in or a phone fill.
State that keeps it deterministic and double-book-proof
The engine reads one DynamoDB table to make the call: wl-offers, which records the live offer per slot and the history of who’s been tried — (slot_id, offer_seq, entry_id, status, window_ends_at). With that, the move logic is a few dozen lines of Python and zero magic. A given slot, a given list, and a given offer history always produce the same move. And because only one offer per slot is ever marked live, the next person is never texted while someone else is still inside their window — which is the rule that, together with the conditional-write claim in Part 5, makes double-booking impossible.
Why this path uses no model
The engine could call a model to “decide” who deserves the slot, or to write a cleverer offer. It doesn’t. Two reasons. First, who gets offered a slot has to be utterly predictable and defensible — if the rules doc says first-come among those who fit, that’s who gets it. A model in that loop introduces variance staff can’t reason about and a customer can’t be told. Second, slots free up at the busiest moments, when speed matters; a deterministic filter-and-sort runs in milliseconds. Bedrock fires elsewhere — on the inbound parsing lane in Part 2 and on the monthly summary in Part 6 — not on the offer path.
Next post: how an offer reaches the next person — channel choice, quiet hours, the claim link and countdown, and the four guardrails on every send.
All posts