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, feed, 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), a feed track (feed), or a class type’s calendar (schedule). Apply expands cells to dates and materializes into the appropriate target table — workout-assignments, workout_feed_posts, 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-only fields bound at creation: targetClassTypeId, daysOfWeek[], startTimes[], optional defaultLocationId, defaultCoachMembershipId.
  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, recurrence (daysOfWeek + startTimes) is derived from the existing class sessions.
  8. Three apply converters (applyToCoaching, applyToFeed, applyToSchedule) with mode-specific conflict handling and target-validation rules.

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-programmingapplyToSchedule writes both class_sessions (recurrence × duration) and daily_programming (one row per workout cell that lands on an active day).
  • Workout feed posts (workout_feed_posts)applyToFeed writes one row per workout cell with publishAt = local-date @ publishTimeOfDay in the org’s timezone.

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. The daily_programming rows are also draft. TODO: verify whether a single publish action covers both, or two separate publishes are needed.
  • 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.