Primitive spec

Seldon

A universal scheduling primitive that handles appointments, stays, events, queues, multi-stage flows, and open-capacity classes through one model. Offers, availability projections, bookings, and lifecycle emission — not just "slots on a grid".

What it owns

Seldon owns the question "when can this happen, who is doing it, and what state is it in?". It models the bookable concept, projects when it's available, records the customer's claim on it, and drives that claim through a status lifecycle — emitting events at every lifecycle anchor so other primitives can react.

Seldon does not own physical resources (that is Trantor), people (Terminus), money (Payments / Mallow), notifications (Speaker), or workflow orchestration (Daneel). It coordinates them by reference.

Concepts

Offer
The thing a customer buys, books, or claims. Polymorphic across kind (appointment, stay, event, queue, staged_flow, open_capacity) and durationShape (fixed, set, range, nightcount, queue, undefined). Carries resource requirements (refs into Trantor), party capacity, and policy fields like cancelDeadlineSec and modifyRevalidates.
Availability
A projection of when an Offer can be booked. One Offer can have several Availability projections — a hotel room on a daily-occupancy grid and in a no-show queue. Five kinds: grid, range, queue, open_capacity, external.
Time scheme
The grid backing model. Templated and instance-parameterised, with verticals (lanes, sides, rooms) and horizontals (courses, courts) as arbitrary dimensions. Generates SlotInstance rows that grid availability projects.
Booking
A customer's claim on an Offer at a specific Availability position. Anchors are deliberately polymorphic: a grid booking's anchor is a slot id; a stay's is a start/end pair; a queue booking's is a position. Carries Trantor resource holds, Terminus identity refs, and a pinned snapshot of the Radiant policy version at booking time.
Lifecycle
Statuses move pending → confirmed → checked_in → live → completed, with cancelled, no_show, expired, and waitlisted branches. cancelDeadlineSec and modifyRevalidates policies gate cancel and modify operations. A lifecycle auto-advancer subscribes to the outbox so booking.starting and booking.ending events promote status without operator intervention.
Emission pipeline
Bookings fire scheduled events at each lifecycle anchor (checkInOpenAt, noShowAt, startedAt, …). A four-worker pipeline backed by a Valkey ZSET (Loader, Watcher, Meerkat, Runner) delivers them in deterministic order. Daneel hook triggers wire side effects to those emissions.
Waitlist
Modelled as a Booking with status=waitlisted, not a separate table. When capacity opens, the Promotion service transitions the highest-priority waitlisted booking to confirmed.

API surface

All endpoints are versioned under /seldon/v1/, read tenantId from the bearer token, and return RFC 7807 problem details on error. Booking write paths use Idempotency-Key to make retries safe.

MethodPathPurpose
POST / GET/seldon/v1/offersCreate or list bookable Offers.
GET / PUT / DELETE/seldon/v1/offers/{id}Fetch, replace, or soft-delete an Offer.
GET/seldon/v1/availabilityQuery Availability projections for an Offer over a time window.
POST/seldon/v1/bookingsCreate a Booking. Holds Trantor resources, checks Terminus entitlement, pins the Radiant policy version.
GET/seldon/v1/bookings/{id}Fetch a Booking with its full lifecycle state.
PATCH/seldon/v1/bookings/{id}Modify a Booking. Reschedules swap Trantor holds and resync emissions. Gated by cancelDeadlineSec + modifyRevalidates.
POST/seldon/v1/bookings/{id}/cancelCancel a Booking. Releases Trantor holds, fires booking.cancelled.
POST / GET / DELETE/seldon/v1/time-schemesManage the grid-backing time scheme templates and instances.

Example: a tee-time Offer

An Offer with a fixed 4-hour duration that requires one tee-time slot and optionally one cart:

POST /seldon/v1/offers
Content-Type: application/json
Authorization: Bearer <token>
Idempotency-Key: a3f7b1c2-…

{
  "name": "18-hole round",
  "kind": "appointment",
  "durationShape": { "mode": "fixed", "fixedSec": 14400 },
  "partyCapacity": { "min": 1, "max": 4, "default": 4 },
  "requirements": [
    { "resourceTypeId": "rt_teetimeSlot", "count": 1 },
    { "resourceTypeId": "rt_cart",        "count": 1, "optional": true }
  ],
  "radiantAssetId": "asset_01J8K2…",
  "cancelDeadlineSec": 86400,
  "modifyRevalidates": true,
  "isActive": true
}

A Booking against a grid Availability projection:

POST /seldon/v1/bookings
Content-Type: application/json
Authorization: Bearer <token>
Idempotency-Key: 9c2e4d1a-…

{
  "offerId":        "offer_01J9TQ…",
  "availabilityId": "avlb_01J9TR…",
  "anchor": {
    "slotInstanceId":  "si_01JAZB…",
    "dimensionValues": { "side": "front" }
  },
  "party": {
    "count": 4,
    "members": [
      { "terminusPersonId": "per_01JAB1…", "role": "organizer" }
    ]
  },
  "requirements": [
    { "resourceTypeId": "rt_cart", "count": 2 }
  ]
}

How it fits with the rest

flowchart LR
  Client[Client] --> S(Seldon Booking)
  R[Radiant] -. policy YAML .-> S
  T[Trantor] -. hold resources .-> S
  Te[Terminus] -. entitlement .-> S
  S --> OB[(Audit outbox)]
  OB --> D[Daneel]
  OB --> Sp[Speaker]
  OB --> Pv[Palver]
  Sp -. consent gate .-> Te
            

Seldon is the busiest hub on the platform. Offers carry pointers into Radiant for their pricing and cancellation YAML. Bookings ask Trantor to hold physical resources atomically and release them on cancel or no-show. Bookings reference Terminus for the booker's identity and pass through Terminus entitlement checks for tier-gated access rules. The lifecycle emission pipeline feeds Daneel, which in turn calls Speaker for outbound confirmations and reminders — with Speaker enforcing the Terminus consent gate before any dispatch. Mallow records the financial side of completed Bookings.

None of those references are foreign keys at the database level. The cross-primitive contract is opaque id in, validated reference confirmed, structured event out.