Zwo format
Use when creating, editing, or reviewing .zwo Zwift workout files — power ramp direction (Warmup/Cooldown), IntervalsT cadence attributes and repeat structure, ERG mode behavior for sprint/max-effort blocks (MaxEffort, FreeRide, power caps), textevent timing, or workout naming limits.From its SKILL.md
npx -y skills add tomjelen/zwift-workout-skills --skill zwo-formatAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- 29 days oldThe repository was created 29 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
8.8 KB, ~2.2k tokens by cl100k_base, as published. Nobody here has run it
ZWO File Format
Rules for the Zwift .zwo XML format. These come from testing against Zwift and community documentation — not from official docs (there are none).
XML structure
<workout_file>
<author>Author Name</author>
<name>Short Name (max ~35 chars)</name>
<activitySaveName>Descriptive post-ride save name</activitySaveName>
<description>Full workout description.</description>
<sportType>bike</sportType>
<tags/>
<workout>
<Warmup Duration="900" PowerLow="0.40" PowerHigh="0.75">
<textevent timeoffset="30" duration="10" message="Text shown during warmup."/>
</Warmup>
<SteadyState Duration="300" Power="0.88" Cadence="55">
<textevent timeoffset="0" message="Text shown during steady state."/>
</SteadyState>
<IntervalsT Repeat="4" OnDuration="240" OffDuration="240" OnPower="0.90" OffPower="0.50" Cadence="55" CadenceResting="85">
<textevent timeoffset="10" message="Text shown during each interval cycle."/>
</IntervalsT>
<FreeRide Duration="30" FlatRoad="1">
<textevent timeoffset="0" message="GO. Maximal effort — ERG is off, just gear up."/>
</FreeRide>
<Cooldown Duration="600" PowerLow="0.50" PowerHigh="0.40">
<textevent timeoffset="30" message="Text shown during cooldown."/>
</Cooldown>
</workout>
</workout_file>
Power targets
All power values can have fractions of FTP: 0.88 = 88% FTP.
Max effort blocks — use FreeRide
For all-out sprint blocks where the rider should go as hard as possible, use FreeRide:
<FreeRide Duration="30" FlatRoad="1">
<textevent timeoffset="0" message="GO. Maximal effort." />
</FreeRide>
- Do not use
MaxEfforton an ERG-mode setup. The community reference describesMaxEffortas "an interval without ERG mode", and on a trainer without ERG it may behave exactly as described (unverified). Under ERG, however, empirical testing (2026-06-14, one smart-trainer setup) found ERG stayed engaged — producing the cadence/resistance "spiral of death" during the sprint. Whether that is a Zwift bug or intended is unknown; treatMaxEffortand ERG mode as incompatible. Since most smart-trainer riders run workouts in ERG mode,FreeRideis the safe default.MaxEffortis also barely-supported: the reference documents only aDurationattribute and no child elements, so it can't carry text events during the effort. FreeRideis the native block that automatically turns ERG off for its duration and back on afterward, with no rider intervention. It supports nested text events and aCadenceattribute.FlatRoad="1"gives flat, controlled resistance the rider gears against, instead of following the in-game gradient.Cadenceis optional (the example above omits it): set it when the workout intends a specific sprint cadence — it drives the HUD target — or omit it to leave cadence free. Either can be right; it depends on the workout's purpose.- Drop
Powerentirely — a free-ride block has no target.
Power continuity (critical — this is the most common mistake)
PowerLow is always the start of a ramp and PowerHigh is always the end, regardless of block type. The names Low/High refer to start/end position, NOT to magnitude.
- Warmup ramps UP:
PowerLow(start, low) <PowerHigh(end, high).PowerHighshould match the power of the next block for a smooth transition.- Example: if followed by
IntervalsT OnPower="0.80", thenPowerHigh="0.80".
- Example: if followed by
- Cooldown ramps DOWN:
PowerLow(start, high) >PowerHigh(end, low).PowerLowshould match the power of the previous block. Yes, its counter-intuitive, PowerLow means start, which means the HIGHER power value.- If preceded by
IntervalsT, the block ends onOffPower, soPowerLow=OffPower. - Exception — cooldown straight after a work block. When a workout ends its interval set on the final work block with no recovery before the cooldown (see "
IntervalsTalways ends on a recovery" below), keepPowerLowat the easy/recovery level (e.g.0.50), not the work power. A cooldown is meant to start easy; the downward step from the work interval into the cooldown is intended — the mirror of a warmup not ramping all the way into a hard block.
- If preceded by
Verify after every edit:
- Warmup:
PowerLow < PowerHigh - Cooldown:
PowerLow > PowerHigh - Adjacent blocks have matching power at their shared boundary — except a deliberate step down into a recovery or cooldown block (a cooldown after a work interval starts at the easy ~0.50 level, not the work power).
Cadence attributes
Cadence="X"(capital C) is the only working cadence attribute. Shows target on HUD, warns at +/-5 rpm.cadenceHighandcadenceLowhave no effect. Do not use them.- A cadence range can't be expressed — the attribute takes a single value, so a workout designed around a range (e.g. 50-60 rpm) has to pick one; the midpoint is the usual choice.
IntervalsT cadence — requires both attributes
IntervalsT has two cadence attributes that map to the two phases:
Cadence— applied duringOnDuration(the work interval).CadenceResting— applied duringOffDuration(the recovery interval).
Always specify both. If only Cadence is set (no CadenceResting), Zwift exhibits a bug: it applies Cadence+5 during OnDuration and Cadence-5 during OffDuration. This is not documented anywhere — it was found through testing.
If you need one of the phases to have free/unspecified cadence, you cannot do that with IntervalsT. Use repeated <SteadyState> blocks instead.
IntervalsT always ends on a recovery
IntervalsT Repeat="N" expands to N work phases and N recovery phases — the block ends on an OffDuration recovery, never on work. Two consequences:
- A
Cooldownthat follows picks up fromOffPower(see power continuity above). - Whether an interval set should end on that recovery or go from the last work phase straight into the cooldown is a workout-design choice, not a format rule. If the design wants to end on work,
IntervalsTalone can't express it: useRepeat="N-1"plus a standalone final work block, or build the set from repeated<SteadyState>blocks — and note the total duration shifts by oneOffDurationeither way.
Naming
<name>— max ~30-35 characters. Longer names cause visual artifacts in Zwift's UI.<activitySaveName>— always include. This is the name saved to your activity history after the ride.
Text events
<textevent timeoffset="X" message="..."/>—timeoffsetis seconds from the start of the containing block.durationattribute (seconds) — controls how long the message is displayed. If omitted, Zwift uses a default display time.- Countdown timing — "X seconds left" messages must have
timeoffset = Duration - X. Example: 240s block, "30 seconds left" =timeoffset="210". - Text events in
IntervalsTrepeat with each interval cycle. Offsets are relative to the start of each cycle (on + off combined).
Common Mistakes
| Mistake | Fix |
|---|---|
Cooldown PowerLow < PowerHigh | PowerLow = start (high), PowerHigh = end (low) — opposite of what the names suggest |
| Adjacent blocks with mismatched power | PowerHigh of Warmup must equal OnPower/Power of next block; PowerLow of Cooldown must equal OffPower of preceding IntervalsT |
IntervalsT with only Cadence set | Always set both Cadence and CadenceResting; Zwift bugs to ±5 rpm offset if only one is set |
Using cadenceHigh/cadenceLow | These have no effect — use Cadence only |
<name> over ~35 chars | Causes UI artifacts in Zwift |
| Countdown offset wrong | timeoffset = Duration - X (not X) |
MaxEffort for a sprint block | MaxEffort and ERG mode are incompatible — ERG stayed engaged in testing, causing the spiral of death. Use FreeRide Duration="..." FlatRoad="1"; it toggles ERG off and back on automatically. |
Reference
There is no official Zwift documentation for the ZWO format. Best community reference: h4l/zwift-workout-file-reference. Built by scraping workout files — some documented attributes don't work. Check usage percentages as a reliability hint.
Found a ZWO behavior this skill gets wrong, or a quirk it's missing? PRs welcome — see CONTRIBUTING.md.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.