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.
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
roleKeyinto the staff role catalog, an optional default hourly rate, and abookableflag. Bookable positions carry atrantorResourceTypeId: the resource type this position staffs, which is what makes the shift-to-availability projection possible. - Shift
- One block of scheduled work:
positionId, nullablepersonId(null = open shift), UTCstartsAt/endsAtplus a locationtimezonesnapshot, abreakPlan, and a lifecycle ofdraft → 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}/applymaterializes 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 setsrequiresApproval=false), withdeclinedandcancelledterminal. Approval atomically reassignsShift.personIdthrough 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 runningTimeOffBalancein 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 asptoMin. - AvailabilityPreference & scheduling advisories
- Per-person weekly availability windows and an optional
maxHoursPerWeek, effective-dated. Deliberately advisory:GET /scheduling/advisoriesreturns non-blocking warnings for a proposed person + window —time_offoverlap,outside_availability(timezone-aware),over_max_hours, andbooking_conflictwhen reassigning away from a shift that already has customer bookings inside it. - TimeEntry
- A punch pair:
clockInAt, nullableclockOutAt, breaks, asource(kiosk,console,mobile,auto_close), and an append-onlyeditLog— timesheet disputes live and die on it. Kiosk punches identify staff by a hashed personalKioskPin. A sweep auto-closes entries left open too long and flags them into aneeds_reviewqueue; managers correct settled punches viaPATCH /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 pureTimesheetCalculatordoes 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 theTimesheetPricerprices 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:
hourlyorsalary, a new row per change, history never mutated. Per-position rates are first-classPositionRateOverriderows (person × position × effective date), so a base-rate change never silently drops an override. Compensation is the sensitive surface — endpoints sit behind an explicitriose.comppermission 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.PayPeriodrows are materialized ahead on that grid; a period refuses to close while punches are unsettled. APayrollRunis created from a closed period's approved sheets and submitted throughPayrollProviderDriverInterface— 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
ResourceAvailabilityWindowon 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 explicitforce=trueoverride raises a health signal. A rollingShiftWindowReconcilerrepairs 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.
Quick reference
| Method | Path | Purpose |
|---|---|---|
| POST / GET | /riose/v1/positions | Define or list schedulable jobs; bookable positions name their Trantor resource type. |
| POST / GET | /riose/v1/shifts | Create a shift (overlap guard 409s on conflict) or list by person and window. |
| POST | /riose/v1/shifts/publish | Batch publish — explicit shiftIds or a window ≤ 35 days; per-item results, one digest per person via Speaker. |
| POST | /riose/v1/shifts/{id}/claim | Staff self-claim an open published shift; /assign, /release, /cancel are siblings, each with the booking-conflict reverse guard. |
| POST | /riose/v1/shifts/{shiftId}/swap-requests | Open a swap; /swap-requests/{id}/accept, /approve, /decline, /cancel drive the lifecycle. |
| POST | /riose/v1/schedule-patterns/{id}/apply | Materialize a week of draft shifts from a recurring weekly template. |
| GET | /riose/v1/scheduling/advisories | Non-blocking warnings for a proposed person + window: time off, availability, max hours, booking conflicts. |
| POST / GET | /riose/v1/time-off-requests | Request and approve/deny/cancel time off; balances live at /time-off-balances. |
| POST | /riose/v1/time-clock/in | Punch in; /out, /break-start, /break-end, /status complete the clock. Kiosk punches identify via /kiosk/identify. |
| GET / PATCH | /riose/v1/time-entries | List punch pairs; manager edits with a mandatory per-field editLog, overlap and locked-timesheet guards. |
| POST / GET | /riose/v1/timesheets | Generate or list persisted timesheets; /{id}/submit, /approve, /reopen, /export. Ad-hoc math via GET /timesheets/compute. |
| POST / GET | /riose/v1/compensation-profiles | Append-only effective-dated compensation; per-position overrides at /position-rates. Permission-gated. |
| POST / GET | /riose/v1/payroll-runs | Create 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.