Specfork

@specfork/scheduling

Booking pages with availability, team round robin, calendar sync, payments and webhooks.

saascalendar Version 0.2.1 122 requirements Licence CC-BY-4.0 Specfork/scheduling

Scheduling

People and teams publish booking pages; others book time without an account. Behaviour derived from Cal.com's open-source code (Cal.diy, MIT), written fresh and stack-free. Not affiliated with or endorsed by Cal.com, Inc. The name is used only to credit the source.

Area Prefix Covers
event-types.md EVT Bookable templates: durations, notice, window, limits, seats, confirmation
schedules.md AVL Weekly hours, overrides, time zones, time off, holidays
slots.md SLT Which start times are offered, buffers, limits, holds
bookings.md BKG Create, confirm, reschedule, cancel, seats, recurring, no-show
team-events.md RR Collective and round-robin team events, fairness, team permissions
calendars.md CAL Calendar and video connections, busy times, writing events
payments.md PAY Paid events, timeout, refunds, no-show fees
notifications.md NTF Emails and webhook events
public-pages.md PUB Profile, booking flow, manage booking, embeds

All times are stored in UTC; all durations are whole minutes.

Bookings

Status: Draft · Prefix: BKG

Purpose

Create, confirm, reschedule and cancel a booking, and keep calendars and people in sync.

Requirements

ID Requirement Verified by
BKG-1 A booking has uid (opaque, unique, URL-safe), event_type, organizer (a user), attendees [{name, email, time_zone, phone?, locale, no_show}], start, end (UTC), location, responses, status, paid, from_reschedule? (uid), rescheduled (bool), cancel_reason?, cancelled_by?, reject_reason?, host_no_show, sequence (starts 0), ical_uid (kept across reschedules, CAL-11). create → uid of ≥ 16 URL-safe chars
BKG-2 Statuses: awaiting_payment, pending, accepted, rejected, cancelled. Only these transitions: awaiting_payment → pending|accepted|cancelled; pending → accepted|rejected|cancelled; accepted → cancelled. accepted → rejected → refused
BKG-3 Booking form: system fields name (required), email (required), phone (hidden), location, notes (optional), guests (list of emails, hideable), reschedule_reason (reschedule only); owner-added fields of type text, textarea, number, email, phone, address, url, select, multiselect, checkbox, radio, boolean, each with name (key for responses and prefill, [a-z][a-z0-9_]{0,39}, unique), label, required, hidden, options?, min_length?, max_length?. System fields other than name and email can be hidden, none deleted; hidden fields are never required. required custom field empty → rejected
BKG-4 Field validation: emails are an addr-spec with a dotted domain; phones E.164-valid; email fields may have allow_domains/block_domains; guests de-duplicated and lower-cased; guests who are hosts of the event or the booker are dropped; each guest becomes an attendee named by their email. guest list with a duplicate → stored once
BKG-5 Location: the booker's pick from EVT-14, required when several are configured (location_required); one configured → it; none configured → organizer's default video app (CAL-5), else a plain meeting link on the instance. event without locations → video link created
BKG-6 Create re-checks, in order: event exists; duration (EVT-3); start in the future, notice and window; limits; booker limit (EVT-12); at least one host free per SLT, at a start on the slot grid. Codes: event_not_found, invalid_duration, in_past, too_soon, outside_window, limit_reached, booker_limit, slot_taken (busy or held), unavailable (no hours, time off, off-grid). two bookers race for 10:00 → one succeeds, other gets slot_taken
BKG-7 At most one accepted booking per host per exact [start, end); enforced atomically. Cancelling or rejecting frees it. concurrent identical requests → one booking
BKG-8 Same booker email, same event type, same start, while an earlier one is pending or awaiting payment → the earlier booking is returned, not a new one. submit twice → same uid
BKG-9 New status: paid event → awaiting_payment (PAY); else confirmation required (EVT-10) → pending; else accepted. An organizer rescheduling their own booking → accepted. free event, no confirmation → accepted
BKG-10 On becoming accepted: create the event in the destination calendar and the video meeting (CAL), store their external ids, notify (NTF). Integration failures are logged and shown to the organizer; the booking stands. calendar API down → booking accepted, organizer sees warning
BKG-11 Confirm/reject a pending booking: by the organizer or a team admin, or via single-use links in the organizer's request email (opening shows the booking, acting needs a POST; using either link voids both). Reject takes an optional reason. Confirming a booking that isn't pending → error; confirming into a slot taken per BKG-7 → slot_taken; confirming past a reached limit (SLT-13) → limit_reached. reuse a confirm link → refused
BKG-12 Reschedule by booker (via uid link) or organizer: creates a new booking with from_reschedule, sets the old one cancelled with rescheduled = true, updates the same external calendar event (sequence + 1), carries over paid, attendees, responses, location and duration. reschedule → old cancelled, new links back
BKG-13 Reschedule refused when allow_reschedule is false, the booking is cancelled or rejected, it has started, or a non-organizer is inside min_reschedule_notice. reschedule 1 h before start, notice 2 h → refused
BKG-14 Organizer may "request reschedule": the booking becomes cancelled with a reason and the booker gets a link to pick a new time. manual: booker email has rebook link
BKG-15 Cancel by booker (via uid link, no login) or organizer, until the booking ends; refused when allow_cancel is false; reason required per EVT-16. Deletes external calendar and video events. cancel after end → refused
BKG-16 Seats (EVT-9): one booking per slot holds up to seats attendees, each with a seat uid to cancel or reschedule their own seat; a full slot → seats_full; the same email twice → already_booked. Removing the last seat cancels the booking. seats 2; third booker → seats_full
BKG-17 Seated bookings show attendees to each other only if show_attendees (default false). manual: attendee email lists only themself
BKG-18 No-show: after the start of an accepted booking, the organizer can mark or unmark any attendee or the host as no-show; marking before start → refused. mark 1 min before start → refused
BKG-19 Recurring events ({freq: weekly|monthly, interval, count ≤ 24}): one booking per occurrence sharing a series_id; every occurrence must be free; cancel one or all future. Seats and recurrence can't be combined. weekly × 4 with week 3 busy → whole series refused

Out of scope

  • Instant meetings, built-in video, recording, CRM routing, SMS, workflow automations.

Decisions

  • awaiting_payment is its own status (the source used pending + unpaid, and let a host confirm an unpaid booking).
  • A recurring series is all-or-nothing at creation.

Calendars and video

Status: Draft · Prefix: CAL

Purpose

Read busy times from people's calendars, write bookings into them, and create meeting links.

Requirements

ID Requirement Verified by
CAL-1 A connection {provider, owner (user|team), secret (encrypted at rest), status: ok|invalid} per connected account. Calendar adapters: list_calendars, busy(from, to, calendars) → [{start, end}], create_event, update_event, delete_event. Video adapters: create_meeting → {url, id, password?}, update_meeting, delete_meeting. adapter contract tests per provider
CAL-2 Ship at least: Google Calendar, Microsoft 365/Outlook, CalDAV; video: Google Meet, Microsoft Teams, Zoom, plain link. manual: each connects
CAL-3 Conflict calendars: a user picks which of their calendars count as busy; per event type a different set may be chosen. A connection with none picked contributes no busy time. unpicked calendar busy → slot still offered
CAL-4 Destination calendar: one per user, optionally one per event type (overrides); none set → first connected calendar. event-type destination set → event written there
CAL-5 Each user may set a default video app; Google Meet requires Google as destination, else the default video app (or plain link) is used instead. Meet + Outlook destination → fallback used
CAL-6 OAuth tokens refresh before expiry; a refused refresh marks the connection invalid, notifies the owner, and stops using it. revoked token → status invalid, owner emailed
CAL-7 Busy query failure fails closed for that host (SLT-11); other hosts unaffected. one host's provider down → other hosts still offered
CAL-8 Busy queries pad the range by 24 h each side, then clip, so time zone edges aren't missed. event at 23:30 UTC−10 day before → seen
CAL-9 Every external event or meeting created for a booking is recorded {provider, external_id, calendar_id?, meeting_url?} and used to update or delete it later. cancel → external event deleted
CAL-10 Video creation failure: organizer is told, the booking falls back to the plain link location if one exists, else keeps no link. Zoom error → organizer email
CAL-11 Events carry an iCalendar UID stable across reschedules, with SEQUENCE = booking sequence. reschedule → same UID, sequence 1

Out of scope

  • Push sync channels, calendar caching, domain-wide delegation, CRM sync.

Decisions

  • One failing calendar blocks only its own host; the source blocked the whole query.

Event types

Status: Draft · Prefix: EVT

Purpose

A bookable meeting template: how long it is, when it can be booked, and what the booker must give. All durations are whole minutes.

Requirements

ID Requirement Verified by
EVT-1 An event type is owned by exactly one user or one team (@specfork/teams) and has title (1–200 chars), slug ([a-z0-9-], 1–64), description?, length (1–1440), hidden (default false), position (display order, ascending, ties by creation). create with length 0 → rejected
EVT-2 slug is unique per owner. second event /intro for same user → rejected; same slug for another user → ok
EVT-3 durations?: list of allowed lengths that includes length; the booker picks one. A slot query or booking with a duration not in the list (or ≠ length when no list) is rejected. durations [15,30]; book 45 min → rejected
EVT-4 slot_interval? (≥ 1): step between slot starts; default = the chosen duration. length 60, interval 30 → overlapping slots 09:00, 09:30 (SLT-6)
EVT-5 min_notice (≥ 0, default 120): no slot starts before now + min_notice. now 10:00, notice 120 → first slot ≥ 12:00
EVT-6 buffer_before, buffer_after (≥ 0, default 0): protected time around each booking of this type (SLT-9). manual: see SLT-9
EVT-7 window: unlimited (default); rolling {days, calendar_days: bool} (last bookable date = today + days calendar days, or + days Mon–Fri days); rolling_available {days} (last bookable date = the days-th date after today with ≥ 1 free slot; fewer than days such dates within 61 days → today + 61); range {start_date, end_date} inclusive in the owner's zone. rolling 5 business days from Fri → last bookable day is next Fri
EVT-8 limits.bookings {day?, week?, month?, year?} and limits.minutes {…} (total booked minutes); each set value must be ≤ every set value of a longer period. save day 5, week 3 → rejected
EVT-9 seats? (≥ 1): several bookers share one slot (BKG-16). Null = one booking per slot. manual: see BKG-16
EVT-10 requires_confirmation (default false), with optional confirm_within {value, unit: minutes|hours}: confirmation needed only when the booking starts within that time of now; otherwise auto-accepted. threshold 24 h; book 3 days ahead → accepted; book in 2 h → pending
EVT-11 pending_blocks_slot (default false): pending bookings of this type block their host's time, for every event type. pending 10:00, flag on → 10:00 not offered
EVT-12 max_active_per_booker?: a booker (by email) with that many future accepted bookings of this type is refused, and shown their next booking to reschedule. max 1; same email books twice → second refused
EVT-13 hidden events are left out of the public profile but stay bookable by direct link. hidden event → not listed; direct URL → slots shown
EVT-14 locations: ordered list of {kind: in_person|link|phone_host|phone_booker|video, value?}; the booker picks one when there are several (BKG-5). two locations → booker must choose
EVT-15 booking_fields: the booking form (BKG-3). manual: see BKG-3
EVT-16 allow_cancel, allow_reschedule (default true; restrict the booker, never the organizer); min_reschedule_notice?; cancel_reason: required_host (default), required_booker, required_both, optional. allow_cancel false → booker cancel link refused
EVT-17 private_links: secret URLs {token, expires_at?, max_uses (default 1), uses} that open a hidden event; a link past expiry or max_uses is refused. link max 1; second booking via it → refused
EVT-18 success_redirect_url?: after booking, the booker is sent there with booking details as query parameters. manual: book → redirected with uid
EVT-19 schedule?: the schedule used for availability (AVL-5). manual: see AVL-5
EVT-20 locked_time_zone? (IANA): the booking page shows times in this zone and no zone picker (PUB-2). locked Europe/Berlin → times in Berlin, no picker

Out of scope

  • Offset start times, "optimised" slot alignment, auto-translation, instant meetings, managed event types pushed to members, recording, attribute-based routing.

Decisions

  • Duration is validated on slot queries as well as on booking (the source validated only on booking).
  • Limits use the owner's week start, not a fixed Sunday.

Notifications

Status: Draft · Prefix: NTF

Purpose

Who hears about what. Emails here; webhook delivery from @specfork/webhooks. Host side = organizer + other hosts; attendee side = booker + guests.

Requirements

ID Requirement Verified by
NTF-1 Emails are sent from the instance's address with Reply-To = organizer (host-side mails: booker). manual: headers of a scheduled email
NTF-2 accepted → host: scheduled; attendees: scheduled, with .ics attachment and manage links (reschedule, cancel). book → both sides emailed, .ics valid
NTF-3 pending → host: request with confirm/reject links (BKG-11); booker: request received. Host reminders at 48 h, 24 h, 3 h after creation while still pending and before start. pending 50 h → 2 reminders sent
NTF-4 rejected → attendees: declined (with reason). cancelled → both: cancelled (with reason). rescheduled → both: rescheduled (old and new time). reschedule requested → booker: rebook link. cancel → both sides emailed
NTF-5 Seats: joining → organizer and the new attendee; cancelling a seat → organizer and that attendee only. manual
NTF-6 Payments: reminder (PAY-4), refund failed → organizer, no-show fee charged → booker. manual
NTF-7 Emails use the recipient's locale and time zone; the event type can turn off host-side or attendee-side emails. attendee in Asia/Tokyo → times in JST
NTF-8 Webhook events: booking.created, booking.requested, booking.rescheduled, booking.cancelled, booking.rejected, booking.payment_initiated, booking.paid, booking.no_show_updated, meeting.started, meeting.ended, time_off.created. each transition → one event
NTF-9 Booking event payload: {uid, status, event_type {id, slug, title}, start, end, organizer {name, email, time_zone}, attendees, location, responses, from_reschedule?, cancel_reason?, reject_reason?}. payload matches schema
NTF-10 meeting.started/meeting.ended fire at start/end of accepted bookings; cancelled or rescheduled bookings cancel their pending ones. cancel before start → no meeting.started
NTF-11 Webhook subscriptions may be scoped to a user, a team or one event type. event-type webhook → only that event's bookings

Out of scope

  • SMS, workflow builder, location-change and guest-added emails.

Payments

Status: Draft · Prefix: PAY

Purpose

Charge for a booking, refund fairly, and optionally hold a card for no-show fees.

Requirements

ID Requirement Verified by
PAY-1 An event type may have price {amount (integer minor units, > 0), currency (ISO 4217)}, payment_mode: on_booking (default) or hold (save card, charge later), and a connected payment provider. A price without a provider → can't publish. price set, no provider → save refused
PAY-2 Booking a paid event creates it awaiting_payment with a payment {uid, amount, currency, provider_ref, status: pending|succeeded|failed|refunded} and returns a checkout URL. book paid event → response has checkout URL
PAY-3 awaiting_payment bookings block the slot until payment_timeout (default 30 min); unpaid at timeout → cancelled with reason payment_timeout, slot freed. no payment in 30 min → booking cancelled, slot offered again
PAY-4 15 minutes after creation, still unpaid → reminder email to the booker with the checkout link. manual: email at +15 min
PAY-5 Provider confirms payment (signed webhook, idempotent by provider_ref) → paid = true; status → pending if confirmation is required, else accepted. duplicate provider webhook → one transition
PAY-6 refund_policy: never, always, or before {days, calendar_days: bool}. Applied when the booking is cancelled or rejected; rejection always refunds. policy 2 days, cancel 1 day before → no refund
PAY-7 A failed refund notifies the organizer; the booking stays cancelled. provider refund error → organizer email
PAY-8 Rescheduling a paid booking needs no new payment. reschedule paid booking → no checkout
PAY-9 hold mode: the organizer can charge the no-show fee after marking the booker no-show (BKG-18); optional auto-charge when the booker cancels within {value, unit} of the start. auto-charge 24 h; cancel 2 h before → charged
PAY-10 Seated paid events take one payment per seat; cancelling a seat refunds that seat per policy. cancel one seat → one refund

Out of scope

  • Coupons, taxes, invoices, multiple providers per event.

Decisions

  • Payment timeout (30 min) is new: the source had none, so unpaid bookings could block a slot forever.

Public pages

Status: Draft · Prefix: PUB

Purpose

What bookers see: profile, booking flow, confirmation, and managing a booking. No sign-in needed.

Requirements

ID Requirement Verified by
PUB-1 /{username} and /{team-slug} (one namespace; route names such as app, api, booking, login are reserved and refused as usernames or slugs): name, avatar, bio, and visible event types (title, durations, description); private teams hide members. hidden event not listed
PUB-2 /{owner}/{event-slug}: month calendar with available dates, time list for the chosen date in the booker's zone (detected, changeable unless EVT-20 locks it), 12/24 h toggle, duration picker when several. zone changed → times re-rendered
PUB-3 /{user1}+{user2}/{duration} (dynamic group): collective booking across users who allow it; any user not allowing it → 404. user disallows → 404
PUB-4 Choosing a time places the hold (SLT-15) and shows the form (BKG-3); fields can be prefilled from query parameters with the field name. ?email=a@b.c → email prefilled
PUB-5 Success page /booking/{uid}: what, when (booker zone), where, who; add-to-calendar links (Google, Outlook, .ics); status shown for pending and awaiting payment. open uid → details shown
PUB-6 The same page offers reschedule and cancel per BKG-12..BKG-15; for seats, by seat uid. cancel from page → status cancelled
PUB-7 Embeds: inline, pop-up button and floating button snippets for any event page; embedded pages post booking_successful to the parent window. manual: embed on a test page
PUB-8 Pages are server-rendered, usable without third-party cookies, WCAG 2.2 AA, and work at 360 px width. manual: axe scan, phone width
PUB-9 Users can hide the product branding and set a brand colour and light/dark theme for their pages. manual

Out of scope

  • Custom domains, SEO settings, routing forms.

Schedules

Status: Draft · Prefix: AVL

Purpose

When a person is available: weekly hours, date overrides, time off.

Requirements

ID Requirement Verified by
AVL-1 Every user has an IANA time_zone (required, chosen at signup) and week_start (monday|sunday, default monday). signup without zone → rejected
AVL-2 A schedule has name, time_zone? (falls back to owner's), weekly rules {days: [0..6] (0 = Sunday), start, end} and date overrides {date, start, end}; times are wall-clock HH:MM; weekly rules need end > start, overrides end ≥ start (AVL-7). create weekly rule with end ≤ start → rejected
AVL-3 A new user gets one schedule, Mon–Fri 09:00–17:00, as their default. The owner can make any of their schedules the default; a user always has exactly one. new user → one default schedule
AVL-4 Deleting the default schedule makes the oldest remaining one default; every reference to a deleted schedule is repointed to the default; deleting the last schedule is refused. delete only schedule → refused
AVL-5 Schedule used for a host on an event: event's schedule, else the host's per-event schedule (RR-2), else the host's default. event without schedule → host default used
AVL-6 Rules expand per local date in the schedule's zone; overlapping or touching ranges on a date merge; end 23:59 means midnight. 09–12 and 12–17 → one range 09–17
AVL-7 Any override on a date replaces all weekly rules for that date; several overrides on a date give several ranges; an override with start == end makes the date unavailable. Mon rule 09–17, override Mon 13–14 → only 13–14
AVL-8 On DST change days, ranges keep their wall-clock times; their UTC instants shift. A time skipped by the change uses the offset before it (02:30 → 03:30); a repeated one the earlier instant. Europe/Berlin, last Sun of March, 09–17 → 07:00–15:00 UTC
AVL-9 Travel periods {time_zone, start_date, end_date?} read the default schedule's rules in the travel zone for those dates. default schedule 09–17 Europe/Berlin, travel to Asia/Tokyo → 09–17 Tokyo time
AVL-10 Time off {start_date, end_date (inclusive, owner's zone), reason (free text), note?, note_public (default false), redirect_to_user?} makes those dates unavailable for booking. time off Tue → Tue has no bookable slots
AVL-11 During time off with redirect_to_user, the public page shows the dates as away with a link to that user's page; note shown only if note_public. manual: away dates show redirect
AVL-12 Optional public holidays by country, each holiday can be switched off; an enabled holiday on a working day acts as time off. country DE, Oct 3 on a Fri → no slots

Out of scope

  • Restriction schedules (intersect with a second schedule).

Decisions

  • Explicit time zone at signup; the source defaulted to Europe/London.
  • Monday week start by default (ISO 8601); used by weekly limits (EVT-8).

Slots

Status: Draft · Prefix: SLT

Purpose

Which start times a booker is offered. The same rules re-check every booking (BKG-6).

Requirements

ID Requirement Verified by
SLT-1 Query: event, from, to (ISO 8601 instants, to > from, ≤ 62 days apart; slots starting in [from, to)), booker time_zone, duration?, reschedule_uid?. Result: map booker-local YYYY-MM-DD → list of {start (ISO 8601 UTC), seats_taken?}. to < from → rejected
SLT-2 Per host: free = schedule ranges (AVL) − time off − busy times (SLT-8..11). busy 10–11 in 09–17 → 10:00 not offered
SLT-3 Hosts combine per RR-3; individual events have one host. manual: see RR-3
SLT-4 Slots start at max(range start, now + min_notice) per free range (schedule ranges minus busy time widened per SLT-9), rounded up to the alignment step: the first of 60, 30, 20, 15, 10, 5 that divides the interval, else 1; alignment is to the hour in the booker's zone. free 09:05–12:00, 60 min → 10:00, 11:00
SLT-5 A slot is offered only if start + duration ≤ range end; next start = start + interval. free 09:00–10:00, 45 min, interval 30 → 09:00 only
SLT-6 Interval < duration gives overlapping slots. 60 min, interval 30, free 09–11 → 09:00, 09:30, 10:00
SLT-7 A start offered by two ranges appears once. manual: two hosts, same start → one slot
SLT-8 Accepted bookings where the host is organiser or attendee (by account email) are busy; awaiting_payment bookings are busy until their payment timeout (PAY-3); pending bookings are busy only per EVT-11, for their host; cancelled and rejected never. cancelled 10:00 → 10:00 offered
SLT-9 Buffers: new event E (before B, after A) conflicts with booking X (before Bx, after Ax) iff [E.start − B, E.end + A] overlaps [X.start − Bx, X.end + Ax]. Buffers never shrink working hours. X 10:00–10:30 with A=0, E B=15 → 10:30 blocked, 10:45 ok
SLT-10 Busy time from connected calendars (CAL-3) is busy like a booking with no buffers of its own. external 14–15 → 14:00 not offered
SLT-11 If a host's connected calendar can't be read, that host has no availability for the query (fail closed). calendar API error → host offers nothing
SLT-12 Overlap means a.start < b.end && a.end > b.start; touching is not overlap. booking 10–11, slot 11:00 → offered
SLT-13 A limit reached (EVT-8) makes its whole period busy; periods are in the event owner's zone and week start; a booking counts in the period of its start; a slot is offered only while bookings < the booking limit and booked minutes + its duration ≤ the minute limit; booking limits count accepted bookings of the event type across all hosts; minute limits count per host. day limit 2, two bookings Tue → no Tue slots
SLT-14 Rolling windows (EVT-7) count dates in the booker's zone using each slot's own UTC offset; range dates are the owner's zone; notice is an absolute duration; for rolling windows all dates after the limit are dropped. rolling 3 calendar days from Mon → no Fri slots
SLT-15 Holds: when a booker (an anonymous browser id) selects a slot, it is held for 5 minutes, replacing their earlier hold; slots overlapping another booker's unexpired hold are neither offered nor bookable (slot_taken); a booker's own hold is ignored; holds have no buffers. A holds 10:00; B queries → no 10:00
SLT-16 Seated events: a slot at time T with an existing booking stays offered until seats_taken = seats; other slots overlapping that booking are busy. seats 3, 1 taken → T offered with seats_taken 1
SLT-17 When rescheduling, the booking being moved is not busy and doesn't count towards limits. reschedule 10:00 → 10:00 offered to that booker
SLT-18 Only slots starting in [from, to) are returned; date keys without slots are omitted. manual: query one day → one date key

Out of scope

  • Slot caching, slot troubleshooter, "only first slot" display mode.

Decisions

  • Seats: a seated booking blocks overlapping slots with other start times (the source counted bookings, not seats, and left them free).
  • Pending bookings block only their own host (the source blocked the slot for every host).
  • UTC offsets per slot date, not the current offset.

Team events

Status: Draft · Prefix: RR

Purpose

Event types owned by a team: everyone attends (collective) or one host is picked (round robin).

Requirements

ID Requirement Verified by
RR-1 A team event type has mode: collective or round_robin, and hosts [{user, fixed (bool), priority 0–4 (default 2), weight ≥ 0 (default 100), group?}]; hosts must be accepted team members (@specfork/teams). all_members (bool) makes every member a host. add non-member as host → refused
RR-2 A host may set a per-event schedule overriding their default (AVL-5). manual: see AVL-5
RR-3 Availability: collective → intersection of all hosts; round robin → intersection of fixed hosts and, for each group, the union of its non-fixed hosts. Fixed hosts with no common time → no slots. RR hosts A 9–12, B 12–17 → slots 9–17
RR-4 Collective booking: all hosts attend; the first host is organizer; every host free or hosts_unavailable. one collective host busy → no slot
RR-5 Round-robin pick, per group: among non-fixed hosts free for the slot, keep the highest priority; then, if weights are on, the hosts furthest below their weighted share of this period's bookings; then the least recently booked. A booked last, B not → B picked
RR-6 Weighted share: share = total_in_period × weight / sum_weights − own_in_period; period is the current month (default) or day, counted by booking creation time; no-shows excluded. weights 2:1, 30 bookings → 20:10 over time
RR-7 A host added mid-period starts with the average bookings per host so far, so they aren't flooded. new host on day 15 → not picked for every booking
RR-8 Reschedule keeps the same host if they're free, else picks again. host free at new time → same host
RR-9 Seats on round-robin: later seats join the first seat's host. manual: second seat same host
RR-10 Recurring round-robin: the picked host must be free for all occurrences, else the next candidate is tried. host busy in week 2 → next host
RR-11 A team admin can reassign a booking to another free host; old host, new host and attendees are notified. reassign → both hosts emailed
RR-12 Team permissions (@specfork/teams:TEAM-10 rows): event types read: member+; create, update, delete: admin+; team bookings read, reassign: admin+; webhooks read: member+, manage: admin+. A host always sees bookings they host. member edits team event → 403

Out of scope

  • Managed event types, routing forms, CRM owner routing, attribute segments, lead thresholds.

Decisions

  • Priority before weights before recency, as the source does; simpler than combined scoring.

From Specfork/scheduling at c798296, licence CC-BY-4.0.