Unreleased documentation. These pages follow the main branch and can change before the next release. For supported guidance, use v0.43.0.
If you are writing the policy a Registry Scheduling deployment will serve, you edit one project
directory and prove it offline. You continue in ./scheduling, the project from
Prove your first booking policy, where the fixture already
closes station-2.
Read the opening hours
Section titled “Read the opening hours”An offering is bookable only inside an opening: the weekly
hours one location publishes. check listed two under effective.openings. This is the one that
serves registry-update-30:
- id: bangkok-counter-hours location: bangkok-counter holidaySet: office-holidays weekdays: [mon, tue, wed, thu, fri] startTime: "09:00" endTime: "12:30" effectiveFrom: "2026-10-01" effectiveUntil: "2026-12-31"Times are wall-clock times at the location, read in the time zone its location record names, so
the counter opens at 09:00 Bangkok time on weekdays. The opening ends on effectiveUntil; from
2027 this offering publishes no starts until you extend that date and deploy the change.
Close a public holiday
Section titled “Close a public holiday”Every opening names a holiday set, and a date in that set closes the opening for the whole day. The starter’s set closes nothing yet. Close Tuesday 13 October 2026:
- id: office-holidays revision: 1 dates: - "2026-10-13" because: Public holidays observed by the registry offices.Prove it with a case at the end of the counter fixture. A booking at 09:00 that day must be refused:
- name: the-holiday-is-not-served request: offering: registry-update-30 start: 2026-10-13T02:00:00Z party: recipients: 1 attendees: 1 duplicateKey: subject:four policyRevision: 1 expect: type: refused code: schedule.unpublishedschedulingctl test ./scheduling --format json{ "detail": "refused schedule.unpublished (detailed: schedule.unpublished)", "name": "the-holiday-is-not-served", "status": "pass"}schedule.unpublished is the answer for any start the policy does not publish, whether the day
is a holiday, a weekend, or outside the opening hours.
A closure that affects one location for part of a day is not policy. It is an exception record in
records.yaml, which the operator applies to a running deployment without a new package:
exceptions: - id: hall-prep location: new-york-hall kind: closure date: "2026-11-01" startTime: "00:15" endTime: "00:45"Read the hold and reminder rules
Section titled “Read the hold and reminder rules”A caller first takes a hold on a start, then confirms it as an appointment. The hold policy applies to every offering:
holdPolicy: ttlMinutes: 5 maximumPerCaller: 3 because: Holds are short because counter capacity is scarce.An unconfirmed hold returns its station to the pool after five minutes, and one caller holds at most three at a time. The offering adds its own rules:
cancellationCutoffMinutes: 240reminders: - offsetMinutes: 1440 because: One reminder the day before a counter update. - offsetMinutes: 120 because: A second reminder two hours before, when stations are scarce.duplicateActiveKey: subjectA caller may not cancel within four hours of the start. Each confirmed appointment records two
reminder intents, due one day and two hours before it; the runtime delivers them to the reminder
destination the operator configures, or keeps them readable in its outbox when there is none.
duplicateActiveKey: subject is the rule the fixture’s second case proved: one active booking per
subject.
Every entry carries because, a sentence for the next person who reads the policy. It is part of
the policy, so a changed reason changes the digest too.
Tell another system when an appointment is confirmed
Section titled “Tell another system when an appointment is confirmed”An observer hook sends a signed event after an
appointment is confirmed, rescheduled, or cancelled. The policy names the trigger and the fields it
sends; it holds no address and no secret. Add one at the end of scheduling.yaml:
hooks: - id: appointment-confirmed phase: after trigger: appointment.confirmed projection: - appointmentId - start handler: type: url destinationId: case-systemschedulingctl check ./schedulingerror[scheduling.runtime.unbound-hook-destination] ./scheduling/runtime.example.yaml:<position> /destinations/hooks destinations.hooks must bind every destination scheduling.yaml hooks[].handler.destinationId namescheck reads runtime.example.yaml as the runtime reads its own file, and refuses a hook with
nowhere to deliver. The address and signing key belong to the deployment. Bind them there:
destinations: {}destinations: hooks: case-system: url: https://cases.example.test/scheduling-events hmacSha256KeyRef: secret:file/case-system-hmacschedulingctl check ./schedulingAuthoring check passed.The projection is closed per trigger. A confirmed or rescheduled appointment may send
appointmentId, revision, offering, start, end, state, and policyRevision; a
cancellation sends only appointmentId, revision, and state. check refuses any other field
with scheduling.project.unsupported-hook-projection, so a hook never sends the station, the
caller, or the cancellation reason. A cancellation intent sent to the reminder destination does
carry the reason. Delivery happens after the appointment commits,
at least once, signed with HMAC-SHA256, and the receiver’s answer never changes a booking.
Plan the package
Section titled “Plan the package”The runtime serves a package, not the project directory. Ask what the package would hold:
schedulingctl package ./scheduling --dry-run --format json{ "dryRun": true, "runtimeConfigurationIncluded": false, "secretsIncluded": false}The report lists one file, scheduling.yaml, and a packageDigest the runtime verifies at
startup. records.yaml and the runtime file stay out: the operator applies records to the
database and keeps the runtime file with the deployment.
When callers arrive in a window
Section titled “When callers arrive in a window”The standalone-arrival-window template sells arrival windows instead of exact times: a caller
books a place in a two-hour block, and the block’s capacity is counted in units. Create it beside
your project and explain it:
schedulingctl init ./arrival --template standalone-arrival-windowschedulingctl explain ./arrival --format json{ "arrival": { "horizonDays": 45, "leadTimeMinutes": 60, "window": "household-morning-window" }, "id": "household-morning", "mode": "arrival-window"}The window itself is a record in ./arrival/records.yaml, with its start, end, and units. A
subquotas entry caps how many of those units one channel, public or assisted, may take. A
subquota is a ceiling, not a reservation: the other channel may still use units the first leaves
free.
Configuration reference
Section titled “Configuration reference”The Registry Scheduling configuration reference lists
every key of scheduling.yaml, records.yaml, the fixtures, and the runtime file.
Deploy and operate Registry Scheduling packages this project, activates it on PostgreSQL, applies its records, and starts the runtime.