Garmin watches show one headline number — the Endurance Score (roughly 1–8600) — that blends all your sports into "how is my endurance overall". Athletes check it before anything else. The formula is closed: Firstbeat Analytics doesn't publish it. My AI agent and I rebuilt the structure from open sports-science formulas, calibrated it on my own training history, and landed within −2% of the Garmin reading.
The shape that worked: a VO₂max anchor (≈3500–5500 points for an age-grouper) plus five capped bonuses for actually using that engine — long-term load, ramp rate, long sessions, consistency, aerobic stability. Per-sport VO₂max comes from published estimators (Storer for cycling FTP, Daniels/ACSM for running threshold pace), blended by each sport's share of training load.
What we rejected or got wrong on the way:
- Multiplicative form
VO₂max × (1 + training)— a sedentary person with good genetics would score high without training. Additive + caps fixed it. - ATL (acute load) as a component — that's fatigue, not endurance. An overtrained athlete would get a bonus. Replaced with capped ramp rate.
- Curve-fitting weights to the Garmin anchor — four free variables against one observation is infinitely many solutions. Weights stay literature-fixed.
- A 12-week consistency window straddled training-phase changes (peak week + injury layoff in one window → CV 0.8, "consistency" became "did you have a long break"). Shrunk to 8 weeks, empty weeks skipped.
- Manual FTP in settings silently poisoned historical trend points — mine still said 225 W from a December peak while actual eFTP was 208. Fixed by reading date-specific eFTP per trend point.
Validation across five phases of one athlete-year: true peak scored ~6900, detraining bottom ~4400, base-without-long-sessions ~4600 — all distinguishable, and the single Garmin anchor (5773) came out as 5660 predicted. Post-ship honesty: two structural defects surfaced later (the score barely leaves your zone week-to-week; "Detrained" is unreachable because the VO₂max anchor never decays) — a decay factor and personal-relative zones are the follow-up.
Reproduce this
Prerequisites: 6+ months of training data in Intervals.icu (activities with training load, per-sport fitness/CTL in wellness sportInfo); your FTP and/or running threshold pace, weight, and age. A Garmin Endurance Score reading is optional but makes Verify stronger. API access per the Intervals.icu API thread — no special skills or plugins required, any agent with HTTP access works.
Your agent will need from you: your Intervals.icu athlete id and API key (Settings → Developer), weight (kg), age, and — if you have one — the Endurance Score number your Garmin currently shows.
Steps:
- Per-sport VO₂max (ml/kg/min):
- Bike, from Storer et al. 1990:
(10.51·ftp_w + 6.35·weight_kg − 10.49·age + 519.3) / weight_kg. Prefer a date-specific eFTP over a manually-set FTP. - Run, from Daniels (threshold ≈ 86% of vVO₂max) + the ACSM running equation:
v_threshold_kmh / 0.86→0.2 · v_m_per_min + 3.5. - Swim: proxy with the run value (no accepted swim-pace estimator exists).
- Blend by sport share of total fitness:
vo2max_composite = Σ share_sport · vo2max_sport, whereshare_sport = ctl_sport / ctl_totalfrom wellnesssportInfo. - Score =
100 · vo2max_compositeplus five capped bonuses:
- LongTerm:
min(ctl_avg_8w / 80, 1) · 1000 - Recent:
clamp(ramp_rate / 8, 0, 1) · 200(negative ramp → 0, not a penalty) - Duration: share of 28-day load from long sessions (run ≥90 min, ride ≥120, swim ≥60, ≥70% of time in Z2+),
clamp(share, 0, 0.5) · 800 - Consistency:
clamp(1 − CV(weekly_load_8w), 0, 1) · 200, population stdev, skip empty weeks, need ≥4 non-empty - Recovery: share of sessions ≥45–60 min with mean DFA-α1 ≥ 0.75,
share · 200(needs an HRV strap; 0 if fewer than 3 valid sessions)
- Compute the score for today and for a few contrasting past dates (a peak block, a layoff, a base block).
Verify: contrasting phases must separate by ≥1500 points (peak highest, layoff lowest — if they don't, your inputs are off). If you have a Garmin Endurance Score, the computed number should land within ±15% of it; ours came within 2% on the only anchor we had. Don't tune the weights to match your single anchor — that's the trap (step-3 weights are the deliverable).