Workouts
What this is
A workout is the canonical, reusable unit of programming in FitKit: a title + scoring spec + ordered list of sections, each section holding an ordered list of movements (exercise + prescription). The workouts domain owns CRUD on workouts, structured/freeform mode handling, section + movement editing, per-movement prescriptions (sets/reps/load/RPE/rest), per-movement coach comments, and the lazy-fork lifecycle that lets a coach edit “this athlete’s copy of Fran” without mutating the library template. Workouts are produced by the structured builder in the dashboard (or pasted in as freeform text) and consumed by workout-assignments, daily-programming, program-templates, feed posts, class sessions, and workout-results.
Who uses it
| Persona | Role with workouts |
|---|---|
| Owner / Admin | Builds + maintains the org’s workout library. Can edit, archive, fork, and delete. |
| Coach | Builds workouts via the structured builder (sections, supersets, prescriptions); pins comments on movements. |
| Member | Read-only consumer via the snapshot the assignment points to. Cannot edit. Can post comments on movements (per-athlete snapshot). |
| Platform | Owns the canonical exercise library that workouts pull from — see exercises. |
| Spotter agent | Creates workouts on behalf of a coach via the same API endpoints; gated by the workout_builder feature for structured mode. |
Persona impact
- Coach gets a reusable library that scales (one Fran row, used by 50 athletes via lazy-forked snapshots). Per-athlete edits do not pollute the template.
- Member sees the exact prescription the coach issued for that day — post-assignment edits to the library do not leak backwards.
- Owner can rely on the library list (
isSnapshot = false) staying clean even as athletes accumulate per-day forks.
Capabilities
- Create a workout (structured with sections+movements, or freeform with description text only).
- List the org library, filterable by
programIdand free-textq. - Fetch a single workout with full section/movement/exercise nesting.
- Replace a workout’s section tree atomically (
PUT /:id/sections). - Update a movement’s prescription, label, and superset group.
- Soft-delete a workout (snapshot rows are protected from deletion).
- Lazy-fork: when a coach edits a movement/section/prescription via a workout cell on someone’s calendar (
?assignmentId=<id>), the library row is deep-copied into a snapshot, and the edit lands on the snapshot. Comments on the original movements come along with the fork. - Pin per-movement coach comments (with image/video attachments) that members can read and reply to. Reply notifications push to the parent author.
- Background semantic-embedding enrichment via BullMQ (
workouts-enrichmentqueue) so workouts are searchable by intent (used by the spotter agent and library search). - Tier-gated structured-mode access: creating or flipping a workout to
mode='structured'requires theworkout_builderplatform feature; freeform mode is onbasic_workouts.
Relationship to other features
- exercises —
workout_movements.exerciseIdmust resolve to a canonical (organization_id IS NULL) row or this org’s own custom/forked row. - program-templates — template cells (
program_template_workouts) reference library workouts; apply() materializes them into workout-assignments,workout_feed_posts, ordaily_programming. - workout-assignments — owns the snapshot pointer + lazy-fork transition. The
forkSnapshotIfNeededflow lives in the assignments service but is called fromWorkoutsServicewhenever an edit carries?assignmentId. - workout-results — results anchor on the snapshot id; PRs anchor on the library id (=
forkedFromIdorid). - daily-programming —
daily_programmingcells point at library workouts (not snapshots); members read the snapshot via their assignment instead. - spotter agent — consumes the same
WorkoutsServiceto create workouts on a coach’s behalf; sees the same tier gate.
Current status
Shipped. The lazy-fork model is in production as of FIT-152 (mid-2025). Section shapes (linear / amrap / emom / for_time / tabata / rep_scheme / rounds / intervals) and prescription JSONB live under libs/shared/src/lib/schemas/section-shape.ts and prescription.ts. Embedding enrichment runs as a non-blocking BullMQ queue.
Known gaps / open Linear issues
- FIT-152 — Lazy-fork model. Shipped. The whole “snapshot pointer + on-edit deep copy + comments follow the fork” lifecycle.
- FIT-159 — Per-cell
kind(workout/rest/note) on assignments and template cells. Workouts service guards against forking non-workout cells. - FIT-201 — Caller bug reported in prod: frontend was holding library-row movement ids after a fork; service now maps through
(section.sortOrder, movement.sortOrder)inresolveSnapshotMovement/mapMovementToSnapshot. - Republish-from-template (drift detection from a snapshot back to its
forkedFromId) — surface exists in i18n (republishBanner) and an index (workouts_forked_from_id_idx) is in place, but the actual republish endpoint is not yet wired. TODO: verify whether FIT-XXX tracks this. - Top-level (non-reply) comment notifications: currently no recipient is derived. Reply notifications work; broader notification is deferred.