HR target workouts broken on Edge 840 firmware ≥30.18 — interpreted as %HRmax instead of bpm
Summary
After installing Edge 840 firmware 30.18 (changelog entry: “Aligned presentation of time in zones for power and heart rate with Garmin Connect”),
structured workouts pushed via the intervals.icu API with HR-bpm targets are misinterpreted by the device.
The Edge now displays the workout’s lower HR bound as bpm_value × HRmax / 100.
For an athlete with HR max 199 and a target of 122 bpm, the Edge alarms "HR below minimum 242 bpm" (= 122 % × 199 ≈ 242.8) continuously throughout the ride.
The same workout structure worked correctly on the Edge before the firmware update.
Reproduction
Push a Z2 workout with HR-bpm target via /events/bulk:
curl -X POST 'https://intervals.icu/api/v1/athlete/0/events/bulk?upsert=true&upsertOnUid=false&updatePlanApplied=false' \
-u "API_KEY:$KEY" -H "Content-Type: application/json" -d '[{
"external_id": "hr-bug-repro",
"category": "WORKOUT",
"type": "Ride",
"start_date_local": "2026-05-25T09:00:00",
"name": "HR bug repro",
"moving_time": 3600,
"description": "- 60m 122-147bpm Z2 endurance",
"workout_doc": {
"target": "HR",
"steps": [{
"intensity": "active",
"duration": 3600,
"hr": {"start": 122, "end": 147, "units": "bpm"},
"text": "Z2 endurance"
}]
}
}]'
Load the synced workout on an Edge 840 running firmware ≥30.18 and start the activity. The device alarms "HR below minimum 242 bpm" (where 242 = 122 × HRmax / 100).
Investigation findings
While debugging this on my own account, I observed that all public export endpoints strip HR-bpm targets entirely:
| Export | HR-bpm result | Power-%FTP result (control) |
|---|---|---|
/events/{id}/download.zwo |
<SteadyState Duration="3600"/> — no target attrs |
<SteadyState PowerHigh="0.75" PowerLow="0.56" Duration="3600"> |
/events/{id}/download.mrc |
Empty [COURSE DATA] |
Populated with target % values |
/events/{id}/download.fit |
195 bytes, no target zone | 204 bytes, with power target |
The hr_zone variant (hr: {units: "hr_zone", value: 2}) also strips on export — even though intervals.icu maintains a distinct HR-zone internal model (zoneTimes returns Z1–Z5 instead of the power-zone Z1–Z7 when target: HR).
Additionally, intervals.icu’s own Zwift importer drops HR targets: uploading a .zwo containing <SteadyState HRHigh="147" HRLow="122" Duration="3600"> via /folders/{id}/import-workout results in a stored workout_doc where the step is replaced by power: {units: "%ftp", value: 0} — the HR target metadata is discarded entirely.
These observations suggest the workout_doc HR-target representation either has no Garmin-format translation in your codebase, or the internal Garmin Connect sync API path emits HR values without an explicit absolute-bpm unit marker — relying on a pre-30.18 lenient Edge default that no longer exists.
Suspected cause
intervals.icu’s Garmin Connect sync emits HR target values without explicitly tagging them as absolute bpm. Pre-30.18, the Edge defaulted to “treat as bpm.” Post-30.18, the Edge follows Garmin Connect’s documented convention and defaults to “treat as %HRmax.” The numeric value passes through unchanged, but its meaning flips.
Workaround (for affected users)
Switch HR-target workouts to power-target with {units: "%ftp"} ranges — these survive both the export pipeline and the Garmin Connect sync correctly. For HR-ceiling enforcement on the Edge, use the device-level HR alert (Activity Profile → Alerts → Heart Rate → set ceiling), which works regardless of workout target.
Ask
Would it be possible to either:
- Tag HR values explicitly as absolute bpm in the Garmin Connect sync payload, restoring pre-30.18 behavior, or
- Translate HR-bpm targets into power-equivalent zones at sync time (if a power meter is available on the athlete’s profile).
Happy to provide additional diagnostic data (specific workout IDs, the captured .zwo / .mrc / .fit exports, FIT hex dump, or a copy of my full investigation) if deemed useful.