agentsclimarketplace

Zwo format

Skill tomjelen/zwift-workout-skills/skills/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

Install
npx -y skills add tomjelen/zwift-workout-skills --skill zwo-format

Assembled 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 MaxEffort on an ERG-mode setup. The community reference describes MaxEffort as "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; treat MaxEffort and ERG mode as incompatible. Since most smart-trainer riders run workouts in ERG mode, FreeRide is the safe default. MaxEffort is also barely-supported: the reference documents only a Duration attribute and no child elements, so it can't carry text events during the effort.
  • FreeRide is 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 a Cadence attribute.
  • FlatRoad="1" gives flat, controlled resistance the rider gears against, instead of following the in-game gradient.
  • Cadence is 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 Power entirely — 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). PowerHigh should match the power of the next block for a smooth transition.
    • Example: if followed by IntervalsT OnPower="0.80", then PowerHigh="0.80".
  • Cooldown ramps DOWN: PowerLow (start, high) > PowerHigh (end, low). PowerLow should 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 on OffPower, so PowerLow = 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 "IntervalsT always ends on a recovery" below), keep PowerLow at 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.

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.
  • cadenceHigh and cadenceLow have 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 during OnDuration (the work interval).
  • CadenceResting — applied during OffDuration (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 Cooldown that follows picks up from OffPower (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, IntervalsT alone can't express it: use Repeat="N-1" plus a standalone final work block, or build the set from repeated <SteadyState> blocks — and note the total duration shifts by one OffDuration either 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="..."/>timeoffset is seconds from the start of the containing block.
  • duration attribute (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 IntervalsT repeat with each interval cycle. Offsets are relative to the start of each cycle (on + off combined).

Common Mistakes

MistakeFix
Cooldown PowerLow < PowerHighPowerLow = start (high), PowerHigh = end (low) — opposite of what the names suggest
Adjacent blocks with mismatched powerPowerHigh of Warmup must equal OnPower/Power of next block; PowerLow of Cooldown must equal OffPower of preceding IntervalsT
IntervalsT with only Cadence setAlways set both Cadence and CadenceResting; Zwift bugs to ±5 rpm offset if only one is set
Using cadenceHigh/cadenceLowThese have no effect — use Cadence only
<name> over ~35 charsCauses UI artifacts in Zwift
Countdown offset wrongtimeoffset = Duration - X (not X)
MaxEffort for a sprint blockMaxEffort 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.

Keep looking

Skills are one crate of 326,144. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.