Garmin-intervals-bridge – your complete Garmin FIT files and wellness data in Intervals.icu, with or without the official Garmin integration

0.5.1 and 0.5.2 are out – two small ones:

  • Windows fix (0.5.1): every command stopped right away with ZoneInfoNotFoundError – Windows has no time zone database Python can read, and the bridge didn’t bring one. Now it does (thanks @t.me_massarob for the report). Update with py -m pip install --upgrade garmin-intervals-bridge.
  • Your own day boundary (0.5.1): without BRIDGE_TIMEZONE the bridge now uses your computer’s time zone instead of mine (Vienna) – the zone decides which date last night’s sleep lands on. Containers have no time zone of their own: add -e BRIDGE_TIMEZONE=Europe/… with your zone, otherwise the bridge falls back to UTC (and says so in the log).
  • Gear picked in the app after the run (0.5.2): the bridge now reads an activity’s gear once more half a day later, so shoes you add in Garmin Connect after the run reach Intervals too (thanks @DarKen). Gear chosen on the watch already came with the activity.
  • Version in the log (0.5.2): run starts with garmin-intervals-bridge 0.5.2 running …, so a container’s log shows which image is running.
  • Behind the scenes, the tests now also run on Windows and macOS, so a slip like the one above gets caught before a release.

Upgrade: pip install -U garmin-intervals-bridge, or pull the new image (ghcr.io/futureweb/garmin-intervals-bridge:0.5.2 / futurewebat/garmin-intervals-bridge:0.5.2). Notes: 0.5.1 · 0.5.2.

1 Like

0.6.0 is out: ImReady4’s training advice, without the morning MATLAB step.

If you use ImReady4 (by @Inigo_Tolosa and @MedTechCD) for HRV-guided training, the bridge can now do the daily run for you: right after its morning read – about ten minutes after your watch syncs – it calculates the advice from Intervals’ HRV and resting HR and writes Rest / LIT / Planned / HIT into your existing TrainingAdvice field. Same inputs, same 30-day z-scores, same zones as the script; on my account it agrees with ImReady4’s own results on 97.6 % of 1,225 days.

  • Off by default. Switch it on with BRIDGE_IMREADY4_ENABLED=true (systemd env file, -e for a container, the .env file on Windows/Mac).
  • Needs: the TrainingAdvice wellness field from ImReady4’s guide (a select with 1 Rest, 2 LIT, 3 Planned, 4 HIT – the bridge finds it, never creates or changes it), and HRV + resting HR on at least 14 of the last 30 days.
  • Safe: it never overwrites a value that’s already there (yours or the script’s), never touches a locked day, writes only that one field, reads it back and does it once a day. If the night’s values are late it waits and retries; if they never come, there’s simply no advice that day. Intervals requests only, no extra Garmin calls.
  • See it first: garmin-intervals-bridge imready4 shows today’s advice with the z-scores and the reference, as a dry run; garmin-intervals-bridge imready4 --from 2026-01-01 --compare compares it with what your field already holds; --apply fills days that have none.

Upgrade: pip install -U garmin-intervals-bridge or pull the new image (ghcr.io/futureweb/garmin-intervals-bridge:0.6.0 / futurewebat/garmin-intervals-bridge:0.6.0), then set the variable. Details: docs/IMREADY4.md · release notes.

Sources:

For anyone considering using this.

It works REALLY well.

@Andreas_Schnederle-W has set it up with some great ideas and executed them really well.

My Intervals activities now get the previously blocked off Garmin fields… My wellness data is more complete…

My setup - using a Docker Container on an old Synology NAS. The addition of it to Docker Hub makes it easy to add the container but you need to watch out for some things.

When setting up a container make sure to go in to advanced settings and set the execution command to run --apply or whatever you are trying to achieve at that particular time. Add your api key in the environmental variables and add a persistent folder that points to /data within the container.

First time setup needs a login execution command and you can then go in to details on the container and the terminal section and enter your garmin username and password. It’ll retrieve the token it’ll use going forward. After that you can bin that container and set one up with sync for testing or run apply like mentioned earlier. After that things “just work” for me.

To add gear I followed Andreas advice, and set up a second container (same data folder mapping etc.) with the execution command for backfilling my activities, stopped my “live” container and let that run (it took about 90 mins to pull in 19 months of activites). Stopped that container, deleted it and restarted my live container.

From there opened the details of the container went in to terminal and enter gear --create --apply pulled in all my gear from Garmin. In hindsight I’d suggest also running with --include-retired as I had some recently retired shoes that were left off but I’ve filled those gaps manually now so no big deal. After the gear sync ran I restarted the container (not sure if that was necessary) and now it’s sitting waiting for my next activity.

My workflow is essentially - Run Intervals defined workout that is synced via Intervals / Garmin connection, finish the activity, it syncs (to Strava and Intervals), if the Strava activity hits intervals first it then gets replaced (by intervals) with the Garmin one, after which the intervals bridge fills in the missing data including my gear.

Happy to field any queries on Synology / Docker quirks.

Maintanence note - once you have it working and setup use the Synology Docker option to export your container settings (your api key, execution command, data folder mapping etc.). This makes it quicker to fire up a new container after you’ve pulled the latest image from Docker Hub (which might be frequently as the bridge continues to be developed at pace).

1 Like

Thanks @DarKen, that’s a great write-up – and exactly the Synology/Docker detail I can’t test myself.

A few small things that might make it easier for the next person:

  • Login without a second container: you can start the live container straight away with run --apply. Until there’s a Garmin session it just pauses, and in the meantime open its Terminal tab and run garmin-intervals-bridge login once – run picks the session up on its own.
  • No restart needed after gear --apply: the running bridge keeps gear in line from then on, for new activities too.
  • Retired shoes: gear --create --include-retired --apply adds them as well (retired, with their dates) – good tip.
  • The history backfill is the one step that still needs the live container stopped, as you did.

And +1 on exporting the container settings – that makes pulling a new image a two-minute job.

0.7.0 is out – mostly things that came out of your feedback here:

  • Strava first, Garmin later: when Intervals replaces a Strava copy with Garmin’s own, the bridge now enriches it within the minute instead of waiting for the next full scan (thanks @DarKen).
  • Synology guide: @DarKen’s write-up, turned into a step-by-step guide – container, Garmin login right in the running container’s Terminal, gear, the one-off history backfill, updating.
  • Windows “an application control policy has blocked this file”: that’s Smart App Control blocking the unsigned garmin-intervals-bridge.exe launcher pip creates. Start the bridge with py -m garmin_intervals_bridge … instead (also in run-bridge.cmd) – the Windows guide now does that everywhere.
  • Gear: the plan now points out retired shoes that --create leaves out; --include-retired adds them.
  • Experimental, off by default – a calmer second training advice: ImReady4 judges single mornings and flips a lot (on my account on 40 % of days). The HRV-guided training studies judged the trend instead – the 7-day HRV average against your 60-day normal range, plus the night’s heart rate. With BRIDGE_HRVTREND_ENABLED=true the bridge writes such an advice into a field of its own (HRVTrendAdvice), next to TrainingAdvice, so you can compare both for a few weeks. Details and sources: docs/HRV_TREND.md.
  • Fix: a fast backfill no longer stops at Intervals’ rate limit (HTTP 429) – it waits and writes again.

Upgrade: pip install -U garmin-intervals-bridge (Windows: py -m pip install --upgrade garmin-intervals-bridge) or pull the new image (futurewebat/garmin-intervals-bridge:0.7.0). Notes: release 0.7.0.

0.7.1 – a small one: new shoes in Garmin Connect reach Intervals by themselves.

Once you’ve set up gear with gear --create --apply, the scheduled runs now also create gear that shows up in Garmin later and put it on its activities – no more running the gear command after buying a new pair (thanks @DarKen for asking). If you set up gear before 0.7.1, run gear --create --include-retired --apply once more: that’s what switches it on. gear --apply --no-create turns it off again.

Upgrade: pip install -U garmin-intervals-bridge (Windows: py -m pip install --upgrade garmin-intervals-bridge) or pull the new image (futurewebat/garmin-intervals-bridge:0.7.1). Notes: release 0.7.1.

0.8.0 is out – no more editing files, and no more container terminal.

A few of you said the setup was the hard part (thanks @DarKen for spelling out where it hurts on a NAS). So:

  • On your own computer: install, then garmin-intervals-bridge setup (Windows: py -m garmin_intervals_bridge setup). It asks for your Intervals API key (and checks it), confirms your time zone, signs you in to Garmin (MFA works), offers gear, charts and ImReady4, shows a trial run that writes nothing – and on your “yes” starts the bridge in the background at every login, without a window (Windows Task Scheduler, a Mac LaunchAgent, a Linux user service). The Windows and Mac guides are three steps now.
  • A web page: garmin-intervals-bridge ui opens it in your browser – what the bridge did, Garmin sign-in, settings, gear and chart plans with a confirm button, the log.
  • On a NAS: the same page runs next to the bridge (BRIDGE_UI_PORT + a password of your own), so the Garmin sign-in with MFA happens in the browser – no container terminal. Synology now gets a project file to paste instead of clicking a container together, and there’s an Unraid template (new – reports welcome).
  • It tells you when it needs you: when Garmin ends the saved session (or something has been failing for a day), you get one note in your Intervals calendar – and, if you like, an e-mail (sent from your own mailbox) and/or a push message on your phone via the free ntfy app. setup sets both up and sends a test message. And once a day the bridge checks whether a new version is out and says so in its log.

Already running it? Nothing to change. On Windows/Mac you can run setup once in your bridge folder – it takes over your settings, Garmin session and data and swaps the hand-made logon task for the windowless one.

Notes: release 0.8.0.

The speed of development of this is rivaling @david :wink:

1 Like

hahaha – careful, @david might be reading along :grinning_face_with_smiling_eyes:
I’d never dare to measure myself against the master himself: he ships features for an entire training platform faster than most of us can read the changelog – I’m just building a small bridge to it. And the bridge only exists because there’s something this good on the other side worth bridging to.

Should my own HRV advice say “Rest” tomorrow, though… we all know why. :wink::rofl:

0.9.0 is out – swap mode: Garmin’s filtered copy out, the original in.

Why: the bridge so far completes the activity Garmin’s import created. But Intervals’ “Reprocess file” re-reads Garmin’s filtered copy and drops everything the bridge added (thanks @DarKen for finding that one – enrich <id> --apply puts it back since 0.8.1).

Swap mode fixes that at the root: Garmin’s import stays on and acts as the trigger. Within a minute the bridge uploads your device’s original file, carries over what you may already have set (name, paired workout, RPE, feel, gear, tags …), and then deletes the copy Garmin’s import created in Intervals (nothing on Garmin Connect is touched). Intervals then holds the complete file – every custom field and stream is computed by Intervals itself, “Reprocess file” keeps everything, and it’s your own upload rather than Garmin-sourced data. Tested on my own account: all streams and Garmin fields from the original, same training load, and all of it still there after a reprocess.

It’s off by default, because it’s the one place the bridge deletes something – carefully: only Garmin’s own import, only within 48 hours, never an activity with comments or hand-edited intervals, and always upload first, delete last. The catch: the activity gets a new id. Switch it on with BRIDGE_SWAP=true, in setup, or on the web page. swap <id> --apply does it for a single (also older) activity. Details: README – Modes.

The cleanest solution would be uploading the original into the existing activity – no new id, nothing to delete. I’ve asked David for exactly that (plus a “new activity” webhook for API-key users): API: upload a file onto an existing activity, and a "new activity" webhook for API-key users – if you’d find it useful too, a :heart: and an upvote (the arrow at the top of the topic) over there help.

Upgrade: pip install -U garmin-intervals-bridge (Windows: py -m pip install --upgrade garmin-intervals-bridge) or pull the image (futurewebat/garmin-intervals-bridge:0.9.0). Notes: release 0.9.0.

2 Likes

@Andreas_Schnederle-W

If I understand the mechanism correctly, the bridge polls Intervals.icu every minute for new activities. Once found, it attempts to contact Garmin and fetch the missing fields.

It would be great to have an easy way to configure a time window for when the bridge queries Intervals.icu at one-minute intervals (for example, only between 10 AM and 6 PM). Additionally, I could imagine extending the polling interval itself—for instance, from one to five minutes. Unfortunately, I don’t see an easy way to do this at the moment other than modifying the code directly or using crone etc.

nice work!

1 Like

Thanks! You got it right: one tiny request to Intervals per minute (about 450 bytes – just the newest activity ids), and Garmin only gets asked once something new shows up.

Both are settings now (0.9.1):

  • BRIDGE_POLL_SECONDS=300 – poll every 5 minutes (this was already possible as run --apply --poll-seconds 300)
  • BRIDGE_POLL_HOURS=10-18 – poll only then; several ranges and overnight work too (6-9,16-22, 22-6)

Outside the hours the minute polling pauses, while the half-hourly full run keeps going – so an activity recorded outside the window still arrives, within about two hours instead of a minute, and last night’s wellness comes in as usual.

The real end of polling would be a webhook from Intervals – that’s the second wish in my feature request: API: upload a file onto an existing activity, and a "new activity" webhook for API-key users. It wouldn’t even need a server reachable from the internet: Intervals could post to a private ntfy topic, and the bridge just listens there – so it would work on a laptop at home too. A :heart: and an upvote over there help :slightly_smiling_face:

To clarify the phrasing here… Deletes the Garmin API copy from Intervals as opposed to deletes Garmin’s copy which sounds like deleting from Garmin Connect?

Good point – yes: the copy in Intervals that Garmin’s import created. Nothing on Garmin Connect is ever touched: the bridge only reads from Garmin (the original file, the activity summary, wellness) and never changes or deletes anything there. I’ve edited the post to make that clear.

If I have Homebrew Python on MacOS, what is best command for installation? pip install garmin-intervals-bridge gives me a lot of warnings about installing a Python library not in Homebrew. Thanks!

1 Like

Love the plugin so far! One issue i’ve encountered is i haven’t been able to backfill these fields, am i doing something wrong? I used the commands you’ve been listing as examples (I am not as code-savvy as some) but expanded the dates out.

Garmin Bridge: Endurance & hill scores
Garmin Bridge: Race predictions 5K / 10K
Garmin Bridge: Race predictions half / marathon		

I noticed yours worked in your screenshot though! Mine only pulled the last four entries/days? Not sure which of the two, i run every day.

Cheers!

1 Like

Homebrew’s Python doesn’t want packages installed into it directly – that’s the warning. Give the bridge its own little environment instead; that’s what the Mac guide does, and it works the same with Homebrew’s Python:

mkdir -p ~/GarminBridge && cd ~/GarminBridge
python3 -m venv venv
venv/bin/pip install garmin-intervals-bridge
venv/bin/garmin-intervals-bridge setup

(If you prefer pipx: brew install pipx, then pipx install garmin-intervals-bridge – after that garmin-intervals-bridge works from anywhere.)

One Homebrew caveat: when brew moves Python to a new version (3.13 → 3.14) and removes the old one, that environment can stop working. Then just repeat the venv and pip lines (or pipx reinstall-all) and run venv/bin/garmin-intervals-bridge autostart once – your settings and data in ~/GarminBridge stay.

A backfill reads only the per-day measurements by default (sleep, HRV, stress, Body Battery … – twelve requests per day) to keep a long backfill short; endurance & hill scores and race predictions aren’t among them. Your daily runs filled the last few days, which is why only those show up.

To get their history, fetch just those three – also for the days already done:

garmin-intervals-bridge backfill --scope wellness --from 2025-01-01 --endpoints endurance_score,hill_score,race_predictions --force-wellness --pause 3 --apply

--force-wellness includes days fetched before; it’s three requests per day, so a year takes about an hour. Only empty values are filled, nothing else changes. If the bridge runs in the background, stop it first (Windows/Mac: autostart --remove, afterwards autostart; in a container: stop it and run the command in a one-off container with the same data folder).

1 Like

Great tool, thanks @Andreas_Schnederle-W! I set it up under Linux with default values. It’s running as a systemd service (checked with systemctl) In the UI under Status I get the following:

Checks

  • last 24 h: 43 runs, 3 activities enriched, 2 wellness days written, 2159 Garmin requests

That seems a lot of requests for 43 minutes to me. At the moment of typing it seems to have settled down a bit but still does 2 Garmin requests per minute. I can’t explain what’s happening / has happened here, and yes I read garmin-intervals-bridge/deploy/README.md at main · futureweb/garmin-intervals-bridge · GitHub. How should I interpret this value? I stopped the bridge for now as this seems abnormal behaviour.

1 Like

@Jeroen_Doesburg Thanks for the report – and good catch: that number is wrong, but the requests aren’t. When the bridge runs as one process (run, as the autostart or a container does), the minute-by-minute checks share the Garmin session of the last full run – and each of them recorded that session’s whole request count again. So one full run with ~50 requests followed by 42 checks showed up as 43 × 50 ≈ 2,150; and after a full run that only needed the sign-in (2 requests), the sum grew by 2 every minute. Fixed in the next release.

What the bridge really asks Garmin: on its first run the originals of the last few days’ activities and three days of wellness – a few dozen to about 150 requests. After that, Garmin only gets asked when something new shows up: a new activity, last night’s values once your watch has synced, a day’s final read – typically 50–150 a day. The minute-by-minute check itself asks only Intervals (one tiny request) and Garmin not at all; the real count per full run is in the log (Full run: … N Garmin requests).

So it’s safe to start it again – sorry for the scare!

1 Like