Workout Schema

Last updated: 2026-05-29

Your Trainer is a multi-rider indoor cycling app for Android tablets. Smart-trainer control with local data + local control. One-time purchase.

The .ytw workout file is plain JSON — author it in any text editor, import via the share sheet, share with friends. The visual editor handles common cases; the schema is the escape hatch when you need full control.

When to hand-author

Most riders never need this page — the visual workout editor and the AI Workout Coach cover everything from a 4×8 threshold session to a microburst stack. Reach for the schema when:

Minimal example

The shortest valid .ytw file is one program with one interval. Save with a .ytw extension and share into Your Trainer.

{
  "programId": "my-sweet-spot",
  "programName": "My Sweet Spot 30",
  "description": "A short sweet-spot workout.",
  "totalDuration": 1800,
  "workoutType": "POWER",
  "primaryLocale": "en",
  "intervals": [
    { "id": "warmup",   "duration": 300,  "targetPowerPercent": 50, "intensityZone": "Z1", "label": "Warmup",     "intervalType": "WARMUP"   },
    { "id": "work",     "duration": 1200, "targetPowerPercent": 88, "intensityZone": "Z3", "label": "Sweet Spot", "intervalType": "INTERVAL" },
    { "id": "cooldown", "duration": 300,  "targetPowerPercent": 50, "intensityZone": "Z1", "label": "Cooldown",   "intervalType": "COOLDOWN" }
  ]
}

Top-level fields

The top-level object describes one workout program. Required fields are flagged.

FieldTypeDescription
programId requiredstringStable identifier. Use kebab-case (my-sweet-spot). The unique key for the workout in your library — re-importing a file with the same programId updates the existing entry rather than creating a duplicate.
programName requiredstringDisplay name in the rider's primary locale.
description requiredstringOne- or two-sentence description shown on the workout card.
totalDuration requiredinteger (seconds)Total workout length. The app recomputes this from the intervals on save, so it's safe to leave inconsistent during authoring.
intervals requiredarrayOrdered list of interval objects or repeat groups.
workoutTypestringWorkout family: POWER (default), HR_ZONE, or ROUTE. See Workout types.
variantstringSub-shape inside the family: STANDARD (default) or RAMP_TEST.
primaryLocalestring (BCP-47)Locale the strings were authored in. Default "en". Drives the cross-locale fallback chain.
categorystringFree-text categorisation (e.g. "threshold", "endurance"). Optional.
difficultyinteger (1–5)Subjective difficulty. Surfaces on the workout card.
isUserCreatedbooleanTrue for workouts authored in-app by the rider; false for imported / bundled workouts. Default false.
isFavoritebooleanPinned-to-top flag. Riders flip this in-app; usually omitted in shared files.
routeProfilearray of { distanceMeters, elevationMeters }Full elevation profile for ROUTE workouts. Null for POWER and HR_ZONE.
stringsobject (locale → LocaleStrings)Per-locale translations of name, description, interval labels, and cues.

Interval fields

Each interval object describes one block of the workout. The block's shape depends on the parent's workoutType — Power blocks use power percentages, HR-Zone blocks use a zone target.

FieldTypeDescription
duration requiredinteger (seconds)Block length in seconds.
targetPowerPercent power-requiredinteger (% of FTP)Power target for POWER workouts. Mutually exclusive with targetHrZone.
targetPowerEndPercentinteger (% of FTP)Optional ramp end-power. When present, the target wattage interpolates linearly from targetPowerPercenttargetPowerEndPercent across the block.
targetHrZone hr-requiredinteger (1–5)HR zone for HR_ZONE workouts. Mutually exclusive with targetPowerPercent.
intensityZone requiredstringVisual zone token: Z1Z5. Drives the colour on the terrain visualization. See Training zones.
intervalTypestringWARMUP, COOLDOWN, or INTERVAL (default). Warm-up and cooldown blocks are excluded from work-only summaries (average-power-of-work-blocks, time-in-zone for the work portion, etc.).
label requiredstringDisplay text on the block in the rider's primary locale. Cross-locale variants live in strings.<locale>.labels.
idstringStable slug — the key used in strings.<locale>.labels and in cue-key composition. Recommended for any workout that ships with translations.
autoLabelbooleanTrue when the label was generated by an editor preset rather than typed by the rider. Auto-labels are localised by Your Trainer itself and don't need per-locale entries in strings. Default false.
cadenceTargetinteger (RPM)Optional cadence target for the block (e.g. 60 for low-cadence climbing, 100 for spin-up drills).
cuesarray of CoachingCueCoaching cues that fire during the block.

Coaching cues

A coaching cue is a short text overlay that appears on the cockpit during a ride. Each cue has an offset within its parent interval, the text to show, and how long to leave it on screen.

FieldTypeDescription
offsetSec requiredinteger (seconds)Seconds from the start of the parent interval at which the cue fires.
text requiredstringCue text in the workout's primary locale. Cross-locale variants live in strings.<locale>.cues, keyed by <intervalId>:<cueIndex>.
durationSecinteger (seconds)How long the cue stays on screen. Default 5.

Example interval with three cues (key composition uses the parent interval's id + the cue's index in the array):

{
  "id": "work",
  "duration": 600,
  "targetPowerPercent": 95,
  "intensityZone": "Z4",
  "label": "Threshold",
  "intervalType": "INTERVAL",
  "cues": [
    { "offsetSec": 0,   "text": "Settle in — find your rhythm." },
    { "offsetSec": 300, "text": "Halfway. Stay smooth.",  "durationSec": 8 },
    { "offsetSec": 540, "text": "One minute. Hold form." }
  ]
}

Localized strings & fallback chain

The strings block carries per-locale translations of every rider-visible string in the workout. Each locale entry has the same shape:

"strings": {
  "en": {
    "name": "Sweet Spot 30",
    "description": "A short sweet-spot workout.",
    "labels": { "warmup": "Warmup", "work": "Sweet Spot", "cooldown": "Cooldown" },
    "cues":   { "work:0": "Settle in", "work:1": "Halfway" }
  },
  "de": {
    "name": "Sweet Spot 30",
    "description": "Ein kurzes Sweet-Spot-Training.",
    "labels": { "warmup": "Aufwärmen", "work": "Sweet Spot", "cooldown": "Ausrollen" },
    "cues":   { "work:0": "Locker einrollen", "work:1": "Halbzeit" }
  }
}

Cue keys follow the pattern <intervalId>:<cueIndex> — so the first cue on the work interval is keyed work:0.

For each rider-visible string, the app picks the best locale match in this order:

  1. strings[<rider-locale>] — the rider's own locale.
  2. strings[primaryLocale] — the author's locale.
  3. strings["en"] — universal fallback.
  4. The top-level field (programName, interval label, cue text).

Strings shown from any locale other than the rider's own appear italicised on workout cards and the cockpit, so the rider can tell which strings haven't been translated yet.

See also: AI prompt skills — the AI Coach can generate strings blocks for additional locales when prompted.

Repeat groups

For repetitive structures, a repeat group authors the unit once and tells the app how many times to play it. Repeat groups are expanded into individual blocks on import, so the rider sees each block in the upcoming-intervals strip during the ride.

{
  "intervals": [
    { "id": "warmup", "duration": 600, "targetPowerPercent": 50, "intensityZone": "Z1", "label": "Warmup", "intervalType": "WARMUP" },
    {
      "repeat": 4,
      "intervals": [
        { "id": "on",  "duration": 480, "targetPowerPercent": 95, "intensityZone": "Z4", "label": "Threshold" },
        { "id": "off", "duration": 240, "targetPowerPercent": 55, "intensityZone": "Z1", "label": "Recovery" }
      ]
    },
    { "id": "cooldown", "duration": 600, "targetPowerPercent": 50, "intensityZone": "Z1", "label": "Cooldown", "intervalType": "COOLDOWN" }
  ]
}

The example above expands to 1 warm-up + 4×(threshold + recovery) + 1 cooldown = 10 blocks. Repeat groups can nest, but flat is preferable for readability.

Workout types & variants

The workoutType field selects the paradigm; variant selects a sub-shape inside it.

workoutTypeWhat it isRequired interval shape
POWER (default)FTP-anchored intervals. The trainer rides target wattage in ERG, or follows resistance curves in SIM.Each interval has targetPowerPercent (and optionally targetPowerEndPercent for ramps).
HR_ZONEHeart-rate-driven intervals. The trainer adjusts wattage live to keep the rider's HR in the target zone — useful when cardiovascular load is the training metric (recovery, base, polarised work).Each interval has targetHrZone (1–5). The START button reads LINK HRM until the HRM is connected.
ROUTESlope-driven simulation rides. The trainer follows the elevation profile from routeProfile; the rider chooses cadence and gearing.Intervals are usually empty or contain a single full-length placeholder. The actual ride content lives in routeProfile.

The variant field selects a sub-shape:

Packs (.ytwpack)

When you want a curated batch of workouts in one go, the .ytwpack format bundles many .ytw files together with a per-pack manifest. A .ytwpack is a ZIP archive under the hood — rename to .zip and unzip to see the contents, or import it via Your Trainer's pack installer to get every workout installed in one tap.

A .ytwpack archive holds:

The pack manifest carries enough info for an install-decision sheet without needing to open every workout. Top-level fields:

FieldTypeDescription
schema_version requiredintegerCurrently always 1.
pack_id requiredstringStable kebab-case identifier (e.g. sweet-spot).
name requiredstringDisplay name shown in the pack catalog.
description requiredstringOne- or two-sentence summary visible before the rider taps to install.
version requiredstring (SemVer)MAJOR.MINOR.PATCH, optional -prerelease. Patch for content fixes; minor for additive workouts; major for breaking schema changes. Embedded in the published filename (v1.0.2.ytwpack).
content_hash requiredstringsha256: over the sorted-by-slug concatenation of every .ytw file's bytes. Stable across regenerations of unchanged content; bumps every time the workouts inside change.
generated_at requiredstring (ISO 8601)UTC timestamp.
set requiredstringpower or hr-zone — the workout family this pack belongs to.
category requiredstringSub-taxonomy inside the set (e.g. sweet-spot).
workout_count requiredintegerNumber of .ytw entries in the pack.
total_ride_time_seconds requiredintegerSum of the durations of every workout in the pack.
experience_level requiredstringComputed from the contents' difficulty range — one of beginner / intermediate / advanced / mixed. Lowercase wire format; the app capitalises for display.
hrm_required requiredbooleanTrue if any workout in the pack uses HR_ZONE.
type_mix requiredobjectPer-category percentage of total ride time (sums to 100). Drives the in-app type-mix donut on the install sheet.
duration_histogram requiredobjectWorkout count per duration bin: 0-30, 30-60, 60-90, 90+ (minutes). Drives the install sheet's duration chart.
contents requiredarrayFull per-workout entries — superset of the library-manifest entry shape; each carries slug, name, duration_seconds, summary metrics, plus a sparkline array for thumbnail rendering.

Two install paths for a downloaded .ytwpack:

Sample pack manifest (truncated contents for readability):

{
  "schema_version": 1,
  "pack_id": "sweet-spot",
  "name": "Sweet Spot",
  "description": "26 sweet-spot sessions across classic intervals, sustained stacks, and over-unders.",
  "version": "1.0.2",
  "content_hash": "sha256:9432a3a76015158dc71ec63…",
  "generated_at": "2025-05-28T00:00:00Z",
  "set": "power",
  "category": "sweet-spot",
  "workout_count": 26,
  "total_ride_time_seconds": 119340,
  "experience_level": "intermediate",
  "hrm_required": false,
  "type_mix":   { "sweet-spot": 100 },
  "duration_histogram": { "0-30": 0, "30-60": 5, "60-90": 12, "90+": 9 },
  "contents": [
    { "slug": "sweet-spot-3x10min-at-88pct-ftp",  "name": "Sweet Spot 3×10min @ 88% FTP",  "duration_seconds": 3300, "tss": 55.8,  "intensity_factor": 0.764, "sparkline": […] },
    { "slug": "sweet-spot-3x15min-at-90pct-ftp",  "name": "Sweet Spot 3×15min @ 90% FTP",  "duration_seconds": 4500, "tss": 81.6,  "intensity_factor": 0.808, "sparkline": […] }
    /* … 24 more workouts … */
  ]
}

Library & pack manifests

Two manifests are published alongside the downloadable artefacts. Both are plain JSON; both are described by JSON Schema documents you can fetch directly.

URLWhat it listsJSON Schema
library/manifest.json Every curated .ytw in the library — per-workout metadata for browse / search / filter clients. Also lists the available .ytwpack downloads (file path, version, content hash, type-mix summary, icon URL). /schemas/workout-manifest.json
packs/manifest.json Pack-catalog endpoint: every published .ytwpack with summary metadata. Same per-pack entry shape as the library manifest's packs array; the in-app Pack Library fetches this on rider-initiated refresh. /schemas/workout-manifest.json
(inside each .ytwpack) Per-pack manifest carried as manifest.json at the archive root — the table above documents its shape. /schemas/workout-pack-manifest.json

If you're building tooling that consumes the library — a custom workout browser, a workout-converter that targets .zwo, a coach's dashboard that surfaces packs — these are the contracts to validate against. The same per-workout entry shape appears in both manifests' contents / workouts arrays, so a client that handles one handles the other.

Worked examples

Ramp interval

A 5-minute warm-up that ramps from 40 % FTP to 75 % FTP via linear interpolation:

{
  "id": "rampup",
  "duration": 300,
  "targetPowerPercent": 40,
  "targetPowerEndPercent": 75,
  "intensityZone": "Z1",
  "label": "Ramp up",
  "intervalType": "WARMUP"
}

Over-under

Three sets of 2 minutes at 95 % FTP / 1 minute at 105 % FTP, expressed as a repeat group:

{
  "repeat": 3,
  "intervals": [
    { "id": "under", "duration": 120, "targetPowerPercent": 95,  "intensityZone": "Z4", "label": "Under" },
    { "id": "over",  "duration": 60,  "targetPowerPercent": 105, "intensityZone": "Z5", "label": "Over"  }
  ]
}

HR-Zone workout

30-minute Z2 endurance ride with a 3-minute Z4 surge in the middle:

{
  "programId": "hr-z2-with-surge",
  "programName": "Z2 with a Z4 surge",
  "description": "Steady Zone 2 with a single 3-minute Zone 4 surge.",
  "totalDuration": 1800,
  "workoutType": "HR_ZONE",
  "primaryLocale": "en",
  "intervals": [
    { "id": "warmup",  "duration": 300,  "targetHrZone": 1, "intensityZone": "Z1", "label": "Warmup", "intervalType": "WARMUP" },
    { "id": "endure1", "duration": 600,  "targetHrZone": 2, "intensityZone": "Z2", "label": "Endurance" },
    { "id": "surge",   "duration": 180,  "targetHrZone": 4, "intensityZone": "Z4", "label": "Surge" },
    { "id": "endure2", "duration": 420,  "targetHrZone": 2, "intensityZone": "Z2", "label": "Endurance" },
    { "id": "cooldown","duration": 300,  "targetHrZone": 1, "intensityZone": "Z1", "label": "Cooldown", "intervalType": "COOLDOWN" }
  ]
}

Multilingual

The minimal sweet-spot example with EN + DE + NL strings blocks. Same workout, three native experiences:

{
  "programId": "my-sweet-spot",
  "programName": "Sweet Spot 30",
  "description": "A short sweet-spot workout.",
  "totalDuration": 1800,
  "workoutType": "POWER",
  "primaryLocale": "en",
  "intervals": [
    { "id": "warmup",   "duration": 300,  "targetPowerPercent": 50, "intensityZone": "Z1", "label": "Warmup",     "intervalType": "WARMUP"   },
    { "id": "work",     "duration": 1200, "targetPowerPercent": 88, "intensityZone": "Z3", "label": "Sweet Spot", "intervalType": "INTERVAL",
      "cues": [
        { "offsetSec": 0,   "text": "Settle in" },
        { "offsetSec": 600, "text": "Halfway" }
      ]
    },
    { "id": "cooldown", "duration": 300,  "targetPowerPercent": 50, "intensityZone": "Z1", "label": "Cooldown",   "intervalType": "COOLDOWN" }
  ],
  "strings": {
    "en": {
      "name": "Sweet Spot 30",
      "description": "A short sweet-spot workout.",
      "labels": { "warmup": "Warmup", "work": "Sweet Spot", "cooldown": "Cooldown" },
      "cues":   { "work:0": "Settle in", "work:1": "Halfway" }
    },
    "de": {
      "name": "Sweet Spot 30",
      "description": "Ein kurzes Sweet-Spot-Training.",
      "labels": { "warmup": "Aufwärmen", "work": "Sweet Spot", "cooldown": "Ausrollen" },
      "cues":   { "work:0": "Locker einrollen", "work:1": "Halbzeit" }
    },
    "nl": {
      "name": "Sweet Spot 30",
      "description": "Een korte sweet-spot-training.",
      "labels": { "warmup": "Inrijden", "work": "Sweet Spot", "cooldown": "Uitrijden" },
      "cues":   { "work:0": "Rustig inrijden", "work:1": "Halverwege" }
    }
  }
}

Common pitfalls

Reference

For programmatic format-spec lookups (field tables, examples, constraints, glossary) the canonical machine-readable source is the Your Trainer MCP — call get_format_spec, get_canonical_examples, get_format_constraints, or get_format_glossary from any MCP client. These tools serve from the same knowledge registry that drives this page; if a tool's answer ever drifts from this page, the MCP is canonical and this page is stale.

JSON Schemas (machine-readable contracts for clients building on these formats):

← Back to Manual & Guides