Garmin Coach
A bridge between an LLM and a Garmin watch: paste a training plan, get structured workouts on your wrist. No manual re-entry.
- ~2k
- Lines of code
- Automated
- Token refresh
- MIT
- License
Challenge
I plan my running weeks with Claude in a separate chat. The output is plain text, and the path from there to my Garmin was manual: read the prose, open the app, tap through every interval by hand. I did it often enough that it stopped being a quirk and started being annoying.
The data shape isn't hard: Garmin's internal API exposes a workout format. The gap was that nobody built the bridge, and getting an LLM to fill that JSON without inventing field names meant the schema itself had to be the first thing I got right.
Approach
I started with the JSON contract: warmup, interval, recovery, cooldown, repeat, each with a typed duration and target. Strict, but short enough to paste into the prompt. Once the schema was stable the CLI followed — it parses the JSON with Pydantic and calls the API. The web app came next, mobile-first on FastAPI, so I can push a plan from my phone.
The last real piece was token refresh. Garmin's OAuth tokens expire in minutes, so I keep one row in Supabase that refreshes itself. The app just works when I pick up my phone mid-run-prep.
Outcome
Live as a Vercel-hosted web app, with the CLI running locally. The whole pipeline (plan in Claude, JSON out, push to a Forerunner 165) is about 2k lines. I'm not training as much as I was in early 2026, but the infrastructure is stable and I reach for it whenever I'm back to structured runs.
Who I designed for
Myself, and no one else. I run with a Garmin, I plan with Claude, and I didn't want to re-enter intervals by hand.
Building a tool you're the only user of is a different discipline. No research phase, no personas, and the feedback loop is ten minutes long. If the schema is annoying to prompt against, I feel it right away. That compression is the whole point of a side project.
The key decision: schema first
The temptation was to start with the API or the UI. I started with the JSON schema, and that was the right call.
An LLM has to fill the schema without a field-by-field explanation every time, so the shape had to be obvious: predictable keys, typed targets, no ambiguous nesting. I iterated it in a single markdown file, pasting it into Claude and refining until the output came back valid with no correction needed. After that the rest was plumbing. The parser is just Pydantic validation, and swapping the model is a config change.
What didn't go well
The serverless deploy fought me. The Garmin client library depends on curl_cffi, which doesn't compile on Vercel, so I run two paths: the full library on the CLI, and direct requests with an exported token on the web. It works, but it's inelegant.
The auth is also a bit of a house of cards. It leans on an unofficial endpoint Garmin can change without notice. When the library I started on broke after Garmin added Cloudflare fingerprinting, I had to migrate mid-project. The calendar view on the web is still incomplete because the listing endpoint returns 404 there. It's a tool I use, not a product I'd hand to a stranger.
Selected screens
" Side projects let you ship the API you want to consume, not the one that survived a committee. The schema is exactly the shape I needed, the auth is exactly the amount I was willing to maintain, and the UI is as minimal as a tool I use alone can be. "
Mosey
→A personal travel app for 27 days across Denmark and Norway. Native iOS, anti-itinerary, planning generated by on-device AI.