HR target workouts broken on Edge 840 firmware ≥30.18 — interpreted as %HRmax instead of bpm

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:

  1. Tag HR values explicitly as absolute bpm in the Garmin Connect sync payload, restoring pre-30.18 behavior, or
  2. 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.

This is no valid intervals syntax. If you use the API the description should contain valid syntax, this is what will be parsed.

An example of correct syntax would be:

"description": "- 122-147bpm 60m Z2 HR",

But intervals is not supporting absolute values. The bpm range is considered as text.

Feed your AI with the correct syntax