A structured workout is a sequence of intervals with defined targets. The targets might be watts, a percentage of FTP, a heart rate zone, or an open effort. Each device vendor has its own format: Garmin uses FIT workout messages, Zwift uses .zwo XML. pacelore stores workouts in a common schema and exports to both.

The step schema

Every workout is an array of steps. Each step has:

  • kind: one of warmup, work, rest, cooldown, or repeat
  • durationSec: duration in seconds (required for all kinds except open-ended steps)
  • targetType: one of watts, ftp_pct, hr_pct, or open
  • targetLow / targetHigh: the target range (watts, percentage, or bpm depending on type)
  • steps: nested array for repeat kind, plus a repeatCount field

A 2x20 at 95% FTP workout looks like this in the schema:

{
  name: "2x20 Sweet Spot",
  sport: "cycling",
  steps: [
    { kind: "warmup", durationSec: 600, targetType: "ftp_pct", targetLow: 50, targetHigh: 65 },
    {
      kind: "repeat",
      repeatCount: 2,
      steps: [
        { kind: "work", durationSec: 1200, targetType: "ftp_pct", targetLow: 88, targetHigh: 95 },
        { kind: "rest", durationSec: 300, targetType: "ftp_pct", targetLow: 50, targetHigh: 55 },
      ],
    },
    { kind: "cooldown", durationSec: 600, targetType: "ftp_pct", targetLow: 40, targetHigh: 55 },
  ],
}

The repeat kind handles nested intervals cleanly. Garmin's FIT format has a native repeat message; Zwift's .zwo has an IntervalsT element. The schema maps to both without needing special cases.

FIT export for Garmin

FIT is a binary format defined by Garmin's FIT SDK. A workout FIT file contains a sequence of messages: one file_id message, one workout message with the workout name, and then a series of workout_step messages.

Each step message specifies:

  • wkt_step_name: display label
  • duration_type: time (most common), open, repeat_until_steps_complete
  • duration_value: milliseconds for time-based steps
  • target_type: power, heart_rate, open
  • target_value: the target, encoded as watts or bpm. FTP percentage targets are pre-resolved using the athlete's stored FTP at export time.
  • custom_target_value_low / custom_target_value_high: range bounds

The repeat step kind becomes a special FIT step with duration_type = repeat_until_steps_complete and a duration_value pointing back to the first step of the repeat block by step index.

Garmin devices display the workout in their structured training view. The device counts down the step duration, shows the target range, and alerts when you drift outside the zone.

Zwift .zwo export

Zwift workouts are XML files with a .zwo extension. The root element is <workout_file>, and the workout steps are child elements with types like Warmup, SteadyState, IntervalsT, Cooldown, and FreeRide.

A SteadyState element looks like:

<SteadyState Duration="1200" PowerLow="0.88" PowerHigh="0.95" />

Zwift encodes power as a fraction of FTP (0.0–1.0). pacelore's ftp_pct targets map directly — divide by 100. Watts targets are divided by the athlete's FTP at export time to produce the fraction.

IntervalsT handles repeats:

<IntervalsT Repeat="2" OnDuration="1200" OffDuration="300"
  OnPower="0.92" OffPower="0.52" />

Zwift's IntervalsT is simpler than the FIT repeat model — it only supports a single on/off pattern. For workouts with more complex repeat structures (e.g., 4x(30s on / 15s off / 2min recovery)), the FIT export handles them cleanly, but the .zwo flattens the outer repeat into separate elements. This is a Zwift format limitation, not a pacelore one.

The 60 workouts

The library is organized by sport (cycling, running, swimming), duration (30min, 45min, 60min, 90min), and intensity (recovery, aerobic, threshold, VO2max, anaerobic). Within each cell of that grid there are one to three sessions. The categorization is displayed as filter tags in the UI.

Running workouts use pace targets (targetType: "pace_pct", a percentage of threshold pace) and heart rate targets. There's no power meter on most runners. The FIT export for running uses HR targets; the .zwo export skips running workouts since Zwift is a cycling platform.

Compliance scoring

After an athlete completes a workout from the library, pacelore matches the resulting activity against the workout definition. The match uses the activity's start time to find a workout assigned to that date, or falls back to the nearest workout within ±2 hours.

The compliance score checks three things:

  • Duration compliance: Did the athlete complete at least 90% of the planned duration?
  • Target compliance: For each work step, what fraction of the step duration was spent within the target zone? Averaged across all work steps.
  • Step completion: Were all required steps completed (not counting cooldown as mandatory)?

The three components are weighted 20/60/20 and produce a 0–100 score. A 95+ means the athlete nailed it. Below 70 usually means the target was too ambitious or the athlete bonked.