HealthKit to JSONL vs JSON: Line-Delimited Snapshots for a Mac Agent
Two apps can write the exact same health data to disk in two formats that behave nothing alike when a sync is interrupted. Here is what JSONL and JSON actually trade, with a measured benchmark, and where the HealthMirror app drops its JSONL files.
Quick answer. JSONL and JSON carry the same fields and roughly the same bytes. The difference is what a reader can do before the file is finished. A JSON array is only valid once the closing bracket lands, so a half-written snapshot parses as zero records. A JSONL snapshot is valid one line at a time, so a reader keeps every complete record it already has. Line-delimited wins for snapshots a phone writes in the background and a Mac agent reads at any moment. Whole-file JSON wins when the document is small and its shape matters more than its durability.
That is the failure mode you hit the morning the export did not finish, which is exactly the morning you wanted the agent to answer.
What the two formats really are
A JSON snapshot is one document, usually an array of objects. It is the shape most APIs hand you, and it is easy to validate because the whole schema is visible at once.
[
{"date":"2026-10-07","metric":"hrv_sdnn","value":54.2,"unit":"ms"},
{"date":"2026-10-07","metric":"resting_heart_rate","value":51,"unit":"bpm"}
]
JSONL, also called JSON Lines or newline-delimited JSON, is the same records with no array and no wrapping commas. One complete object per line:
{"date":"2026-10-07","metric":"hrv_sdnn","value":54.2,"unit":"ms"}
{"date":"2026-10-07","metric":"resting_heart_rate","value":51,"unit":"bpm"}
Strip the surrounding brackets and swap the commas for newlines and you have converted one into the other. That is the whole trick, and it is why the file sizes barely move.
The one difference that matters: an interrupted write
A background export writes while the phone is doing something else. iCloud copies the file to the Mac while it is still arriving. An agent wakes up and reads it. The question is what survives when the process stops in the middle.
I ran this on 2026-10-08 with a synthetic set of 3,200 daily records, 400 days across 8 metrics, written once as a JSON array and once as JSONL, then truncated both at 100,000 bytes to simulate a cut-off sync. The script is in the repo at scripts/bench_jsonl_vs_json.py if you want to rerun it; the read times are the best of five runs and land in the same range for both formats.
| Test (3,200 records, 2026-10-08) | JSON array | JSONL |
|---|---|---|
| File size | 236,069 bytes | 236,068 bytes |
| Size difference | 1 byte, or 0.0004 percent (a newline costs the same as a comma; the array pays one extra closing bracket) | |
| Read a whole file with jq | jq length, ~45 ms | jq length over lines, ~40 ms |
| Cut off at 100 KB | Parse error, 0 records recoverable | 1,355 records read cleanly |
Read the last row twice. The array did not lose one record, it lost all of them, because an unfinished array is not JSON at all. The JSONL reader lost only the single partial line at the cut. If your agent polls a folder while your phone syncs, that is the difference between a stale answer and no answer.
Four more places the line format earns its keep
- Append instead of rewrite. A new day is one appended line. JSON means rewriting the document, and a crash mid-rewrite can corrupt a file that was fine.
- Filter without loading.
jq -c 'select(.metric=="hrv_sdnn")' health.jsonltouches only what it needs, so a multi-year file does not become gigabytes of held memory. - Grep and tail work.
grep 2026-10-07 health.jsonlortail -n 30is a real diagnostic. Against a single-line JSON array both return the whole file. - Partial trust is explicit. An agent can honestly say "I read 1,355 lines and refused the 1,356th."
Where the HealthMirror app puts its JSONL
HealthMirror is the clearest example of a phone app that chose JSONL. Its App Store description says it plainly: it mirrors your health data into "plain-text JSONL" files in your own iCloud Drive, read-only, covering sleep with stages, steps, active energy, workouts, heart rate, HRV and weight.
The layout decides how an agent reaches the data. HealthMirror writes into its own iCloud container, which on a Mac resolves to a path under your Mobile Documents folder:
iPhone HealthKit
-> HealthMirror iOS app (reads HealthKit, writes JSONL)
-> iCloud.com.mlyz.HealthBridge
-> ~/Library/Mobile Documents/iCloud~com~mlyz~HealthBridge/Documents/raw/
-> the MCP server (reads the per-metric subfolders)
Files sit in per-metric subfolders under raw/. Its companion server, mlyxz/healthmirror-mcp, runs locally and exposes ping, get_sleep, get_metric, get_workouts and get_daily_summary; queries default to 30 days unless you pass allow_historical=true, every call lands in a local audit log, and a sandbox-exec profile denies it all network access.
The trade: JSONL is a flat dump of samples, so there is no schema across the files, no aggregates, and nothing that reports freshness for you. You are reading raw material.
Two formats, one folder. JSONL and JSON can live in the same directory. A server can read per-metric JSONL for the raw series and one cache file for the daily aggregates. The format is not a religion; the read pattern decides.
What MetricBridge writes
Our own export takes the whole-file route: a compact JSON daily cache, .health-cache.json, that the local MCP server reads from a folder you choose. Workouts, logged events, an opt-in profile, sleep sessions, cycle starts and a timezone-change log each get their own optional companion file, and an absent file is reported as available:false rather than passed off as zero data.
That shape is deliberate. One documented object is easier to validate, and the agent gets structure instead of a pile of samples: get_health_metrics returns daily values plus an aggregate, get_trends compares a window against the prior one, get_structured_export paginates clean JSON, and get_freshness flags the snapshot stale after 26 hours by default. If a sync has not landed, the agent says so instead of answering from yesterday.
The cost is the one every JSON array carries: a half-copied snapshot is not readable. Our answer is that the server reads a file the app finished writing, not a stream still in flight.
How to check any snapshot in five minutes
- Is it valid right now?
jq . yourfile.jsonfor JSON,jq -c . yourfile.jsonlfor JSONL. A parse error on the array is fatal; on JSONL it tells you which line to look at. - Is it complete? Compare the record count to the date range you expect. A count that stops two days ago is the real answer, whether or not the file parses.
- Is it fresh? Check the newest timestamp, not the file's modified time. A sync that stuck still updates nothing.
What we are not claiming
This compares file formats and read patterns, not fitness apps. JSONL is not "better" than JSON; it is better at surviving an unfinished write and at streaming, and worse at being one obviously-shaped document. And the product computes and describes from your own data. It does not diagnose, detect, treat, or monitor any condition, and where a number moves away from your recent range the language is deviation from your personal baseline. Wellness, not medical advice.
On-device answers, and a local server your agent can read
Ask your Apple Health data anything on the iPhone and see the numbers behind every answer, then export 190 metrics as local JSON to a zero-dependency MCP server. One purchase covers it all: $24.99 one-time, no subscription.
Frequently asked questions
What is the difference between JSONL and JSON for Apple Health data?
JSON keeps every record inside one array, so the file is only valid once it is fully written. JSONL puts one JSON object on each line, so the file is valid line by line and a reader can use whatever it has already. The bytes are nearly identical; the failure behaviour is not.
Does a JSONL snapshot survive an interrupted iCloud sync?
Yes, up to the last complete line. If a sync stops halfway, a JSON array fails to parse and a reader gets zero records, while JSONL still yields every line before the cut. In a test cut at 100 KB, the array threw a parse error and the JSONL reader recovered 1,355 records.
Where does the HealthMirror app put its JSONL files?
HealthMirror writes plain-text JSONL into its own iCloud container, iCloud.com.mlyz.HealthBridge, which lands on a Mac under ~/Library/Mobile Documents/iCloud~com~mlyz~HealthBridge/Documents/raw/ as per-metric subfolders. Its own MCP server reads those folders locally.
Which format does MetricBridge export?
MetricBridge writes a compact JSON daily cache, .health-cache.json, plus optional companion files for workouts, events, profile, sleep sessions, cycles and timezone changes. It is a snapshot the MCP server reads locally, and get_freshness reports how old it is.
Is JSONL better than JSON for a health agent?
Not universally. JSONL is better when the file is written incrementally or in pieces, when it may be read while still being written, or when the reader filters with grep or jq. JSON is simpler for small whole-file reads because the schema is one object with a known shape.
Can I read my Apple Health snapshot with the command line?
Yes. With JSONL you can select one metric with jq -c 'select(.metric=="hrv_sdnn")' file.jsonl or grep a date, without loading the rest. With a JSON array you use jq over the whole document. Both agree on the numbers; only the access pattern differs.