Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesProgram TemplatesProgram templates

Program templates

What this is

A program template is a reusable, multi-week programming structure — a grid of (week, day, slot) → workout | rest | note cells, tagged with a deliveryMode (coaching or schedule). A coach designs the structure once (“Hyrox 8-week block, Mon/Wed/Fri at 7am”) and then applies it to a target: a set of athletes (coaching) or a class type’s calendar (schedule). Apply expands cells to dates and materializes into the appropriate target table — workout-assignments or class_sessions + daily-programming. FIT-74 is the originating issue.

Who uses it

PersonaRole
Owner / CoachAuthors templates, runs preview, applies to athletes / programs / class types.
MemberIndirect — sees materialized assignments/sessions, never the template itself.
Spotter agentTODO: verify whether agent currently authors templates or only triggers apply.

Persona impact

  • Coach stops re-creating the same 8-week block by hand for every new cohort. One author surface (the template builder), three apply targets.
  • Owner gets one canonical source for “how we program Hyrox blocks” so the offering is consistent across coaches.
  • Member sees consistently-published workouts/sessions; the template indirection is invisible.

Capabilities

  1. CRUD a template header (name, description, deliveryMode, durationWeeks, tags[], isActive).
  2. Schedule-mode recurrence bound at creation as slots[] — one per class type, each with independent (day, time) occurrences, optional locationId / coachMembershipId.
  3. Replace the entire cell grid in one call (POST /:id/workouts with cells[]).
  4. POST /:id/duplicate — full clone, name suffixed (copy).
  5. POST /:id/preview — return what apply would produce, with no DB mutation.
  6. POST /:id/apply — materialize to the target mode in one transaction.
  7. POST /from-history — derive a template from already-materialized rows (save-as-template); for schedule sources, the slot’s occurrences[] are derived from the existing class sessions.
  8. Two apply converters (applyToCoaching, applyToSchedule), both honouring conflictMode (skip | overwrite | abort). Schedule conflicts are same-class-type time overlaps; overwrite never replaces a session that has bookings.

Relationship to other features

  • workoutsprogram_template_workouts.workoutId references the library workout. Cells with kind='rest' | 'note' carry no workout pointer.
  • workout-assignmentsapplyToCoaching is the bulk path that inserts workout_assignments rows (one per cell × athlete). Lazy-fork pointer state on insert.
  • daily-programming — no longer written by apply. Since FIT-250 applyToSchedule is pure recurrence (class_sessions only); workouts attach per class-type-per-day through the daily board.

Current status

Shipped. Header CRUD, cells upsert, preview, apply across all three modes, duplicate, and from-history are all live (apps/api/src/program-templates/program-templates.service.ts, ~1119 lines). deliveryMode='course' is explicitly excluded from program_templates_delivery_mode_chk.

Known gaps / open Linear issues

  • FIT-74 — Founding spec. Shipped.
  • applyToFeed skips kind='note' cells (“notes deferred to v2”). kind='rest' is silently skipped (no feed concept of rest day).
  • applyToCoaching does not push notifications on bulk insert; per-athlete pushes happen via the regular assignment flow (e.g. assignPersonal does push; applyToCoaching does not). TODO: verify whether this is intentional.
  • applyToCoaching conflict modes are skip (default), abort, overwrite. Overwrite soft-deletes the existing conflicting row; results referencing it survive via FK.
  • applyToSchedule always writes sessions in draft status — they must be published manually from the schedule.
  • from-history for schedule mode derives recurrence in UTC weekday/time — does not currently translate through org.timezone. TODO: verify and add ticket if confirmed.