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

Exercises

See Movement lookup and alias review for exact matching, local MCP aliases, and canonical collision review.

What this is

The exercises domain owns the movement library that workouts pull from. It is a two-tier system: a global canonical library of ~600 movements (rows with organization_id IS NULL) that platform admins seed and enrich, plus per-org customs and forks (organization_id = <org>). Editing a canonical exercise creates or reuses a private organization copy. Legacy JSONB overrides remain readable for existing references. A hybrid lexical + semantic search service (/exercises/search) fuses three signals — full-text, pg_trgm similarity, vector cosine on a 1024-dim Voyage embedding — using Reciprocal Rank Fusion. Platform admins drive seeding and alias enrichment via BullMQ jobs.

Who uses it

PersonaRole
Platform adminSeeds canonical movements from R2 JSON; enqueues Hebrew/Russian alias enrichment via Anthropic Haiku.
Owner / CoachBuilds the org’s view: Create private forks of canonical rows, create org-customs, delete customs. Consumes the library inside the workout builder.
MemberReads the merged effective view through workouts they’re assigned (exercise name, cues, video).
Spotter agentSearches via /exercises/search to resolve a workout’s movements before persisting.

Persona impact

  • Coach sees one merged list: canonical rows pre-loaded with their cues/videos/translations, plus their org’s customs at the top.
  • Owner can localize a canonical movement (Hebrew name, swap video, tweak primary muscles) by creating a private copy; later canonical updates do not overwrite the copy.
  • Spotter agent can semantically resolve “barbell hip thrust” against the library even when the gym uses “glute bridge with bar” as the canonical name.

Capabilities

  1. List org-only exercises (GET /exercises, legacy).
  2. List the merged effective library (GET /exercises/library) — paginated canonical + custom + override-merged, with filters: q, category, movementPattern, equipment[], discipline[], difficulty, source (all/canonical/custom/customized/mine — mine is the movements list’s “My gym’s” chip: org-custom rows plus canonical rows this org has customized).
  3. Get a single exercise (GET /exercises/:id) — accepts canonical or org-owned ids.
  4. Get a single effective exercise (GET /exercises/library/:id) — merged shape.
  5. Create org-custom (POST /exercises), update (PATCH /exercises/:id), soft-delete (DELETE /exercises/:id).
  6. Upsert org-custom by case-insensitive name (POST /exercises/upsert).
  7. Create/reuse a private fork of a canonical row (PUT /exercises/:id/override); reset legacy overrides with DELETE /exercises/:id/override.
  8. Hybrid search (GET /exercises/search) — lexical/semantic/hybrid mode with RRF.
  9. Platform-admin: enqueue canonical seed jobs (POST /admin/exercises/canonical/seed) and alias enrichment (POST /admin/exercises/canonical/enrich); inspect jobs (GET /admin/exercises/canonical/jobs).
  10. Platform-admin: enqueue video discovery (POST /admin/exercises/canonical/video/discover, YouTube search → appends video_candidates), video ranking (POST /admin/exercises/canonical/video/rank, supports dryRun) or both chained (POST /admin/exercises/canonical/video/refresh), and cues generation (POST /admin/exercises/canonical/cues, supports rebuild/dryRun); verify (POST /admin/exercises/canonical/:id/video/verify) or demote (POST /admin/exercises/canonical/:id/video/demote) a canonical video.
  11. Any org member: report a bad demo video (POST /organizations/:orgId/exercises/:id/video/report) — auto-demotes once negative reports cross a threshold.
  12. Platform-admin: find canonical duplicates (POST /admin/exercises/canonical/duplicates, always dry run — reports candidate pairs by alias/name match or embedding similarity) and merge approved pairs (POST /admin/exercises/canonical/merge, { pairs, dryRun } — repoints every referencing table, soft-deletes the loser, folds its name/aliases into the winner). See behavior.md → Duplicate detection and merge.

Thumbnail generation

Owned, visually-unified thumbnails (soft-3D house style) are generated via fal.ai/FLUX on the exercise-thumbnail Bull queue and stored in a dedicated public R2 bucket (R2_THUMBNAILS_BUCKET, images only — the canonical JSON bucket stays private); thumbnailUrl holds the public URL (rendered directly by clients).

  • Canonical backfill (platform-admin): POST /admin/exercises/canonical/thumbnails ({ force?, limit? }) enqueues generation for canonical rows missing an owned thumbnail. ControlNet-Canny preserves pose from a clean reference; flux/dev is the no-reference fallback.
  • Custom movements: creating a custom exercise auto-enqueues a thumbnail. Coaches can replace it with their own image — PUT /organizations/:orgId/exercises/:id/thumbnail (multipart file, png/jpeg/webp ≤5 MB, org-owned only).
  • Requires FAL_KEY + R2_THUMBNAILS_BUCKET + R2_PUBLIC_BASE; no-ops cleanly when unset.
  • Full prod procedure: runbook → Movements library update.

Relationship to other features

  • workouts — workout_movements.exerciseId must resolve to canonical (organization_id IS NULL) or this org’s customs. Validated on both create and setSections.
  • workout-results — workout_set_results.exerciseId and personal_records.exerciseId reference exercises. Per-movement PR detection runs against the exerciseId.
  • Spotter agent — /exercises/search is the primary movement-resolution endpoint. Search results carry a source flag (canonical / org / customized) so the agent can prefer org rows.

Current status

Shipped. M0 (canonical library) is live with ~600 movements seeded from a curated JSON in R2. Slug-based dedup, HNSW vector index, pg_trgm name index, and tsvector full-text index are in place. Override system shipped. The video moderation loop (TKN-318) is shipped: a platform-admin ranking pass scores video_candidates and picks/clears videoUrl, a cues pass fills/normalizes coaching content, and verify/demote (admin) + report (any org member, auto-demote at a vote threshold) cover the review loop. See behavior.md → Video moderation loop. Duplicate detection and merge (TKN-328) is shipped: slug dedup at seed time can’t catch two canonical rows naming the same movement under different slugs (e.g. “Pull-Up” / “Strict Pull-Up”), so a platform-admin dry-run report plus an explicit merge endpoint find and fold these — no schema change, the merge trail lives in the winner’s aliases. See behavior.md → Duplicate detection and merge.

Known gaps / open Linear issues

  • Exercise org fork — canonical customization and local alias writes create/reuse a private copy through the existing override/MCP endpoints. The response carries the new ID and forkedFromId. Existing drafts adopt the fork; published and logged work and legacy overrides remain unchanged. See ownership.
  • Aliases enrichment — Haiku enrichment runs in canonical-enrich.service.ts for Hebrew + Russian; canonical seeds happen via canonical-seed.processor.ts. Jobs require platform-admin role.
  • Video/cues moderation mobile UI — the movements detail sheet (web, TKN-317) covers “Replace video” and “Report bad video” for coaches; a native/mobile surface is a separate follow-up issue. Admin verify/demote stay platform-admin-only.
  • Search relevance tuning ongoing — apps/api/src/exercises/exercise-search.int.spec.ts covers RRF fusion; relevance-evaluation work likely tracked elsewhere.
  • No merged_into_id column (TKN-328). The merge trail is recorded by folding the loser’s name/aliases into the winner’s aliases and soft-deleting the loser, so read-side resolution of a merged loser depends on that alias trail existing — a row deleted before this feature shipped, or merged some other way, still 404s. If alias-trail resolution proves too fragile in practice, a dedicated column is the next step.