Files
mop-skills/skills/zwift-workout/references/zwo-format.md
T
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

74 lines
3.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
<?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.