Inspiration

I'm a runner, but I wanted one plan that covered running, strength and cycling together — not three plans that ignore each other. Nothing did that, so at Path Variable we built OmniCoach: a training platform that plans the whole week across all three disciplines, reads your recovery each morning, and pushes the workouts straight to your Garmin.

OmniCoach still has the blind spot every training platform has. It knows everything about your body and nothing about your life. It has never seen your calendar.

So every week I did the same reconciliation by hand. A Saturday flight appears, the long run has to move, and you guess whether to shorten it or drop it — and whether moving it wrecks the Tuesday session it now sits beside.

What I actually wanted to know was whether I'd trust an agent to write to my real training calendar, unsupervised. Cadence is what we built to answer that.

What it does

A multi-sport week is a constraint problem. Sports compete for one time budget, they interfere with each other physiologically, some sessions are immovable, and readiness changes overnight and invalidates the plan.

Every morning at 08:00, without being asked, Cadence:

  1. Reads immovable commitments from Google Calendar
  2. Reads the plan, recent load and this morning's readiness from OmniCoach
  3. Detects conflicts deterministically against seven interference rules
  4. Asks Gemini 3.5 Flash to choose between valid resolutions and explain itself
  5. Writes the corrected schedule back to Garmin
  6. Records every decision, with the rule that fired, to an auditable ledger

The seven rules encode how the disciplines interfere:

Rule Constraint
R1 Lower-body strength needs a clear day between it and a hard run
R2 A run and lower-body strength on one day; each blunts the other
R3 Consecutive hard days only when readiness supports them
R4 Long run adjacent to long ride is a build-week pattern, not a tired one
R5 (remedy) An easy ride preserves aerobic volume at lower impact cost
R6 Readiness below the gate caps the day's planned intensity
R7 Calendar commitments are fixed; sessions flex around them

How we built it

Detection is deterministic; the model only chooses. The domain/ package finds what is wrong with a week in plain Python and imports nothing outside the standard library — no SDK, no HTTP client, no credentials. Gemini is invited afterwards to pick between resolutions that already pass the rules. It cannot invent one, and if its answer doesn't improve the week against the same deterministic scoring, the proposal is refused and the deterministic fallback stands. That has happened on four of ten production runs, and it is visible in the public ledger.

That split is why the entire reasoning core runs under pytest in three seconds with no network and no credentials — 357 tests.

Google services used:

Service Role
Cloud Run Hosts the scheduled agent and the public sandbox
Cloud Scheduler Fires the daily reconciliation (OIDC-authenticated)
Firestore Decision ledger and the idempotency watermark
Secret Manager OmniCoach bearer token, mTLS certificate, calendar service account
Gemini 3.5 Flash Resolution and rationale
Gemma 3 Classifying workout names the rules cannot place
Google Calendar Immovable commitments (read-only)

Why gemini-3.5-flash: every Gemini model at 3.5 or newer is Flash-tier — the Pro line stops at gemini-3.1-pro-preview, which predates the requirement. gemini-3.7-flash exists but returned 503 high demand under free-tier load, which is not a dependency worth taking for a scheduled job.

The Gemma tier exists because the deterministic classifier returns MODERATE for workout names it cannot place — a safe default, not a good one. Gemma is asked about exactly those cases and nothing else; a reply outside {EASY, MODERATE, HARD} is discarded, and any failure returns the deterministic answer unchanged. Same shape as the main path, one tier down: rules decide what is true, models are asked only where the rules have nothing to say.

Reaching Garmin: Cadence never touches Garmin directly. It calls OmniCoach's REST API over mutual TLS with a client certificate, and OmniCoach owns the Garmin Training API integration. The agent is IAM-protected on Cloud Run and not publicly reachable; only the read-only sandbox is.

Challenges we ran into

Garmin issues a new schedule id on every reschedule. Rollback tracked the original id, so a failed second write left the first one stranded. Fixed by carrying the effective id forward from each reschedule response.

A swap is invisible in Garmin's month view. Two sessions exchanging days leave the grid pixel-identical, because the calendar draws one bar per non-empty day. This surfaced only while reviewing demo footage, and it changed how the scenario was designed rather than how the code works.

Cloud Run silently swallows /healthz — its frontend 404s that path before the container sees it, with nothing in the logs.

--allow-unauthenticated silently no-ops under domain-restricted sharing. The deploy reports success and the service stays private.

The write path took nine review rounds. More than once the defect was the previous round's fix — a parameter threaded but not connected at the call site, twice. The decision and fallback logic is genuinely subtle, and the tests that now cover it exist because of those rounds.

Accomplishments

It has run unattended every morning since 23 August, and there is a ledger to prove it rather than a screenshot. Ten recorded passes, four with writes applied to a real Garmin account, writes_planned == writes_applied every time, zero write errors.

It refuses its own model. Four of those ten passes rejected Gemini's proposal because it didn't improve the week, and fell back to the deterministic resolution. That is the quality gate working, in public.

It refuses to rewrite the past. A session already underway or past is never modified, even when a rule says it should be — the guard fires before the write and says so in the run record.

What we learned

The interesting engineering was in deciding what the model is not allowed to do. Every failure mode came from giving reasoning to a place that should have had a rule, and every fix moved a decision back into deterministic code while leaving the model the genuinely subjective choice — which of several valid resolutions a tired athlete would actually prefer, and how to say why.

What's next

Cadence is a scheduling brain that OmniCoach doesn't yet have, and the natural path is to fold it in as a feature of the platform rather than a companion to it. Three known limitations are filed as issues rather than hidden:

  • #35 — the rules are blind to training that already happened, so R1 cannot see the squats it exists to protect against
  • #37 — the idempotency watermark fingerprints volatile Garmin schedule ids, so it can never match after a reschedule
  • #34 — the demo path has no near-side window bound

Built With

  • cloud-scheduler
  • docker
  • fastapi
  • firestore
  • garmin-connect
  • gemini
  • gemma
  • google-adk
  • google-calendar-api
  • google-cloud-run
  • mtls
  • nginx
  • pytest
  • python
  • secret-manager
Share this project:

Updates

Submission history