Workout structure
How to describe the steps of a workout, and what Vekta does with them.
A training prescription or a training collection can have a structure: the
list of steps of the workout, with their targets. Vekta uses it to
estimate the workout (duration, distance, work...) and to send it to the
athlete's devices.
Structures are only available for cycling, running and swimming.
Steps and repetitions
A structure is a list of steps and repetitions.
A step has:
- a
label:warm up,active,intra-recovery,rest,cool downormaximum; - a
length: a duration in seconds, a distance in meters, orfreewhen the athlete decides when to move on; - an
intensity: its targets, all optional.
A repetition is a block of steps done nRepetitions times. It can contain
other repetitions, up to 3 levels deep.
For example, 3 × 12 minutes at threshold in a cycling workout with
zonesType: "power" (explained below):
[
{ "type": "step", "label": "warm up", "length": { "type": "duration", "duration": 900 } },
{
"type": "repetition",
"nRepetitions": 3,
"steps": [
{
"type": "step",
"label": "active",
"length": { "type": "duration", "duration": 720 },
"intensity": { "power": { "zone": "threshold" }, "cadence": { "min": 85, "max": 95 } }
},
{
"type": "step",
"label": "intra-recovery",
"length": { "type": "duration", "duration": 300 },
"intensity": { "power": { "minPercentage": 50, "maxPercentage": 60 } }
}
]
},
{ "type": "step", "label": "cool down", "length": { "type": "free" } }
]Targets
Each sport accepts different targets:
| Target | Cycling | Running | Swimming | Unit |
|---|---|---|---|---|
power | ✓ | ✓ | watts | |
heartRate | ✓ | ✓ | ✓ | bpm |
speed | ✓ | ✓ | m/s | |
cadence | ✓ | ✓ | rpm (steps per minute for running) | |
stroke | ✓ | freestyle, backstroke... | ||
passive | ✓ | true for a rest at the wall |
Sending a target the sport doesn't accept returns a 400.
power, heartRate and speed can be given in three ways:
- A zone:
{ "zone": "threshold" }, the name of one of the athlete's zones. Only for thezonesTypetarget (see below), and not in collections: a collection isn't tied to an athlete, so it has no zones. - A % of a metric:
{ "minPercentage": 95, "maxPercentage": 105 }. The metric is the critical power for power, the max heart rate for heart rate, and the critical speed for speed. - An absolute range:
{ "min": 250, "max": 280 }, in the target's unit.
The main target: zonesType
Whenever you send a structure, also send a zonesType in the same request.
It has to be one the sport allows:
| Sport | Allowed zonesType |
|---|---|
| Cycling | power, heartRate |
| Running | speed, heartRate, power |
| Swimming | speed, heartRate |
Only the zonesType target can be given as a zone. With zonesType: "power",
a step can have { "power": { "zone": "threshold" } }, but its heart rate can
only be a % or an absolute range.
Why a single kind of zone? Vekta tells you the zone of each step (the
step's zone, in what it returns). Athletes have power, heart rate and speed
zones, often with the same names, so a step's threshold could mean three
different things. With one zonesType, every zone in the workout is of that
kind: the step's zone is always the one its zonesType target falls in.
Why collections need one too. A collection has no zones, but when you
prescribe it, Vekta finds the zone of each step for that athlete. The
zonesType tells it which target to use, so again only one kind of zone shows
up.
How targets are converted
When you create a prescription with a structure, send a new one, or prescribe a collection, Vekta converts every target with the athlete's zones and latest metrics. A collection's targets are kept as you send them until then.
- A zone gives the range of that zone. The athlete's zones are the ones
set for the sport and
zonesType. If they have none, Vekta's default zones are computed from their metric (for example their critical power). - A % gives a range in the target's unit, from the athlete's metric.
- An absolute range also gets its % of the metric, when the athlete has it.
If a step needs something the athlete doesn't have, you get a 400, for
example: a % of power without a critical power, a zone that isn't one of the athlete's zones,
or a zone with neither zones nor a metric to compute them from.
What Vekta computes
From the steps, Vekta estimates:
duration: the sum of the step durations. Steps with a distance use their speed target to get a duration.distance: the sum of the step distances. Steps with a duration use their speed target to get a distance. Rests at the wall count as 0.work,averagePowerandcalories: from the power of each step. Steps without power count as 0 W.carbIntake: only if you don't send it, when the athlete has the metrics it needs, unless the workspace the prescription was created in has carb recommendations turned off.
When Vekta can estimate a value, it replaces the one you send. When it can't (for example a distance step without a speed target):
- The value you send is used,
nullincluded. - If you don't send one and the steps stay the same, the previous value stays. Prescribing a collection with its own steps keeps the collection's values.
- If you don't send one with new steps, it's cleared, since it belonged to the old steps. Send it again if you want to keep it.
For a collection, only duration and distance are estimated. Since there's
no athlete, only absolute speed targets can turn a distance into a duration, or
the other way around.
Free steps
Steps with a free length share the time the other steps leave. If you send
"duration": 3600 and the other steps add up to 50 minutes, the remaining 10
minutes are split equally between the free steps. You get each one's share
back in freeDuration.
This time is used in two ways:
- For the estimates: with it, the free steps count in the work, average power and calories.
- For devices: Wahoo, Zwift and Hammerhead don't support free steps,
so they get each free step as a step of
freeDuration. Without afreeDuration(noduration, or one shorter than the other steps), the workout can't be sent to them, anddeviceSyncssays so.
When you send new steps with free steps, send the duration too: as for the
other values, the previous one isn't reused.
Updating
Without a new structure, the stored one is kept. You can still change
duration, distance and work, except when the steps decide them:
duration, when every step has a duration;distance, when every step has a distance (rests at the wall aside);work, when every step has a power and a duration.
Sending a different value in these cases returns a 400. Changing the
duration of a structure with free steps shares the new time between them.
To change the sport of a workout with steps, also send a new structure,
or structure: null to remove the steps. The old steps may not fit the new
sport.
structure: null removes the steps. The workout keeps its other fields.
The steps aren't converted again when the athlete's zones or metrics change.
They're converted with their current values when you send the structure
again, or when you change the duration of a structure with free steps (which
fails with a 400 if a step now needs a zone or metric the athlete doesn't
have).
Changing a collection never changes the prescriptions already made from it.
Devices
Unless it's hidden, a prescription with steps is sent to the athlete's
connected devices. Updating or deleting it updates or removes it there too. The
deviceSyncs field of the response tells you what happened on each device,
and why when the workout couldn't be sent. A prescription without steps isn't
sent to devices.
Each device supports different things. A workout a device can't take is
skipped on it (for example a past date on Zwift), and deviceSyncs says why.
| Garmin | COROS | Wahoo | Zwift | Hammerhead | |
|---|---|---|---|---|---|
| Cycling | ✓ | ✓ | ✓ | ✓ | ✓ |
| Running | ✓ | ✓ | ✓ | ✓ | |
| Swimming | ✓ | ✓ | |||
| Steps with a distance | ✓ | ✓ | ✓ | ||
| Free steps | ✓ | ✓ | with freeDuration | with freeDuration | with freeDuration |
| Targets per step | up to 2 (1 for running) | 1 | all | 1 (+ cadence) | 1 (+ cadence) |
| Dates | any | from today, up to 1 year ahead | any | from today | any |
How each device picks the targets of a step:
- Garmin: the
zonesTypetarget first, then the other targets, then cadence, keeping the first two the step has (only one for running). For swimming, the speed target and the stroke. - COROS: one target: the
zonesTypeone, or else cadence for running, or else the other target the step has. For swimming, only the stroke. - Wahoo: every target the step has: power, heart rate, speed and cadence.
- Zwift: power for cycling and speed for running, as a % of the athlete's critical power or critical speed, plus cadence. Every step needs one, so the athlete needs that metric.
- Hammerhead: like Zwift, for cycling only.
Reading a structure back
The structure you get back has the same steps, repetitions, labels and
lengths as the one you sent. Only each step's intensity is different: it
lists every field of the sport, set to null when unused (except passive,
only present on rests at the wall), with the values
Vekta converted. For example, the first active step above, for an athlete
with a critical power of 280 W:
{
"vektaIntensity": "threshold",
"zone": "threshold",
"fromPower": "zone",
"minPower": 252,
"maxPower": 294,
"minPowerThresholdPercentage": 90,
"maxPowerThresholdPercentage": 105,
"fromHeartRate": null,
"minHeartRate": null,
"maxHeartRate": null,
"minMaxHeartRatePercentage": null,
"maxMaxHeartRatePercentage": null,
"minCadence": 85,
"maxCadence": 95
}from…tells how the target was given:zone,metric(a %) orabsolute(a range in the target's unit).min…/max…are in the target's unit, and the…Percentagefields are the same range in % of the metric.zoneis the athlete's zone the step is in, for thezonesTypetarget.vektaIntensityis Vekta's own classification of the step.- Swimming steps have no cadence, but have
stroke(nullwhen not set), andpassive: truefor rests at the wall. - In a collection, nothing is converted yet: a % target keeps its
…Percentagefields, and itsmin…/max…staynulluntil it's prescribed.
This returned intensity format may change in the future, to look like the
one you send ({ "power": { "zone": "threshold" } }...).