Notion + Garmin + Strava: My Running Intelligence Stack
A runner combined Garmin, Strava, and Notion into a personal training intelligence stack using Python scripts, webhooks, and a static HTML dashboard. The system revealed key insights: threshold runs were too intense, ACWR guided safe load increases, and polarized training zones were validated—turning raw data into actionable decisions.
Garmin and Strava are great at recording. Notion is great at planning. Neither, on its own, gives you insight.
After three years of stagnant strength numbers and a four-point drop from my VO₂max peak, I got tired of staring at three different apps and feeling no smarter. So I wired them together. The result is a personal running intelligence stack that actually tells me what to do next — not just what I did yesterday.
Here’s how it’s put together, what it shows me, and how you can run a version of it yourself.
The three layers
The stack has one job: turn raw data into decisions. It does that in three layers.
1. Notion — the spine
Every workout — running and strength — lives as a row in a Notion database called Øktlogg (“session log”). Each row has fields for date, distance, time, pace, average heart rate, elevation, location, surface, shoe, and a link back to the planned workout from a separate Treningsbibliotek (workout library). Strength sessions get their own per-set rows in an Øvelseslogg database with weight, total reps, and RIR (reps in reserve).
Why Notion and not a spreadsheet? Two reasons: relations and views. The set-level log links to an exercise catalog, which links to muscle groups. That means I can ask “how much volume did my upper chest get last month” without moving a single row. And calendar/board/gallery views give me a different lens on the same data depending on whether I’m planning, reviewing, or just curious.
Notion is the source of truth. Everything else flows into it.
2. Garmin Firstbeat — the physiology layer
Garmin’s Firstbeat algorithms are the closest thing a non-athlete gets to a sports lab. The Forerunner watch plus a chest strap give me LTHR (lactate threshold heart rate), LT-pace, VO₂max estimates, race predictions, training load, HRV status, and acute-to-chronic workload ratio. These aren’t vanity metrics; they’re the dials that actually steer the training.
My current snapshot, pulled live: LTHR 168, LT-pace 4:15/km, VO₂max 57.1, fitness age 18. Garmin auto-detected the threshold values during a real chest-strap interval session — not from an age formula.
3. Strava — ground truth and cadence
Strava is where I cross-check Garmin and pull cadence, weekly volume, and GPS traces. It’s also the most reliable source for historical activity dumps. I talk to it through a small Python CLI in my dotfiles — strava recent, strava since 2026-03-01, strava activity <id> — backed by a long-lived OAuth refresh token.
How it’s wired together
The glue is small and boring on purpose. Three pieces:
- A Strava ↔ Notion integrator — a small Next.js + Supabase app that listens to Strava webhooks and links each activity to its planned Notion page based on time-window and similarity rules. Encrypted tokens, an inbox for ambiguous matches, OAuth for both ends.
- A fitness CLI —
stravaandgarminshell wrappers around Python that pull recent activities, daily stats, and Firstbeat values. Used inside Claude Code sessions for ad-hoc analysis (“what did my last threshold session look like”). - A static HTML dashboard — one file, no build step. A refresh script pulls 12 weeks of Strava activities and Garmin Firstbeat values, writes them to a JS data file, and the page renders charts, race predictions, and the last few sessions with notes from Notion.
That’s it. No Kubernetes, no streaming pipeline, no ML model. A handful of OAuth tokens, a few hundred lines of Python, and a single HTML file.
The dashboard
The HTML dashboard is the part I actually look at every day. A sticky chip-row at the top jumps to: status, Garmin Firstbeat panel, plan, HR zones, weekly volume, quality sessions, race predictions, latest runs, threshold history, and a glossary.
Concretely, it shows:
- Last threshold session and current LT-pace target, side by side
- Heart-rate zones with bpm ranges and pace mappings, derived from LTHR (not max HR — more accurate for steady-state work)
- Bar chart of weekly kilometres for the last 12 weeks
- Line charts of threshold pace and cadence with goal lines from Garmin
- Race predictions for 5K / 10K / half / marathon, recomputed each refresh
- Last six runs with type, note excerpt, and link back to the Notion row
- Threshold-session history with HR-cap discipline scoring
- A glossary with inline tooltips for LT1, LT2, LTHR, VO₂max, cadence, ACWR, TE — so future-me doesn’t have to re-Google
All of it driven by one JSON refresh. No login, no SaaS, no monthly subscription.
Four insights I couldn’t see before
1. Threshold is HR-driven, not pace-driven
For years I ran threshold by feel, settling at whatever pace felt “comfortably hard.” When I scanned my last seven threshold sessions side by side, every single one drifted into 171–177 bpm — over my LTHR of 168. That’s not threshold; that’s a slow VO₂ session. Now the rule is hard: HR cap 165, drop pace 5–10 s/km if needed. That single change is doing more for my aerobic ceiling than the previous six months of “hard” running did.
2. ACWR tells me when to push
Acute-to-chronic workload ratio (last 7 days vs last 28) is the single best predictor of injury risk in endurance research. Sweet spot: 0.8–1.3. Above 1.5 is the danger zone. Mine is sitting at 1.1 right now, which means I have margin to add a session without risking the kind of overuse niggle that has historically derailed my spring blocks.
3. Plateau diagnosis from physiology, not the scale
Mid-cut I hit a 10-day weight plateau and assumed metabolic adaptation. The dashboard said otherwise: resting HR was still falling (49 → 45 over five weeks), Body Battery during sleep was rising (52 → 62), and HRV stayed BALANCED. Classic adaptation looks the opposite. The real culprit was variable sleep quality driving water retention. The treatment changed from “refeed and reset metabolism” to “fix bedtime” — a cheaper and more effective intervention.
4. Polarized training, validated
Polarized training (80% easy, 20% hard, almost nothing in between) only works if your easy is actually easy. The HR-zone breakdown of my last three quality sessions vs my easy runs makes it visible: 30% / 59% in Z1 / Z2 on an easy day, vs 22% / 31% in Z3 / Z4 on an interval day. No drift into the grey zone. That’s the model working as designed — and I only know because the dashboard puts the percentages next to each other.
Want the repo?
The Strava ↔ Notion integrator is the most reusable piece of this stack. It handles OAuth for both services, encrypts tokens, listens to webhooks, and links activities to Notion pages with configurable rules. It’s a multi-tenant Next.js + Supabase app that can run on Vercel.
If you’d like access to the repo to run your own copy — connect your Strava, your Garmin, and your Notion training database — send me a message. I’ll add you to the GitHub repo and walk you through the setup. The CLI tools and dashboard template are part of the same kit.
Two small caveats. It’s a personal project, not a polished SaaS — expect to read code and edit a config file. And it’s most useful if you already use Notion for training; if you don’t, the dashboard alone (which only needs Strava + Garmin) is the lighter-weight starting point.
Either way, the punchline is the same: the value isn’t in any single app. It’s in the wiring between them, plus a single page that puts the answers next to each other. That’s where the insight lives.
Indie maker and developer. Building productivity tools and writing about systems, automation, and the craft of focused work.
Want a custom Notion template?
Browse my ready-made tools or get in touch for a custom build.

