Layout
Health Access/ # the folder you picked, any name index.json # one row per workout, newest first; the app rewrites it after every export briefing.md # one page any AI can read workouts/ 2026-05-14_0712_PoolSwim_7A1C3E90.json # <local date_time>_<Activity>_<first 8 of the HealthKit UUID> 2026-05-12_0630_IndoorRow_3F8B2D41.json vitals/ 2026-05-14.json # one per day, rewritten when values change days/ 2026-05-14.json # one per day, every Health group (v0.4; vitals/ stays for one release) catalog.json # which Health types have data, and where each was exported (v0.4) profile.json # age, birth year, sex, blood type, move mode (v0.4) plans/ 2026-05-13T19:02:11Z-s12.json # written by your AI (push_plan) or the CLI 2026-05-13T19:02:11Z-s12.status.json # written by the app: did it reach the Watch mcp/clients/ <id>.json # presence file written by the MCP: client, machine, counts
Who writes what
| Path | Written by | Read by |
|---|---|---|
| workouts/*.json | The app, once per workout. Never overwritten by export. The MCP or CLI may update exactly two keys, effort and feel, through rate_workout. | MCP, CLI, your AI, you |
| vitals/*.json | The app, today and yesterday on every run; 30-day backfill from Settings. | MCP, CLI, your AI |
| days/*.json, catalog.json, profile.json | The app, from v0.4: every Health group per day, the list of types with data and where each was exported, the athlete's characteristics. | MCP, CLI (healthaccess day, catalog, profile; doctor checks the coverage), your AI |
| index.json | The app, after every export and every rating. The MCP updates a row's effort after rate_workout, never adds rows. | MCP (for listing), CLI |
| plans/<id>.json | The MCP (push_plan) or the CLI (healthaccess plan push). Your AI can also write it directly. | The app |
| plans/<id>.status.json | The app, after "Put on Watch". | MCP (get_plan_status, list_plans) |
| briefing.md | The app, after every sync. | Any AI, a coach, you |
| mcp/clients/<id>.json | The MCP, on start and after tool calls, at most once a minute: client name and version, machine name, call count, last tool name. Never arguments or results. | The app, to show "Connected · Claude Code on Alex's MacBook Pro · last seen 2 min ago" |
Nothing else writes. Nothing ever deletes: a duplicate workout (same start within 2 seconds, same source) stays on disk and is left out of index.json. All JSON is UTF-8 with sorted keys; all timestamps are ISO 8601 in UTC with a Z; the local time is in the file name and in metadata.HKTimeZone.
An example workout, trimmed
This is workouts/2026-05-14_0712_PoolSwim_7A1C3E90.json from the synthetic example folder in the repository (generated by a script, not a real person): a 1,200 m pool swim, swum from the plan S12 on the plans page. The full file has 24 steps, 24 lengths and 431 heart-rate samples; here three steps, two lengths and three samples stand in for them. Everything else is as written; keys are sorted on disk.
{
"schema": "healthaccess.workout.v1",
"healthaccess_version": "0.4.0",
"id": "7A1C3E90-5B2D-4F61-9C8E-2A7D40B6E1F3",
"activity": "pool_swim",
"source": "Apple Watch",
"start": "2026-05-14T05:12:04Z",
"end": "2026-05-14T05:48:10Z",
"duration_s": 2166,
"distance_m": 1200,
"active_kcal": 338.6,
"avg_hr": 129,
"max_hr": 162,
"pool_length_m": 50,
"plan_ref": {"plan_id": "2026-05-13T19:02:11Z-s12", "name": "S12"},
"steps": [
{"key": "0.0.0", "block": 0, "rep": 0, "step": 0, "kind": "warmup", "label": "FR easy, find your breathing",
"start": "2026-05-14T05:12:04Z", "end": "2026-05-14T05:16:43Z", "duration_s": 279,
"distance_m": 200, "avg_hr": 100, "pace_s_per_100m": 139.5, "goal_met": true},
{"key": "1.0.0", "block": 1, "rep": 0, "step": 0, "kind": "work", "label": "DRILL 6-3-6: 6 kicks side, 3 strokes",
"start": "2026-05-14T05:16:43Z", "end": "2026-05-14T05:18:08Z", "duration_s": 85,
"distance_m": 50, "avg_hr": 125, "pace_s_per_100m": 170.0, "goal_met": true},
{"key": "2.3.0", "block": 2, "rep": 3, "step": 0, "kind": "work", "label": "FR 100 steady, breathe every 3",
"start": "2026-05-14T05:34:44Z", "end": "2026-05-14T05:36:56Z", "duration_s": 132,
"distance_m": 100, "avg_hr": 151, "pace_s_per_100m": 132.0, "goal_met": true}
],
"laps": [
{"start": "2026-05-14T05:12:07Z", "end": "2026-05-14T05:13:14Z", "duration_s": 67, "stroke": "freestyle", "swolf": 88.0},
{"start": "2026-05-14T05:13:14Z", "end": "2026-05-14T05:14:22Z", "duration_s": 68, "stroke": "freestyle", "swolf": 90.0}
],
"hr_series": [["2026-05-14T05:12:05Z", 84], ["2026-05-14T05:12:10Z", 85], ["2026-05-14T05:48:06Z", 115]],
"recovery_hr": {"end": 115, "1min": 104, "2min": 96},
"effort": 6,
"effort_source": "apple",
"effort_estimated": 5,
"feel": null,
"metadata": {"HKAverageMETs": "6.84 kcal/hr·kg", "HKIndoorWorkout": "0", "HKLapLength": "50 m", "HKSwimmingLocationType": "1", "HKTimeZone": "Europe/Berlin"}
}
What the fields mean
- activity
pool_swim,open_water_swim,indoor_row,run,walk,cycle, orother:<HealthKit raw type>.- zones
- Not written any more. Zone seconds are computed by the MCP and the command line from
hr_series, with the four upper bounds fromzone_boundsin~/.config/healthaccess/config.tomlor, without them, an estimate from the age inprofile.json(max heart rate 208 minus 0.7 times age; zones end at 72, 79, 86 and 92 percent of it). Every zone result says which it used (bounds_source). Files from before v0.4 may still carry app-computedzones; they are accepted. - steps
- One per interval of a structured workout.
keyis Apple'sblock.rep.step, with0.0.0the warm-up and a trailing single step the cool-down.labelis the step name from the plan your AI wrote; Apple drops step names on the way to HealthKit, so the app joins them back from the matching plan. A free workout hassteps: []. - laps
- One per pool length: stroke (freestyle, backstroke, breaststroke, butterfly, mixed, kickboard) and SWOLF.
- hr_series
- Heart rate about every 5 seconds, at least 4 seconds apart. The MCP leaves it out unless asked.
- recovery_hr
- Heart rate at the end, one minute and two minutes after.
nullright after a background export; filled in on the next run. - effort, feel
- Your rating, 1 to 10, and a line of text. The rating comes from your Watch, which asks after a workout; Apple exports it and the file says
effort_source: apple. The app never asks.rate_workoutwritesaiafter you told the AI a number; files from older builds may sayapp.effort_estimatedis Apple's own estimate and is never used as your effort. - plan_ref
- The plan this session was started from, matched by the WorkoutKit id the app recorded, else by sport and scheduled time within two hours.
nullfor a workout started on the Watch by hand. - metadata
- Apple's raw workout metadata as strings.
HKLapLengthis where the pool length comes from.
Vitals, one file per day
{
"schema": "healthaccess.vitals.v1",
"date": "2026-05-14",
"resting_hr": 54,
"hrv_sdnn_ms": 58.3,
"sleep_s": 26160,
"body_mass_kg": 78.4,
"steps": 9412,
"vo2max": 44.1,
"bp": null
}
Every field can be null; a day on which only steps were recorded gives a file full of nulls. Sleep is credited to the morning it ended and summed from the one source with the most sleep, so Watch and iPhone are not double-counted. HRV is the day's average; resting heart rate, weight, VO2max and blood pressure are the day's latest. A day with no values gets no file.
days/, one file per day with everything v0.4
From v0.4 the app writes days/<date>.json next to vitals/, with every Health group that has data that day: activity (the rings, steps, distances, energy, exercise and stand minutes, daylight), heart, respiratory, sleep (stages and sessions), body, blood, mobility, hearing, nutrition, mind, symptoms, cycle, medications, hygiene, uv, wheelchair. A group is present only when it has data; inside a present group, absent values are an explicit null. The example day, trimmed to three groups:
{
"schema": "healthaccess.day.v1",
"date": "2026-05-14",
"activity": {
"rings": {
"exercise_min": {
"actual": 47,
"goal": 30
},
"move_kcal": {
"actual": 712.3,
"goal": 650
},
"move_mode": "active_energy",
"stand_hours": {
"actual": 13,
"goal": 12
}
},
"steps": 9412,
"distance_walking_running_m": 7180.4,
"distance_swimming_m": 1200,
"active_energy_kcal": 712.3,
"exercise_min": 47,
"time_in_daylight_min": 63
},
"heart": {
"resting_hr": 54,
"hr": {
"avg": 72,
"max": 162,
"min": 47,
"samples": 1184
},
"hrv_sdnn_ms": 58.3,
"vo2max": 44.1
},
"sleep": {
"asleep_s": 26160,
"deep_s": 4980,
"rem_s": 5760
}
}
Two more files come with it. catalog.json lists every HealthKit type the app knows, with has_data, the sample count over 90 days, first and last date, and exported_in (workouts, days, ecg, profile or none), so healthaccess doctor can say "a type with data was exported nowhere" the day it happens. profile.json carries age and birth year (not the birthday), biological sex, blood type, wheelchair use, move mode and skin type. The full contract: docs/export-coverage.md.
index.json
{
"schema": "healthaccess.index.v1",
"updated": "2026-05-14T06:03:17Z",
"workouts": [
{"id": "7A1C3E90-5B2D-4F61-9C8E-2A7D40B6E1F3", "file": "workouts/2026-05-14_0712_PoolSwim_7A1C3E90.json",
"activity": "pool_swim", "start": "2026-05-14T05:12:04Z", "duration_s": 2166, "distance_m": 1200,
"avg_hr": 129, "effort": 6, "effort_source": "apple", "plan_name": "S12", "source": "Apple Watch"},
{"id": "3F8B2D41-9E6C-4A17-B2D8-5C3E7F9A1B04", "file": "workouts/2026-05-12_0630_IndoorRow_3F8B2D41.json",
"activity": "indoor_row", "start": "2026-05-12T04:30:11Z", "duration_s": 1680, "distance_m": null,
"avg_hr": 137, "effort": 7, "effort_source": "apple", "plan_name": "R4", "source": "Apple Watch"}
]
}
Newest first, no series. The MCP lists from the index and opens a workout file only on demand. If the index lags behind the files, which happens, ids are also resolved by scanning workouts/.
Plans and their status
Your AI writes plans/<id>.json; the app answers with plans/<id>.status.json. The format, the rules and the "Put on Watch" flow have their own page. The status file for the plan behind the example swim, as the app wrote it after WorkoutKit scheduled the session for the next morning:
{
"plan_id": "2026-05-13T19:02:11Z-s12",
"state": "scheduled",
"at": "2026-05-13T19:04:40Z",
"method": "schedule",
"workoutkit_id": "D4E7A2C1-6F3B-4E8A-9B5C-1A2F3E4D5C6B",
"scheduled_for": "2026-05-14T05:00:00Z",
"error": null
}
mcp/clients/<id>.json
The one file the MCP writes that is not a plan: a presence note, so the app can show that an AI is connected. Written on server start and after tool calls, at most once a minute, one file per machine and client, overwritten in place. It never contains tool arguments or results.
{
"schema": "healthaccess.client.v1",
"id": "9f3c1a7e5b2d4c80",
"client": {
"name": "Claude Code",
"version": "2.1.0"
},
"machine": "Alex's MacBook Pro",
"mcp_version": "0.2.0",
"first_seen": "2026-05-09T17:20:41Z",
"last_seen": "2026-05-14T06:11:02Z",
"last_tool": "list_workouts",
"calls": 123
}
The app shows "Connected · Claude Code on Alex's MacBook Pro · last seen 2 min ago" when any file in the folder is newer than 7 days, else "Not connected yet".
briefing.md
One page of Markdown the app rewrites after every sync, so an AI without the MCP (ChatGPT, a Claude Project, a local model, a human coach) still gets the picture. Sections, in this order: the title with the athlete's name and the time written; Last 4 weeks, one line per ISO week with sessions, distance, time and mean effort; Recent sessions, newest first, at most ten, one line each; Vitals, the latest resting heart rate, HRV, sleep and weight with their dates and the 21-day direction; Next session, the newest plan not yet on the Watch; Open, unrated sessions and plans waiting for a tap; Limits, the two fixed sentences about unlocking and pool length. No charts, no JSON, under 60 lines. An example: examples/briefing.md.
Files from before the rename
The project was called "Tide" until 2026-10-25. Files written before that carry "schema": "tide.workout.v1" (or tide.workout.v0, the first format), tide.vitals.v1, tide.index.v1, tide.plan.v1, and the key tide_version instead of healthaccess_version. Every reader accepts both for good; nothing is rewritten just to change a tag. The same goes for the environment variable TIDE_DIR and the resource URIs tide://index and tide://workout/<id>.
The complete contract, with every field and the deviations the app documents, is docs/schema.md in the repository.