Free daily AI brief from your Garmin data (Gemini and GitHub Actions) A free, fully automated pipeline now pulls daily health data from Garmin Connect into a SQLite database, computes percentiles and trends across a user's entire history, and delivers a short coached brief via ntfy, Telegram, or email. The system runs on GitHub Actions with no server to maintain, using the free tiers of Garmin's API, Google's Gemini, and GitHub to keep costs at zero. Users can set up the pipeline in minutes by cloning a public repository, running a backfill script, and configuring notification secrets. Free daily AI brief on your Garmin data — full-history stats, Gemini insight, push to your phone. $0 to run. Fully automated pipeline: pull Garmin Connect into SQLite, compute percentiles and trends over your whole history, and deliver a short coached brief via ntfy, Telegram, or email. Runs on GitHub Actions; no server to maintain. Example notification layout — your numbers and wording change daily. WATCH • Stress 34 vs 30d avg 23 — worsening • Sleep 6.15h — under 7h target WINS • Intensity 63 min — all-time high • Resting HR improving, 2-day streak near ATL TODAY In bed 30 minutes earlier tonight. — Poor sleep raises cortisol and daily stress. Cost: $0 Garmin via garminconnect , Gemini free tier, GitHub Actions, ntfy/Telegram/email . python3.12 -m venv .venv source .venv/bin/activate pip install -r requirements.txt python scripts/mint token.py Use your Garmin email, password, and MFA code. Tokens are saved to ~/.garminconnect valid ~1 year . BACKFILL DAYS=180 python -m src.backfill Creates garmin.db . Commit it to your repo after backfill completes re-run backfill after schema changes to refresh columns . Package tokens for CI: bash scripts/pack tokens.sh Copy the single long line of output into repository secret GARMIN TOKENS B64 no quotes, no spaces, no line breaks in the secret value . If Actions fails with base64: invalid input , the secret was pasted wrong — re-run pack tokens.sh and replace the secret entirely. Manual alternative: tar -cf tokens.tar -C ~/.garminconnect . base64 < tokens.tar | tr -d '\n\r ' paste that one line into GARMIN TOKENS B64 rm -f tokens.tar | Secret | Purpose | |---|---| GARMIN TOKENS B64 | base64 tar of ~/.garminconnect | GEMINI API KEY | | NOTIFIER ntfy default , telegram , or email NTFY TOPIC TELEGRAM BOT TOKEN / TELEGRAM CHAT ID EMAIL USER / EMAIL APP PASSWORD / EMAIL TO ntfy default : Install the ntfy app https://ntfy.sh , subscribe to a long random topic, set NTFY TOPIC . Telegram: Create a bot via @BotFather, get your chat id. Email: Gmail app password not account password . cp .env.example .env fill in secrets python -m src.main Create a GitHub repo, commit everything including garmin.db , add secrets, run workflow dispatch on garmin-daily once to verify CI. The repo can be public if you accept that garmin.db exposes daily health numbers see below . Only scalar daily wellness metrics are stored. There is no raw JSON , no GPS, no activity routes, no maps, and no activity list/workout details. | Stored | Not stored | |---|---| | Steps, resting HR, sleep duration/score/stages , stress, Body Battery high/low, HRV, training readiness, intensity minutes, active kcal | Location, lat/long, track polyline | SpO2, VO2 max, Garmin fitness age, weight g , body fat % | Activity names, routes, timestamps per lap | Date YYYY-MM-DD only | Full API responses, heart-rate streams | Fitness age is Garmin’s estimated fitness age a single number , not your birthdate or home address. python -m src.main — pull recent days → digest → Gemini brief → notification. GitHub Actions runs at 13:00 UTC ~8am US Eastern , commits updated garmin.db , and pushes. Cron drift: Scheduled runs may start a few minutes late; fine for a daily brief. Workflow auto-disable: Repos with no commits for 60 days disable scheduled workflows. Committing garmin.db each run prevents this. Token expiry: Re-run scripts/mint token.py when Actions fail auth ~yearly . Gemini privacy: Free tier may use inputs for training. The digest contains only aggregated numbers — no names or emails. Rate limits: One Gemini call/day; flash → flash-lite fallback on 429. Thin data: Metrics with fewer than 14 days skip percentile/record claims. NULL: Missing Garmin fields are stored as NULL, never zero. Gemini quota 0: If you see limit: 0 429 errors, link a billing account on the GCP project tied to your API key still $0 for one call/day within free tier . src/ config.py env, metrics registry, goals db.py SQLite garmin client.py pull.py / backfill.py features.py statistics digest insight.py Gemini brief notify.py main.py scripts/mint token.py .github/workflows/daily.yml docs/sample-notification.png