Are You Actually Progressing? Cross-Activity Interval Comparison Using the API

Are You Actually Progressing? Cross-Activity Interval Comparison Using the API

Hey everyone,

I’ve been experimenting with something that I think could benefit a lot of us, and I wanted to share the approach + results before making a proper feature request.

The Question

We all do recurring structured workouts — SweetSpot blocks, VO2max intervals, cadence drills. But how do you objectively answer: “Am I getting better at this specific effort over the past 8 weeks?”

Right now, you’d have to open each activity individually, find the matching interval, write down the numbers, and compare manually. Intervals.icu is amazing at per-activity analysis, but there’s no way to compare the same labeled interval across multiple sessions.

What I Did

Using Python + the Intervals.icu API, I built a simple workflow:

  1. Label intervals consistently across recurring workouts (e.g., “Set1 95rpm”, “Set2 SweetSpot”) — either at recording time or retroactively via PUT /api/v1/activity/{id}
  2. Query matching intervals across all activities of the same type
  3. Compute trends automatically (power, HR, decoupling, cadence delta)

That’s it. The magic is in the consistent labeling.

What It Revealed

SweetSpot blocks over 8 weeks (6 sessions):

  • Power: 195W → 201W (+3.2%) with HR basically flat (+2 bpm)
  • Decoupling: 4.48% → 3.36% (−25%) — aerobic efficiency improving
  • Min watts: 106W → 148W (+40%) — holding target much more consistently

Cadence drills — “95rpm” blocks over 8 weeks (5 sessions):

  • Actual cadence: 79.9 rpm → 94.1 rpm — took 8 weeks to finally hit the target!
  • Power: 150W → 156W (+4%) at same HR — neuromuscular adaptation confirmed

These trends were invisible when looking at activities one by one. Seeing them lined up chronologically made the progression (and the problem areas) immediately obvious.

Where It Doesn’t Work

Pure endurance rides — no internal structure to compare, prescriptions vary by design, outdoor terrain adds noise. For those, whole-activity metrics (Pw:HR ratio, global decoupling) remain more useful.

The Feature Request

I think this could be incredibly powerful as a native feature:

  • “Compare Across Activities” — select a workout name pattern, pick a label, see a trend chart at interval granularity
  • Label templates in workouts — define semantic labels (“Set1 SweetSpot”, “Recovery”) in the workout builder that auto-apply on recording
  • Interval-level progression charts — a mini fitness chart scoped to a specific block type

For the Dev-Curious

Retroactive labeling works perfectly via the API — the stream data is already there, you’re just telling Intervals.icu how to slice it. Happy to share code snippets and more detailed methodology if there’s interest.


Has anyone else tried systematic interval comparison across workouts? Curious to hear other approaches, and whether this kind of feature would be useful to you.

Context: indoor trainer, FTP ~220W, structured training since mid-2023

1 Like

I tag my activities accordingly and can then easily find them on the compare activity page. I want to compare the entire activity, i.e., how I performed in all intervals, not just a single interval of an activity, which works relatively well.

Right — the Compare Activity page is great for whole-activity comparison. What I’m describing goes one level deeper: comparing a specific labeled interval (e.g., “Set2 95rpm”) across multiple activities over time.

For instance, in a Cadence Variation workout with 6 different blocks, I don’t care about the warmup or cooldown drifting — I want to see if my power at 95rpm is improving week over week while HR stays flat. That granularity isn’t available in Compare Activity today.

Do you find that whole-activity comparison gives you enough signal for structured workouts with mixed intensity blocks?

1 Like

Yes, I completely agree with you that it is difficult to make a comparison at this level at the moment.

Personally, when looking back, I don’t look at individual intervals in detail in comparison with others, but rather consider the overall picture in terms of the activities and circumstances surrounding them.

One more thing: if you select Cadence and Power under Progress, for example, you can at least observe the development across all interval, each dot represents an interval (the small dots are the averages for all intervals):

Thanks for the tip on the Progress view with Cadence + Power dots — I tried to reproduce it but couldn’t find the setting that gives you that interval-level scatter plot. It’s not immediately obvious from the UI, which is already a minor friction point worth noting.

But even assuming I could reproduce it, I ran a concrete test using the API to compare my two CadenceVariations sessions (Feb 18 vs Feb 25), and it illustrates exactly why the dot view would be insufficient here.

The two sessions had very different internal structures: 36 labeled intervals in S081, 15 in S082. In a Progress scatter plot, all those points would land in the same cloud with no way to distinguish “Set1 95rpm” from “Set3 105rpm” from the warmup. You’d be looking at 51 mixed dots — different target cadences, different block durations (some blocks went from 119s to 180s between sessions), different physiological intents.

The only blocks I could meaningfully compare across both sessions were the ones sharing the same label. For “Set1 95rpm” specifically:

  • Power: 156W → 156W (stable)
  • Avg HR: 104 → 100 bpm (−3.8% — cardiac efficiency improving)
  • Decoupling: 3.82% → 2.12% (−44% — solid aerobic adaptation signal)

Those trends are real and actionable. But they’d be invisible in a global scatter, drowned out by the structural noise between sessions.

This is why I think label-scoped comparison needs to be a first-class feature — not a workaround through a whole-activity view. The signal is there in the data, it just needs the right filter to surface it.

This is the kind of question a connected assistant answers without anyone building a chart for it. With access to the raw streams and interval data it can compute [EF on your threshold intervals / IF per activity / NP curve] across whatever range you name, and show its working — which activities, which dates, what it excluded.

Not a substitute for a native chart if you want it on the dashboard every day. But if you mainly want to know whether the number is moving, you can have the answer now instead of waiting for it to be built.

Happy to post what it returns for one of my own blocks if that’s useful before you try it.

That’s exactly the approach I’ve been running for the past few months — a connected assistant with direct access to the Intervals.icu API, raw streams included.

Here’s what it actually returns for my CadenceVariations sessions, comparing Feb 18 vs Feb 25 on the “Set1 95rpm” block specifically:

  • Power: 156W → 156W (stable)
  • Avg HR: 104 → 100 bpm (−3.8% — cardiac efficiency improving)
  • Decoupling: 3.82% → 2.12% (−44% — meaningful aerobic adaptation signal)
  • Block duration: 119s → 180s (structure change between sessions — which the assistant flags automatically)

It also surfaces the structural noise I mentioned above: S081 had 36 labeled intervals, S082 had 15. Without label-scoped filtering, any chart-based view would mix those into an unusable cloud.

The transparency on what it excluded is key — I know exactly which blocks were skipped and why (different labels, duration mismatch, incomplete streams).

Would be curious to compare with what your setup returns. What’s your stack — direct API queries, or something built on top?

That 44% drop looks more convincing than it probably is. These are short blocks and the second one is a minute longer, so HR lag alone could change the result quite a bit. Still worth tracking, but I’d want to see it happen across a few similar sessions before calling it aerobic adaptation.

@RideReviewer is right, and it’s worth being specific about why, because this is the failure mode that makes connected-assistant analysis untrustworthy rather than just noisy.

Heart rate lags a step change in power with a time constant of roughly 30–60 s, and takes two to three minutes to settle. Decoupling compares the second half of a block with the first. On a 119 s block, HR is still climbing through nearly all of it, so the second half reads high and decoupling reads high. On a 180 s block, HR has time to plateau, so the second half is flatter and decoupling reads low.

A longer block produces lower decoupling regardless of fitness. Feb 18 was 119 s and Feb 25 was 180 s, so a drop was arriving whether or not anything adapted. The 44% is mostly block length.

The HR figure has a second problem: 156 W at ~100 bpm is very low intensity, and at that end HR is dominated by ambient temperature, sleep, caffeine and time of day. 104 → 100 bpm is comfortably inside day-to-day noise for the same rider on the same block.

None of which means the tooling is wrong - the workflow is the right idea and the label-scoped filtering is the hard part solved. It’s that an assistant will compute decoupling on mismatched blocks perfectly happily and hand you a number that looks like adaptation. It has no idea the comparison wasn’t like-for-like unless you make that its problem:

Compare those metrics across the sessions. Before reporting anything, check the blocks are comparable: same label, duration within 10%, similar intensity. If they are not, say so and do not report decoupling - the shorter block will read higher on HR lag alone. List every block you excluded and why.

That turns the assistant from something that answers into something that refuses, which is the more useful behaviour here.

For the underlying question - am I progressing at this effort - I’d want three or four matched sessions before reading anything into a trend, and I’d trust power at fixed HR, or HR at fixed power, well ahead of decoupling on short blocks.