Primitive spec

Riose

The workforce primitive. Riose owns shift scheduling, time & attendance, timesheets, and payroll-ready settlement math — and its marquee move is that published shifts project availability into Trantor, so staffing decides what customers can actually book through Seldon.

Note: Payroll execution awaits a partner-of-record — the driver seam is real, but no money moves yet; CSV export of approved hours by earning code is the universal fallback for any payroll provider. Everything upstream of that — scheduling, time clock, approved timesheets, compensation — is live.

What it owns

Riose owns the operational side of employing people: the Shift a person works, the TimeEntry that proves they worked it, the Timesheet that turns punches into priced minutes, and the PayrollRun that hands approved hours to a payroll provider. It is where a gym publishes next week's front-desk and trainer schedule, staff claim open shifts and swap with each other, and a manager approves a period's overtime math before payroll.

Riose does not own identity or employment — who works here lives in Terminus as a staff group, and Riose consumes that roster by opaque personId. It does not own the bookable resource — a bookable stylist or instructor stays a Trantor resource; Riose projects shift windows onto it. And it is not a tax authority: gross-to-net, filings, and money movement belong to the embedded payroll partner behind the driver seam.

Concepts

Position
A schedulable job — "Front Desk", "Head Trainer" — with a roleKey into the staff role catalog, an optional default hourly rate, and a bookable flag. Bookable positions carry a trantorResourceTypeId: the resource type this position staffs, which is what makes the shift-to-availability projection possible.
Shift
One block of scheduled work: positionId, nullable personId (null = open shift), UTC startsAt/endsAt plus a location timezone snapshot, a breakPlan, and a lifecycle of draft → published → (claimed) → completed | cancelled. The overlap guard is a hard rule: two schedule-occupying shifts for the same person return a 409 with the conflict listed in the problem detail — on create, assign, claim, and swap alike.
SchedulePattern
A recurring weekly template — rules of day-of-week, start/end time, position, headcount, optional person. POST /schedule-patterns/{id}/apply materializes a week of draft shifts (local times to UTC, overnight-aware); rules that would violate the overlap guard are skipped and reported rather than failing the whole week.
ShiftSwapRequest
Staff-to-staff reassignment with a paper trail: pending → accepted → approved (or auto-finalized when the policy sets requiresApproval=false), with declined and cancelled terminal. Approval atomically reassigns Shift.personId through the same overlap guard and re-runs the availability projection. One active swap per shift.
TimeOffRequest & TimeOffBalance
Requests by kind (pto, sick, unpaid, other) with an approve/deny/cancel flow. Approval debits a kind-keyed running TimeOffBalance in minutes (cancel restores it), using the same 480-minutes-per-day rule the timesheet uses, so requested days and paid hours never disagree. Insufficient balance is advisory, not blocking; approved paid time off flows into the timesheet as ptoMin.
AvailabilityPreference & scheduling advisories
Per-person weekly availability windows and an optional maxHoursPerWeek, effective-dated. Deliberately advisory: GET /scheduling/advisories returns non-blocking warnings for a proposed person + window — time_off overlap, outside_availability (timezone-aware), over_max_hours, and booking_conflict when reassigning away from a shift that already has customer bookings inside it.
TimeEntry
A punch pair: clockInAt, nullable clockOutAt, breaks, a source (kiosk, console, mobile, auto_close), and an append-only editLog — timesheet disputes live and die on it. Kiosk punches identify staff by a hashed personal KioskPin. A sweep auto-closes entries left open too long and flags them into a needs_review queue; managers correct settled punches via PATCH /time-entries/{id}, with overlap and locked-timesheet guards returning 409s.
Timesheet & TimesheetCalculator
One row per person per pay period, with snapshot totals: regularMin, otMin, dblOtMin, ptoMin, unpaidBreakMin, estimatedCostCents. The pure TimesheetCalculator does the money math — per-punch rounding, then daily double-OT, daily OT, and weekly OT per the tenant's policy (FLSA weekly by default, daily fields cover CA) — and the TimesheetPricer prices minutes at effective-dated rates, computing the FLSA weighted-average blended rate when a person works multiple positions at different rates. Lifecycle: open → submitted → approved → exported → paid; exported sheets are immutable — corrections are next-period adjustments.
CompensationProfile & PositionRateOverride
Append-only, effective-dated compensation: hourly or salary, a new row per change, history never mutated. Per-position rates are first-class PositionRateOverride rows (person × position × effective date), so a base-rate change never silently drops an override. Compensation is the sensitive surface — endpoints sit behind an explicit riose.comp permission and audit events redact amounts.
PayrollProfile, PayPeriod & PayrollRun
The PayrollProfile (one per tenant) holds frequency (weekly, biweekly, semimonthly), an anchor date, the OT policy, and rounding rules. PayPeriod rows are materialized ahead on that grid; a period refuses to close while punches are unsettled. A PayrollRun is created from a closed period's approved sheets and submitted through PayrollProviderDriverInterface — the seam a partner-of-record driver drops into. Worker PII never touches LatticeKit; it belongs to partner-hosted components.
Shift → availability projection
The marquee. Publishing a shift for a bookable position upserts a ResourceAvailabilityWindow on the person's Trantor resource, keyed to the shift; reassignment moves it, cancellation deletes it. Trantor's allocator only grants holds inside a window (resources with no windows keep always-available semantics), so a lesson offer shows zero slots on the instructor's day off — with no Riose-to-Seldon coupling at all. A reverse guard protects customers: cancelling or reassigning a staffed shift with bookings inside its window returns a 409 listing the impacted bookings, and an explicit force=true override raises a health signal. A rolling ShiftWindowReconciler repairs any drift in both directions.

API surface

Endpoints are versioned under /riose/v1/ and return RFC 7807 problem details on error. Scheduling, clock, timesheet, compensation, and payroll surfaces all ship; kiosk PIN management and pay-period materialization ride the same prefix.

Riose endpoints in the Foundation API reference OpenAPI 3.1 schema for Riose with request/response shapes, parameters, and a try-it client.

Quick reference

MethodPathPurpose
POST / GET/riose/v1/positionsDefine or list schedulable jobs; bookable positions name their Trantor resource type.
POST / GET/riose/v1/shiftsCreate a shift (overlap guard 409s on conflict) or list by person and window.
POST/riose/v1/shifts/publishBatch publish — explicit shiftIds or a window ≤ 35 days; per-item results, one digest per person via Speaker.
POST/riose/v1/shifts/{id}/claimStaff self-claim an open published shift; /assign, /release, /cancel are siblings, each with the booking-conflict reverse guard.
POST/riose/v1/shifts/{shiftId}/swap-requestsOpen a swap; /swap-requests/{id}/accept, /approve, /decline, /cancel drive the lifecycle.
POST/riose/v1/schedule-patterns/{id}/applyMaterialize a week of draft shifts from a recurring weekly template.
GET/riose/v1/scheduling/advisoriesNon-blocking warnings for a proposed person + window: time off, availability, max hours, booking conflicts.
POST / GET/riose/v1/time-off-requestsRequest and approve/deny/cancel time off; balances live at /time-off-balances.
POST/riose/v1/time-clock/inPunch in; /out, /break-start, /break-end, /status complete the clock. Kiosk punches identify via /kiosk/identify.
GET / PATCH/riose/v1/time-entriesList punch pairs; manager edits with a mandatory per-field editLog, overlap and locked-timesheet guards.
POST / GET/riose/v1/timesheetsGenerate or list persisted timesheets; /{id}/submit, /approve, /reopen, /export. Ad-hoc math via GET /timesheets/compute.
POST / GET/riose/v1/compensation-profilesAppend-only effective-dated compensation; per-position overrides at /position-rates. Permission-gated.
POST / GET/riose/v1/payroll-runsCreate a run from a closed period's approved sheets; /{id}/submit drives the payroll driver, /{id}/export.csv emits hours by earning code. Profile at /payroll-profile, periods at /pay-periods.

Example: schedule a shift, then check the period's math

A racquet club schedules an evening front-desk shift with an unpaid meal break. If the person already has an overlapping shift, this 409s with the conflict; otherwise it lands as a draft:

POST /riose/v1/shifts
Content-Type: application/json
Authorization: Bearer <token>

{
  "positionId": "rpos01J9K4…",
  "personId":   "prsn01J8W2…",
  "startsAt":   "2026-07-21T15:00:00Z",
  "endsAt":     "2026-07-21T23:30:00Z",
  "timezone":   "America/Chicago",
  "breakPlan":  [{ "offsetMin": 240, "durationMin": 30, "paid": false }],
  "notes":      "Evening league check-in"
}

HTTP/1.1 201 Created

{
  "id":              "shft01JAB7…",
  "positionId":      "rpos01J9K4…",
  "personId":        "prsn01J8W2…",
  "startsAt":        "2026-07-21T15:00:00+00:00",
  "endsAt":          "2026-07-21T23:30:00+00:00",
  "timezone":        "America/Chicago",
  "status":          "draft",
  "source":          "manual",
  "publishedAt":     null,
  "durationMinutes": 510,
  …
}

POST /riose/v1/shifts/shft01JAB7…/publish flips it to published, notifies the person through Speaker, and — for a bookable position — projects the availability window into Trantor. Later, before approving the week's timesheet, a manager sanity-checks the overtime math for any person and window:

GET /riose/v1/timesheets/compute?personId=prsn01J8W2…
    &start=2026-07-20T00:00:00Z&end=2026-07-27T00:00:00Z
Authorization: Bearer <token>

{
  "personId":       "prsn01J8W2…",
  "periodStart":    "2026-07-20T00:00:00+00:00",
  "periodEnd":      "2026-07-27T00:00:00+00:00",
  "regularMin":     2400,
  "otMin":          150,
  "dblOtMin":       0,
  "ptoMin":         480,
  "unpaidBreakMin": 150,
  "paidMin":        3030
}

The same totals are snapshotted onto the persisted Timesheet at generate and approve time, priced through the person's CompensationProfile and any PositionRateOverride rows into estimatedCostCents.

How it fits with the rest

flowchart TB
  Dz[Demerzel console] --> Ri(Riose)
  Ri -- shift windows --> Tr[Trantor]
  Ri -- digests + reminders --> Sp[Speaker]
  Ri -- approved timesheets --> Drv[[Payroll driver seam]]
  Ri --> OB[(Audit outbox)]
  Tr -- window-aware allocation --> Se[Seldon booking]
  OB --> Pe[Pelorat]
            

The staffing-to-bookability chain runs through Trantor, never a direct Riose-to-Seldon contract: publishing a shift projects a ResourceAvailabilityWindow onto the person's resource, and Seldon's allocator simply stops offering slots outside staffed hours. Speaker delivers one publish digest per person (push when a device is registered, else SMS, consent fail-closed) and per-shift shift.starting reminders scheduled on the same emission pipeline Seldon uses. Terminus owns the roster Riose schedules against; Daneel steps auto-publish next week's schedule and close pay periods on cron; Demerzel hosts the schedule grid, kiosk, timesheet, and payroll consoles; and every punch, publish, and approval rides the audit outbox that Pelorat reports off. Payroll execution sits behind PayrollProviderDriverInterface — the Mock driver today, a partner-of-record driver when the partnership signs, with no product-surface change in between.