com.bookbaltic.services.staff_choice
Status: Draft. Version 2026-11-03. Part of BookBaltic Scheduling Extensions for USP / UCP Local Services.
Extends: dev.usp-protocol.services.availability, dev.usp-protocol.services.bookings
Schema: https://bookbaltic.com/schemas/usp/staff_choice.json
What it adds: "Any staff" as a first-class choice, pinned vs preferred staff, what happens when the staff member drops out
1. Staff selection
Base USP already does: list staff (resources[].options), filter by resource_id, book by slot. Gaps: (i) with N therapists free at 15:00, USP requires N slots; an agent that does not care must deduplicate, and the merchant cannot keep assignment flexible; (ii) no way to say whether the guest insists on that person; (iii) no defined behaviour when the assigned staff member becomes unavailable.
Related: discussion #1022 lists what a small salon sells: a service, a time slot and "often a specific professional (or anyone available)"; staff_mode: "any" is our concrete shape for it and is an extension of USP 4.1 (one slot per resource combination). The goods side already has the same pattern: #896 lets the buyer agree ahead of time what happens on a surprise after the order (otherwise: cancel | contact_buyer), and #944 defines how a business asks for a fresh buyer decision after the order, with a decision_deadline. A general post-order consultation protocol stays with #944; awaiting_buyer is one domain case of it. staff_preference and staff_change are the services counterpart; if #896 and #944 land, staff_preference can be expressed as a services contingency and awaiting_buyer as a #944 consultation with the same deadline. The fields stay on the booking so free bookings without checkout (#922 Phase 2) work too. Against #922: staff_mode is input to Phase 1 availability, staff_preference and staff_change to Phase 2 post-booking.
Not live: HEADSPA does not publish staff, so the extension is specified but not declared in its profile.
Schema
{
"$id": "https://bookbaltic.com/schemas/usp/staff_choice.json",
"requires": { "protocol": { "min": "2026-08-20" },
"capabilities": { "dev.usp-protocol.services.availability": { "min": "2026-08-20" },
"dev.usp-protocol.services.bookings": { "min": "2026-08-20" } } },
"$defs": {
"AvailabilityQueryExt": { "type": "object", "properties": {
"staff_mode": { "enum": ["any", "per_staff"], "default": "per_staff",
"description": "any: one slot per start, staff assigned by the merchant. per_staff: base USP behaviour." } } },
"SlotExt": { "type": "object", "properties": {
"staff_assignment": { "enum": ["assigned", "any"] },
"eligible_staff_count": { "type": "integer", "minimum": 1,
"description": "Optional. How many staff could take this start; never their identities." } } },
"BookingRequestExt": { "type": "object", "properties": {
"staff_preference": { "enum": ["required", "preferred", "none"], "default": "preferred" } } },
"BookingExt": { "type": "object", "properties": {
"staff_preference": { "enum": ["required", "preferred", "none"] },
"staff_change": { "type": "object", "required": ["changed_at", "outcome"], "properties": {
"changed_at": { "type": "string", "format": "date-time" },
"outcome": { "enum": ["reassigned", "awaiting_buyer", "canceled_by_business"] },
"previous": { "$ref": "#/$defs/StaffRef" },
"current": { "$ref": "#/$defs/StaffRef" },
"decision_deadline": { "type": "string", "format": "date-time",
"description": "As UCP #944: the last instant a buyer decision is admitted. Required when outcome is awaiting_buyer." } },
"if": { "properties": { "outcome": { "const": "awaiting_buyer" } } },
"then": { "required": ["decision_deadline"] } } } },
"StaffRef": { "type": "object", "required": ["id", "name"], "additionalProperties": false,
"properties": { "id": { "type": "string" }, "name": { "type": "string", "description": "Display name only" } } }
}
}
Rules
staff_mode: "any"returns at most one slot per start;resourceson that slot omits staff; the booking gets staff on confirmation or at service time.eligible_staff_countis optional and a count, never a list; a business that does not want to expose it omits it.- On staff dropout the merchant MUST apply the booking's
staff_preference: noneorpreferred: reassign and sendbooking.rescheduledwithstaff_change.outcome: "reassigned".required: setoutcome: "awaiting_buyer"withdecision_deadlineand offer the other person, a reschedule, or free cancellation. As in #944, silence MUST NOT be read as approval: no answer bydecision_deadlinemeans free cancellation with a full refund, never reassignment.- No one can take the visit:
canceled_by_business, full refund. - A dropout MUST NOT move the start time; a time change is a normal reschedule under the booking's policy (#922 Phase 2).
- As in #896, the platform MUST show
staff_preferenceto the guest before booking and MUST NOT send a value the guest has not seen. The defaultpreferredis what most guests mean when they name a person without saying more. - The agent MUST tell the guest the outcome: who will serve them, or what they must decide and by when.
canceledfollows the UCP spelling (checkout.json,lodging/booking.json).
Example (HEADSPA, illustrative staff; not declared in its profile)
Request:
{ "service_id": "svc-aqua-head-spa", "start_date": "2026-11-07", "end_date": "2026-11-07",
"party_size": 1, "com.bookbaltic.services.staff_choice": { "staff_mode": "any" } }
Response slot:
{ "id": "slot_20261107_1400_any", "service_id": "svc-aqua-head-spa",
"start": "2026-11-07T14:00:00+02:00", "end": "2026-11-07T15:30:00+02:00", "duration": "PT90M",
"state": "available", "resources": [{ "id": "room-1", "type": "room", "name": "Ritual room" }],
"com.bookbaltic.services.staff_choice": { "staff_assignment": "any", "eligible_staff_count": 2 } }
Booking after the pinned therapist dropped out:
{ "com.bookbaltic.services.staff_choice": {
"staff_preference": "required",
"staff_change": { "changed_at": "2026-11-06T09:12:00+02:00", "outcome": "awaiting_buyer",
"previous": { "id": "staff-anna", "name": "Anna" },
"current": { "id": "staff-liga", "name": "Liga" },
"decision_deadline": "2026-11-06T20:00:00+02:00" } } }
Without the extension: the agent gets base per-staff slots and books one; it cannot express "any" or "required", and a dropout arrives as an unexplained booking.rescheduled.
Open questions: should staff_change become a new USP webhook booking.resource_changed rather than ride on booking.rescheduled? Should per-staff price differences use slot.pricing only (we assume yes)?