Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesWorkout AssignmentsWorkout assignments

Workout assignments

What this is

A workout assignment is a single dated cell on a user’s calendar: “Mia is assigned Fran on 2026-06-03, slot 0, kind=workout”. Assignments are the central join between workouts, users, programs, and dates. The domain owns three publish surfaces (personal / feed / schedule), the lazy-fork lifecycle (FIT-152), per-cell kinds (workout/rest/note, FIT-159), bulk preview/delete/publish over coaching programs, morning-of publish drip, push notifications on assign, the coach overview dashboard (KPIs + per-member sparkline), and the auto-completion side-effect when a result is logged. The assignments service exposes forkSnapshotIfNeeded, which is called by both workouts edit flows and workout-results result creation.

Who uses it

PersonaRole
CoachAssigns workouts via cell click, drag, or bulk apply; runs bulk operations; views compliance overview.
OwnerSame as coach + monitors getCoachOverview.
MemberReads own assignments (my-week, today, :id); completes / skips.
Spotter agentCreates assignments via the same controllers (typically assignPersonal).

Persona impact

  • Coach sees one calendar where program-driven assignments and ad-hoc personal assignments coexist. Bulk publish flips drafts to published with one click; bulk delete reasonably scopes to enrolled members.
  • Member sees a stable per-day prescription that survives any post-assignment edit the coach makes to the library workout — the snapshot pointer freezes the prescription as of completion/edit time.
  • Owner has a single roster view (coach-overview) with traffic-light compliance and 7-day sparklines.

Capabilities

  1. Three modes of creation (FIT-74 / PRD §7.4):
    • POST /assignments/personal — assign a workout to N athletes for a date (also handles kind='rest'/'note').
    • POST /assignments/feed — publish to a feed-mode track via workout_feed_posts.
    • POST /assignments/schedule — add to class-schedule slots (creates class_sessions + class_session_workouts).
  2. Legacy single-athlete create (POST /assignments) for backwards compatibility.
  3. Read endpointsGET /assignments?weekStart&userId|programId, GET /assignments/my-week, GET /assignments/today, GET /assignments/:id (week-independent single fetch with full snapshot payload).
  4. Mutation endpointsPATCH /:id, DELETE /:id (soft), POST /:id/complete, POST /:id/skip, PATCH /publish (bulk explicit set).
  5. Bulk filter-driven operations:
    • POST /assignments/bulk-preview — count + 10 samples for a filter.
    • POST /assignments/bulk-delete — soft-delete every match, audit-logged under one batch_id.
    • POST /assignments/bulk-publish — publish every draft match.
  6. Coach overviewGET /assignments/coach-overview — org-wide KPIs + roster with 7-day sparkline + traffic-light status (on_track/at_risk/overdue/quiet).
  7. Lazy-forkforkSnapshotIfNeeded(assignmentId) is the single deep-copy entry point; called from workouts edit-from-cell and from workout-results result completion. Uses SELECT … FOR UPDATE for concurrency.
  8. Morning-of drip publishdto.drip === 'morning_of' on assignPersonal writes published=false + publish_at = date @ 05:00 UTC. A scheduled job flips published=true at that moment (TODO: verify the publisher job lives elsewhere — search for publish_at).
  9. Auto-completion — when a result is created against the assignment id, the assignment auto-transitions assigned → completed with completed_at = now().

Relationship to other features

  • workouts — assignments hold both workout_id (library) and snapshot_workout_id (per-athlete frozen copy). Workouts edit endpoints redirect writes via forkSnapshotIfNeeded.
  • workout-results — result completion forks the snapshot and auto-completes the assignment.
  • program-templatesapplyToCoaching materializes template cells as assignments in pointer state.
  • daily-programming — orthogonal: daily programming is class-type-based, assignments are user-based. A schedule-mode program populates daily_programming; coaching-mode uses assignments.
  • scheduling/bookingsassignSchedule writes class_sessions rows that feed scheduling-bookings if it exists. TODO: verify cross-feature.

Current status

Shipped. Lazy-fork (FIT-152), per-cell kind (FIT-159), bulk operations, coach-overview dashboard, and morning-of drip are all live. Bulk preview/delete/publish observed at apps/api/src/workout-assignments/workout-assignments.service.ts and tested in workout-assignments.bulk.unit.spec.ts.

Known gaps / open Linear issues

  • FIT-152 — lazy-fork. Shipped.
  • FIT-159 — per-cell kind. Shipped.
  • FIT-201 — bulk-filter scoping bug surfaced in prod. Fixed: bulk preview now intersects with programEnrollments instead of workoutAssignments.programId so ad-hoc personal rows visible on the week grid are also covered (see workout-assignments.service.ts:1165).
  • Template-apply does not push notify; ad-hoc assignPersonal does. TODO: verify whether this is intentional or a known gap.
  • Morning-of publisher job — the assignment carries publish_at, but the worker that flips published=true at that timestamp must exist elsewhere. TODO: verify the publisher service / cron.
  • Skip endpoint (POST /:id/skip) — flips status to skipped. No undo endpoint surfaced.
  • assignFeed writes only via workout_feed_posts; it does not also create per-member workout_assignments rows — feed delivery is broadcast.