Health Vitals

approvedunreviewed

by Johannes Kaindl

Import Apple Health exports and explore your health data in charts and tables.

โ†“ 951 downloadsUpdated 8d agoAGPL-3.0

Health Vitals

An Obsidian plugin that reads Apple Health exports and makes the data searchable and visualizable inside your vault.

License: AGPL-3.0 Docs: CC BY-SA 4.0 Release Obsidian

๐Ÿ‡ฌ๐Ÿ‡ง English ยท ๐Ÿ‡ฉ๐Ÿ‡ช Deutsch

No HealthKit access โ€” Obsidian runs in Electron, and HealthKit is a native iOS/macOS API. This plugin works with the export file you send yourself from the Health app.

The Health Vitals dashboard in Obsidian: favourite tiles for steps, resting heart rate and sleep with sparklines, below them the Activity category expanded

Features

  • Reads Export.zip or an unpacked Export.xml as a stream โ€” several gigabytes without filling up memory.
  • A dashboard with three tabs: Overview (one tile per metric with a sparkline, favourites pinnable), Detail (time series with period presets and a value table), Workouts.
  • Aggregation that matches the kind of metric โ€” sum, mean with min/max, or duration.
  • Sleep is merged, not summed, so nights recorded twice don't add up to impossible values.
  • Sleep stages as a stacked bar โ€” deep, core and REM per night, with the bar height still being the time slept. Over longer periods a bar shows the average night rather than the sum. Nights from before watchOS 9 carry no breakdown and stay neutrally coloured, with a note when they dominate the period.
  • Copy the value table to the clipboard, or write it as .md/.csv into a vault folder of your choice.
  • Charts are hand-drawn SVG without a charting library and use nothing but Obsidian's own theme variables.
  • Bilingual interface (German/English), following Obsidian's UI language.
  • No network calls โ€” everything stays on your machine.

Why

Apple's Export.xml easily reaches several gigabytes (2.6 GB with 5.7 million records in the test case). Ordinary XML parsers load all of it into memory and crash. This plugin parses as a stream (SAX-style, chunk by chunk) and stores nothing but compact daily aggregates โ€” the cache built from 5.7 million records is ~2.7 MB.

Requirements

  • Obsidian 1.8.7 or newer.
  • Desktop โ€” the plugin is isDesktopOnly: true; importing multi-gigabyte XML files only makes sense there.
  • An Apple Health export (Export.zip or an unpacked Export.xml), created in the Health app on an iPhone. So an iPhone is only needed to produce the export, not to use the plugin.

Install

From the community store (recommended): in Obsidian โ†’ Settings โ†’ Community plugins โ†’ Browse โ†’ search for "Health Vitals" โ†’ Install โ†’ Enable.

Manual install: download main.js, manifest.json and styles.css of the latest release from the releases page, put them into <vault>/.obsidian/plugins/health-vitals/, then enable the plugin under Settings โ†’ Community plugins.

From source: clone the repository, run npm install && npm run build, then copy main.js, manifest.json and styles.css into the same folder.

Usage

  1. In the Health app (iPhone): Profile โ†’ Export all health data โ†’ move the resulting Export.zip to your computer.
  2. In Obsidian: the Health Vitals dashboard ribbon icon (heart-pulse) (or the command palette โ†’ "Health Vitals: Open dashboard").
  3. Click "Choose export" in the dashboard and pick the Export.zip (or an unpacked Export.xml) in the file dialog.
The dashboard before the first import: No data yet, a hint pointing to Profile then Export All Health Data in the Health app, and a Choose export button

A large export takes a few minutes. Progress, current phase and a cancel button stay visible in the dashboard while it runs; the overview opens by itself afterwards.

The result is health-cache.json in the plugin directory: daily aggregates per metric plus a list of workouts.

The interface is bilingual (German/English) and follows Obsidian's UI language automatically โ€” a German Obsidian shows German, anything else shows English. There is no separate setting for it.

Dashboard

Command palette โ†’ "Health Vitals: Open dashboard" (or the ribbon icon). The dashboard loads health-cache.json lazily when opened, so vault startup stays untouched. Three tabs:

  • Overview โ€” one tile per metric with a headline figure and a sparkline. Metrics can be pinned to the top as favourites with a star (this is remembered); the rest is grouped by category and collapsible.
  • Detail โ€” clicking a tile opens the time series: period presets 1M / 3M / 1Y / All, with the matching figures below. Long periods are bucketed automatically (days โ†’ weeks โ†’ months) to keep the chart readable. Below that, a value table with the underlying rows can be expanded; its contents can be copied to the clipboard or written as a Markdown or CSV file into a vault folder of your choice (see "Privacy").
  • Workouts โ€” workouts per month as bars, with a monthly summary (count, distance, active energy burned) above them and the most recent sessions listed below by type, date, duration, distance and active energy โ€” distance shows "โ€”" for workouts that don't have one, such as strength training.
Detail view of resting heart rate over three months: a line with a min/max band, labelled axes, dashed week starts, and average, minimum, maximum and last value below

The value table below the chart expands on demand, and can be copied or written into the vault as Markdown or CSV:

The expanded Values section: Copy and Save buttons, a Markdown/CSV switch, a folder field, and the first table rows with date, average, minimum and maximum The Workouts tab: one bar per month, a monthly summary of count, distance and active energy, and a list of recent sessions with type, date, duration, distance and active energy โ€” a strength training session shows a dash for distance

Charts are hand-drawn SVG without a charting library and use nothing but Obsidian's theme variables โ€” so they adapt to any theme (light/dark/community).

Access outside the vault

This plugin reads one file outside your vault: the health export you pick in the file dialog. That is necessary because an Apple Health export is several gigabytes and does not belong in a vault. That export file is only ever read โ€” nothing about it is written, moved or sent anywhere. The data derived from it lands as health-cache.json in the plugin directory on your machine.

Separately, the Detail tab can write value tables inside the vault as files on request โ€” that is not access outside the vault, but an ordinary write into a folder you chose. Details under "Privacy".

Configuration

The plugin has no settings tab. What it remembers, you set where it takes effect โ€” in the dashboard:

SettingWhereMeaning
FavouritesStar on a tile (Overview)Pinned metrics sit at the top
Collapsed stateCategory groups (Overview)Which groups stay open
Export folderDetail tab, folder field with autocompleteTarget folder of the value table
Export formatDetail tab.md or .csv

That is stored in data.json in the plugin folder โ€” not in the cache, which the next import overwrites. The interface language is deliberately not a setting: it follows Obsidian.

Changing the sleep-stage colours

The chart takes its colours from your theme's own variables, so it already follows any theme you install. If you want different ones anyway โ€” colour vision differences are individual, and no default fits everyone โ€” a CSS snippet does it without a settings tab (Settings โ†’ Appearance โ†’ CSS snippets):

/* Every stage takes one rule: `fill` colours the bar segment, `background-color` the
   swatch in the legend. Setting only one of them makes the legend lie. */
.ah-stage-deep        { fill: var(--color-purple); background-color: var(--color-purple); }
.ah-stage-core        { fill: var(--color-blue);   background-color: var(--color-blue); }
.ah-stage-rem         { fill: var(--color-pink);   background-color: var(--color-pink); }
.ah-stage-unspecified { fill: var(--text-muted);   background-color: var(--text-muted); }

One thing worth knowing when you pick: the accent colours of a theme (--color-*) owe you no contrast against the background โ€” they are made for lines and highlights. On a light theme several of them land near 2:1, where WCAG asks for 3:1 on meaningful areas. Mixing an accent with the text colour keeps the hue and buys the contrast, which is what the default for "core" does: color-mix(in srgb, var(--color-cyan) 75%, var(--text-normal)).

How it works

How metrics are aggregated

The presentation follows the kind of metric:

KindExamplesAggregationChart
sumSteps, caloriesDaily sumBars
measureWeight, heart rateMean with min/maxLine + band
durationMindfulnessSum of intervalsBars

Weekly/monthly bucketing sums or averages accordingly (not sums) โ€” a mean heart rate over a month stays a mean.

Durations are shown as hours and minutes (7h 12m), and as hours only beyond a day. The CSV export instead contains the raw values in minutes.

Sleep

Sleep is not aggregated like the other metrics and appears as two equally ranked values: "asleep" (time actually slept) and "in bed".

The reason lies in the data: Apple exports the same time several times over for the same night โ€” time in bed encloses the sleep stages within it, and several devices (iPhone, watch, third-party apps) describe the same night in parallel. Added up, that yields impossible values. Overlapping intervals are therefore merged rather than summed: time recorded twice counts once.

A night belongs to the day you wake up on. Sleep starting from 20:00 counts towards the following day โ€” otherwise the night that ends in the morning and the one that begins in the evening would fall on the same calendar day.

Picking "Sleep" in the Detail tab adds a stacked bar per night below the time series โ€” deep, core and REM, with the bar height still being the time slept:

The Sleep stages section: one stacked bar per night over three months, split into deep, core and REM, with a legend and the average awake time below

Nights recorded before watchOS 9 carry no breakdown. They stay neutrally coloured, and once they make up more than half of the period, a note says so rather than leaving the grey to be guessed at:

The same section over the full period: the older half of the nights is a single neutral grey block, the newer half is split into stages, and a note states that 54 percent of nights carry no breakdown

Privacy

Health data is particularly sensitive. Therefore:

  • Everything stays local. The plugin sends nothing anywhere; there are no network calls.
  • health-cache.json is gitignored โ€” it never ends up in a repository by accident. There is no import/ folder anymore; the export is read straight from the file dialog, without anything being copied into the plugin directory.
  • isDesktopOnly: true โ€” importing large XML files only makes sense on the desktop.
  • The value export in the Detail tab writes into your vault, but only when you click "Save". There is an expandable value table there with the raw values of the current metric and period; clicking "Save" puts them as a .md or .csv file into a vault folder you chose (folder field with autocomplete over your existing folders). The file name is composed of the metric name plus the first and last time key of the table. If the file already exists, it is never overwritten โ€” the plugin appends a running number instead ( 2, 3, โ€ฆ) until it finds a free name. These export files then sit in your vault like any other note: if your vault is synced or versioned, so are they.

If you sync your vault, health-cache.json sits in the plugin folder under .obsidian/ and comes along depending on your sync configuration โ€” deliberately your decision.

Development

npm run dev        # esbuild watch
npm run build      # typecheck + production bundle โ†’ main.js
npm test           # vitest
npm run typecheck  # tsc --noEmit
npm run lint       # eslint (obsidianmd, type-checked)
npm run deploy     # build + copy to $OBSIDIAN_PLUGIN_DIR

The code is split into a pure core layer (src/core/ โ€” parser, aggregation, chart geometry, view models; no obsidian import, testable in Node) and an Obsidian layer (src/obsidian/ โ€” view, SVG rendering, file access). Conventions and architecture notes: AGENTS.md.

Note for contributors: renderer-specific behaviour (SVG DOM, ItemView, web workers) is invisible to Node unit tests โ€” changes to the Obsidian layer need a manual test in real Obsidian on top.

Documentation

License

Copyright ยฉ 2026 Johannes Kaindl

Licensed under the GNU AGPL v3.0 or later. The documentation is licensed CC BY-SA 4.0, see LICENSE-DOCS.

For plugin developers

Search results and similarity scores are powered by semantic analysis of your plugin's README. If your plugin isn't appearing for searches you'd expect, try updating your README to clearly describe your plugin's purpose, features, and use cases.