Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesWorkoutsWorkouts

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

PersonaRole with workouts
Owner / AdminBuilds + maintains the org’s workout library. Can edit, archive, fork, and delete.
CoachBuilds workouts via the structured builder (sections, supersets, prescriptions); pins comments on movements.
MemberRead-only consumer via the snapshot the assignment points to. Cannot edit. Can post comments on movements (per-athlete snapshot).
PlatformOwns the canonical exercise library that workouts pull from — see exercises.
Spotter agentCreates 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

  1. Create a workout (structured with sections+movements, or freeform with description text only).
  2. List the org library, filterable by programId and free-text q.
  3. Fetch a single workout with full section/movement/exercise nesting.
  4. Replace a workout’s section tree atomically (PUT /:id/sections).
  5. Update a movement’s prescription, label, and superset group.
  6. Soft-delete a workout (snapshot rows are protected from deletion).
  7. 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.
  8. Pin per-movement coach comments (with image/video attachments) that members can read and reply to. Reply notifications push to the parent author.
  9. Background semantic-embedding enrichment via BullMQ (workouts-enrichment queue) so workouts are searchable by intent (used by the spotter agent and library search).
  10. Tier-gated structured-mode access: creating or flipping a workout to mode='structured' requires the workout_builder platform feature; freeform mode is on basic_workouts.

Relationship to other features

  • exercisesworkout_movements.exerciseId must 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, or daily_programming.
  • workout-assignments — owns the snapshot pointer + lazy-fork transition. The forkSnapshotIfNeeded flow lives in the assignments service but is called from WorkoutsService whenever an edit carries ?assignmentId.
  • workout-results — results anchor on the snapshot id; PRs anchor on the library id (= forkedFromId or id).
  • daily-programmingdaily_programming cells point at library workouts (not snapshots); members read the snapshot via their assignment instead.
  • spotter agent — consumes the same WorkoutsService to 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) in resolveSnapshotMovement / 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.