Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Query Google Health API v4 — steps, heart rate, exercise, sleep, weight, SpO2, HRV, ECG, blood glucose, nutrition, and 40 total data types
.claude/skills/google-health-api-ghealth/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 199% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 214% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 67% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 97% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 71% | 0% |
CLI for the Google Health API v4. 40 verified data types.
Prerequisites: See ../ghealth-shared/SKILL.md for auth, setup, global flags.
| Goal | Operation | Example | |------|-----------|---------| | Daily totals (steps, distance, calories) | daily-rollup | ghealth data steps daily-rollup --from 2026-03-22 --to 2026-03-29 | | Individual readings (HR, weight, SpO2) | list | ghealth data heart-rate list --from today --limit 20 | | Sessions (exercise, sleep) | list | ghealth data exercise list --from 2026-03-01 | | Daily summaries (resting HR, HRV, resp rate) | list | ghealth data daily-resting-heart-rate list --from 2026-03-01 | | Merged multi-source data | reconcile | ghealth data weight reconcile --from 2026-01-01 |
Why this matters: steps list returns minute-level intervals without counts. Use daily-rollup to get actual step totals (countSum). Same for distance (millimetersSum) and floors.
Run ghealth schema types for the live version. Quick reference:
Use daily-rollup for totals:
steps → countSum per daydistance → millimetersSum per daytotal-calories → kcalSum per day (rollup-only)floors → countSum (rollup-only)active-minutes → (rollup-only)swim-lengths-data → strokeCountSum per daycalories-in-heart-rate-zone → caloriesInHeartRateZones per day (rollup-only)Use list for readings: heart-rate, weight (writable), body-fat (writable), height (writable), oxygen-saturation, heart-rate-variability, altitude, vo2-max, active-zone-minutes, activity-level, basal-energy-burned, active-energy-burned, blood-glucose, core-body-temperature, respiratory-rate-sleep-summary, run-vo2-max, sedentary-period, swim-lengths-data, hydration-log
Use list for sessions:
exercise (writable) — includes type, duration, calories, HR summary, notessleep (writable) — includes summary by default. Add --detail for per-stage breakdown.Cardiac (dedicated scopes, list-only):
electrocardiogram — waveform samples + rhythm classification. Requires ecg.readonly.irregular-rhythm-notification — alert windows. Requires irn.readonly.Nutrition:
nutrition-log — logged food entries with nutrient/energy breakdown (list, get, rollup, daily-rollup, reconcile)food, food-measurement-unit — reference catalogs (list, get only). No time filter — --from/--to are ignored.Daily summaries (one value per day, filter by date): daily-resting-heart-rate, daily-heart-rate-variability, daily-oxygen-saturation, daily-respiratory-rate, daily-vo2-max, daily-sleep-temperature-derivations
Get a single point by ID: get --id <id> is supported on exercise, sleep, weight, body-fat, height, hydration-log, nutrition-log, blood-glucose, core-body-temperature, food, food-measurement-unit.
These require judgment that --help and schema don't provide.
Get the user's timezone before querying date-sensitive data:
bashghealth user settings get # → timeZone: "Europe/London", utcOffset: "3600s" # Then use --from/--to with the correct local dates
This reports the account timezone for information only — to have the CLI resolve dates in that zone, set it explicitly with ghealth config set timezone <IANA zone>.
Sleep/exercise page size is capped at 25 per request (auto-paginated by CLI):
bashghealth data sleep list --limit 5 # CLI handles pagination internally
Paging through large list results. list returns up to --limit rows (default 500). When more exist, the response carries a nextPageToken and a hint. Pass it back with --page-token to fetch the next page — it resumes exactly where the last page ended, no rows skipped or repeated:
bashghealth data heart-rate list --from 2026-06-15 --limit 500 # → {"dataPoints":[…500…], "nextPageToken":"ABC", "_hints":[…]} ghealth data heart-rate list --from 2026-06-15 --limit 500 --page-token ABC # → next 500 rows
Correlate heart rate with exercise sessions:
bash# 1. Get exercise time window ghealth data exercise list --from today --limit 1 # → start: "2026-03-29T14:18:32+01:00", end: "2026-03-29T14:39:14+01:00" # 2. Query HR for that window using --filter (raw API syntax, UTC required) ghealth data heart-rate list --filter 'heart_rate.sample_time.physical_time >= "2026-03-29T13:18:32Z" AND heart_rate.sample_time.physical_time < "2026-03-29T13:40:00Z"'
Use -o <file> to write data to a file. When -o is set, stdout shows only a summary with the column schema — not the data itself. This means you can fetch data and immediately write analysis code using the column names from stdout, without reading the file.
bashghealth data steps daily-rollup --from 2026-03-24 --to 2026-03-30 --format csv -o steps.csv
What stdout shows (this is all the agent sees):
Wrote 6 rows to steps.csv
Columns: countSum, date
Preview:
countSum,date
4062,2026-03-29
9122,2026-03-28
2469,2026-03-27What the file contains (full CSV, not printed to stdout):
csvcountSum,date 4062,2026-03-29 9122,2026-03-28 2469,2026-03-27 6541,2026-03-26 4025,2026-03-25 3995,2026-03-24
The agent now knows the columns are countSum and date, and can write pd.read_csv("steps.csv") without ever reading the file.
Do not pipe to file — use -o instead. Piping (> file.csv) sends the full data to the file but prints nothing to stdout, so the agent has no column schema and must read the file to learn the structure.
More examples:
bash# Sleep — nested stageMinutes auto-flattened to stageMinutes.AWAKE, stageMinutes.DEEP, etc. ghealth data sleep list --from 2026-03-01 --format csv -o sleep.csv # Exercise — metricsSummary.caloriesKcal, metricsSummary.averageHeartRateBeatsPerMinute, etc. ghealth data exercise list --from 2026-03-01 --format csv -o exercise.csv # Heart rate — 500 readings straight to file ghealth data heart-rate list --from today --limit 500 --format csv -o hr.csv
Exercise time series (GPS/heart-rate track) → CSV. export-tcx --as csv flattens the TCX track to one row per trackpoint — pd.read_csv it directly instead of parsing TCX XML:
bash# Find the exercise id first, then export its track ghealth data exercise list --from 2026-06-01 --limit 10 ghealth data exercise export-tcx --id <id> --output ride.csv --as csv # or --output - for stdout
Columns (fixed, stable for dataframes): time, activity, lap, sport, latitude_deg, longitude_deg, altitude_m, distance_m, heart_rate_bpm, cadence_rpm, speed_mps, watts. Absent sensors are empty cells (NaN in pandas), never zeros. distance_m is cumulative. 0 rows = indoor/no-sensor activity (Google emits no track for those) — the session summary and workout notes come from data exercise list, not the track export.
Writable types: exercise, sleep, weight, body-fat, height. Writes are async (API returns Operation object).
Discover the correct payload format by inspecting a real response with --raw:
bashghealth data weight list --raw --limit 1 # Use the response structure as a template for your create payload
Write operations use create, update --id <id> [--update-mask fields], delete --ids <ids>.
altitude, distance, floors, steps, total-calories): a date absent from rollup output means the device wasn't worn / didn't sync — NOT zero. countSum: "0" is a true zero (worn, no activity). Never coalesce missing buckets to 0 or average over absent days as zeros — that silently deflates weekly/monthly statsint64 fields (beatsPerMinute, countSum, minutesAsleep) are strings; int32/double fields (weightGrams, caloriesKcal, percentage) are numbers--filter raw syntax: only >= and < comparators. Civil time fields (no Z): interval types use {type}.interval.civil_start_time, sleep uses sleep.interval.civil_end_time (only end-time is filterable), daily types use {type}.date. Physical time fields (with Z): sample types use {type}.sample_time.physical_timelist to verify persistencedelete returns HTTP 500 — this is an API bugupdate returns HTTP 400 ("updateMask not recognized") — API bug; use create + delete as a workarounddaily-rollup aggregates by civil/local day (1-day windows; override with --window-days N). rollup aggregates by physical time (--window-size, default 86400s); bare --from/--to dates anchor at midnight in the configured timezone (ghealth config set timezone <IANA zone>), falling back to machine-local time when unset. For local-day totals use daily-rollup. Both send their window size explicitly — the API rejects requests that omit it| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 7,254 | 5,076 | -30% | 1 | 1 | 0% | 1,284 | 3,833 | +199% | 0 | 0 | — |
case-02 | fail→fail | 4,884 | 8,093 | +66% | 1 | 1 | 0% | 273 | 3,108 | +1038% | 0 | 0 | — |
case-03 | fail→fail | 12,349 | 7,867 | -36% | 1 | 1 | 0% | 2,054 | 3,410 | +66% | 0 | 0 | — |
case-04 | fail→fail | 11,742 | 11,073 | -6% | 1 | 1 | 0% | 1,896 | 4,535 | +139% | 0 | 0 | — |
case-05 | pass→pass | 16,454 | 5,065 | -69% | 1 | 1 | 0% | 2,286 | 3,716 | +63% | 0 | 0 | — |
case-06 | pass→pass | 8,127 | 6,790 | -16% | 1 | 1 | 0% | 1,440 | 4,085 | +184% | 0 | 0 | — |
case-07 | fail→pass | 6,218 | 3,479 | -44% | 1 | 1 | 0% | 1,082 | 3,398 | +214% | 0 | 0 | — |
case-08 | fail→pass | 15,986 | 10,656 | -33% | 1 | 1 | 0% | 2,913 | 4,856 | +67% | 0 | 0 | — |
case-09 | fail→pass | 10,473 | 4,845 | -54% | 1 | 1 | 0% | 1,898 | 3,737 | +97% | 0 | 0 | — |
case-10 | fail→pass | 12,704 | 4,032 | -68% | 1 | 1 | 0% | 2,043 | 3,490 | +71% | 0 | 0 | — |
case-11 | fail→pass | 12,317 | 2,840 | -77% | 1 | 1 | 0% | 2,333 | 3,258 | +40% | 0 | 0 | — |
case-12 | pass→pass | 9,430 | 5,559 | -41% | 1 | 1 | 0% | 1,714 | 3,862 | +125% | 0 | 0 | — |
case-13 | pass→pass | 9,516 | 3,389 | -64% | 1 | 1 | 0% | 1,716 | 3,390 | +98% | 0 | 0 | — |
case-14 | pass→pass | 13,872 | 6,221 | -55% | 1 | 1 | 0% | 2,500 | 4,054 | +62% | 0 | 0 | — |
case-15 | fail→pass | 13,831 | 3,618 | -74% | 1 | 1 | 0% | 2,476 | 3,479 | +41% | 0 | 0 | — |
case-16 | pass→pass | 8,234 | 3,905 | -53% | 1 | 1 | 0% | 1,482 | 3,488 | +135% | 0 | 0 | — |
case-17 | fail→pass | 8,219 | 5,198 | -37% | 1 | 1 | 0% | 1,455 | 3,754 | +158% | 0 | 0 | — |
case-18 | fail→pass | 13,121 | 8,092 | -38% | 1 | 1 | 0% | 2,132 | 4,397 | +106% | 0 | 0 | — |
case-19 | fail→pass | 5,735 | 1,860 | -68% | 1 | 1 | 0% | 1,045 | 3,168 | +203% | 0 | 0 | — |
case-20 | fail→pass | 15,203 | 4,939 | -68% | 1 | 1 | 0% | 2,391 | 4,001 | +67% | 0 | 0 | — |
case-21 | fail→pass | 15,095 | 3,317 | -78% | 1 | 1 | 0% | 2,343 | 3,326 | +42% | 0 | 0 | — |
case-22 | fail→pass | 9,577 | 5,629 | -41% | 1 | 1 | 0% | 1,783 | 3,682 | +107% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 20 counted toward the lift figure. The other 2 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +59 percentage points is the difference between those two pass rates over the 20 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.