Files
mop-skills/skills/zwift-workout/references/zwo-format.md
IT Guy 109162889c feat(skills): add zwift-workout skill (.zwo builder, validator, preview, publishing)
Bike-only Zwift .zwo generator with human-readable preview + confirmation loop.

- scripts/zwo_builder.py: spec<->.zwo build/validate/parse/render/TSS estimate
- scripts/zwo.py: CLI (build, from-template, validate, parse, render, templates)
- references/zwo-format.md: condensed offline format reference
- assets/example-vo2max.zwo + tests/test_zwo.py (9 tests)
- Publishes via hosting skill; files result into Knowledge Wiki

Format ground truth: h4l/zwift-workout-file-reference
2026-09-20 07:47:05 +00:00

3.1 KiB
Raw Permalink Blame History

Zwift .zwo Format Reference (condensed)

Offline copy of the essentials from https://github.com/h4l/zwift-workout-file-reference. A .zwo file is XML; there is no official Zwift schema. This is the subset Zwift reliably accepts.

Document structure

<?xml version="1.0" encoding="UTF-8"?>
<workout_file>
  <author>IT Guy</author>
  <name>VO2 Max 5x3</name>
  <description>5 × 3 min @ 115 % FTP</description>
  <sportType>bike</sportType>
  <tags><tag name="vo2max"/></tags>
  <workout>
    <Warmup Duration="600" PowerLow="0.50" PowerHigh="0.75"/>
    <IntervalsT Repeat="5" OnDuration="180" OffDuration="180"
                OnPower="1.15" OffPower="0.55"/>
    <Cooldown Duration="300" PowerLow="0.75" PowerHigh="0.40"/>
  </workout>
</workout_file>

Conventions

  • Power is a fraction of FTP (0.50 = 50 %, 1.15 = 115 %). units="power" is the implicit default.
  • Duration is always in seconds.
  • <sportType> values seen: bike (61 %), run (37 %), swim, ride. We use bike.
  • Text cues are children of a step: <textevent timeoffset="30" message="Push!"/>.

Step elements (child of <workout>)

Element Attributes (relevant) Meaning
Warmup Duration, PowerLow, PowerHigh, Cadence linear ramp Low→High
Cooldown Duration, PowerLow, PowerHigh, Cadence linear ramp High→Low
SteadyState Duration, Power, (PowerLow,PowerHigh), Cadence constant target
SolidState Duration, Power constant power, no ERG variation
Ramp Duration, PowerLow, PowerHigh explicit ramp
IntervalsT Repeat, OnDuration, OffDuration, OnPower, OffPower, Cadence, CadenceResting, OverUnder N repeated on/off pairs
FreeRide Duration, FlatRoad ERG off, rider's choice
MaxEffort Duration all-out, ERG off
RestDay — placeholder
textevent timeoffset, message, duration on-screen cue

Top-level metadata elements (child of <workout_file>)

activitySaveName, author, author_alias, authorIcon, category, categoryIndex, description, durationType, entid, ftpOverride, ftpMaleOverride, ftpFemaleOverride, name, nameImperial, nameMetric, painIndex, setFtpAtPercentage, ShowCP20, Skippable, sportType, subcategory, tags, test_details, visibleAfterTime, visibleOutsidePlan, workout, workoutLength, WorkoutPlan.

Minimal viable set we emit: author, name, description, sportType, tags, workout.

Importing a .zwo into Zwift

  1. Copy the file into ~/Documents/Zwift/Workouts/<userID>/ (folder name is your numeric Zwift ID), or
  2. Use the Custom Workouts import on zwift.com, or
  3. Drop it into the Zwift folder sync (Dropbox/OneDrive) if configured.

Then restart Zwift; the workout appears under Custom Workouts.

Gotchas

  • Malformed XML or unescaped &/quotes → Zwift silently ignores the file.
  • Power outside ~0.20–1.50 FTP is almost always a unit mistake.
  • Duplicate/unknown attributes are tolerated but ignored; keep it minimal.