Jaan Altosaar23 downloadsSummarize Oura sleep, heart-rate and activity metrics into one LLM-ready note — 7, 14, or 28 days, compared against the weeks before.
An Obsidian plugin that turns your Oura data into one LLM-ready markdown note — 7 days, 2 weeks, or 4 weeks — ready to paste into a chat.
A companion to Microlite (Obsidian plugin link), which does the same for your note edits. Same shape: tap the ribbon icon, get a dated note in a folder, paste it somewhere useful.
Oura's app shows you today. This note shows each metric's change from your own recent baseline over weeks, including its spread, not just its average.
It reports a small, fixed set of scalars and nothing else. Every extra field is context an LLM has to wade through, and noise it can pattern-match against.
Sleep — total sleep, efficiency, onset latency, deep/REM/light as a percentage of total sleep time, midsleep clock time, sleep score.
Physiology and activity — nocturnal resting and average heart rate, HRV (RMSSD), respiratory rate, skin-temperature deviation, activity score, active MET minutes, within-day activity CV.
Derived — the window measured against the three weeks before it: each metric's mean and SD for both periods, the change between them in the metric's own unit and in baseline SDs, a 7-night rolling SD of sleep duration, and every day sitting beyond ±1.5 SD of the baseline.
A window that is its own baseline cannot see a shifted week. Take seven nights of five hours' sleep: measured against their own mean, every one of them is unremarkable. So the fetch reaches three weeks further back than the window it prints. Those days are never listed — they set the mean and SD everything in the window is read against.
z is the difference between the two means in baseline SDs (a standardized mean
difference against a reference period), not a test statistic. Dividing by the standard
error instead would call nearly every week significant: n is 7 and consecutive nights are
correlated. Read ±0.5 as visible and ±1 as pronounced, and look at several metrics
together before either.
Both periods report an SD, not just a mean: a week can hold its average and still scatter far more than the weeks before it.
The intra-night hypnogram and the 1440-point daily MET series never appear. They are reduced to scalars — stage percentages, one activity CV per day. A language model reads a 288-character stage string poorly and a single number well.
npm install
npm run install:vault # builds, then copies into the vault in .vault-path
.vault-path is a gitignored one-line file holding the absolute path to your
vault (or set OBSIDIAN_VAULT). Then enable Oura Metrics in Settings →
Community plugins.
Breaking in 0.2.0: Oura retired personal access tokens in December 2025, and the plugin now connects through OAuth. The old token setting is gone and is deleted from plugin data on first load; connect once as below.
obsidian://oura-metrics and tick Daily, Personal,
Heartrate, Workout, Tag, Session, SpO2 and Heart Health. For a personal app, the
repo URL is fine as website, privacy policy and terms.OURA_CLIENT_ID=… in a gitignored .env here and npm run install:vault writes it in.The plugin uses Oura's client-side (implicit) flow, so it needs no client secret and nothing is hosted: Oura hands the token straight back to Obsidian. The cost is no refresh token. Access lasts 30 days, then you click Reconnect.
Only daily (sleep, activity, readiness) is read today. The other scopes are requested
up front so later metrics don't need new consent, and you can decline them.
The access token lives in the vault's plugin data (.obsidian/plugins/oura-metrics/data.json),
never in git. It is stored in plaintext — Obsidian gives a plugin nowhere else to
persist settings — so if your vault syncs to a cloud service, the token syncs with it.
It is read-only and expires in 30 days; Disconnect forgets it sooner.
Tap the ribbon icon, or run Generate summary (last 7 days / 2 weeks / 4 weeks)
from the command palette. A dated note — oura/oura-metrics-YYYY-MM-DD.md —
opens, ready to copy.
Settings cover the Oura connection, output folder, default window, deviation threshold,
whether the output folder is excluded from search, and the prompt template that
leads each note (placeholders: {{date}}, {{time}}, {{window}}). The prompt
is saved in the vault's plugin data, so a personal prompt never touches the repo.
The headless CLI lives in petrograph
(tools/oura_metrics.ts), which pins this repo as a submodule and calls the very
same buildDays and renderNote the plugin does — so the note reaching a model
is the note you have reviewed by eye. OuraClient takes an OAuth access token and its HTTP transport as
constructor arguments, so the plugin passes requestUrl and the CLI passes
fetch without either importing the other's.
Midsleep is computed in the timestamp's own timezone, not the machine's. Oura
records the UTC offset the ring was in, and that is the clock that matters — a
night slept in Lisbon should read as Lisbon local time no matter where the note is
generated later. Using getHours() would silently re-clock your whole history when
you travel.
Today's activity is partial and excluded from baselines. Last night's sleep is
complete by morning, but the day's activity is not — a run at noon sees a third of
a day. Those values are shown (marked *) but kept out of the mean, or every
pre-bedtime run would flag a phantom collapse in activity and drag the baseline
the other days are compared against. Sleep on the same row still counts.
Stage percentages are of total sleep time, not time in bed. Dividing by time in bed silently deflates every stage by the awake fraction.
Deviations are measured against the previous three weeks. That is a real reference period rather than the window judging itself, but it inherits whatever those weeks were: if they were already unusual, the current one reads as normal. Several metrics moving together say more than any single flag. Sleep stages from a consumer ring are estimates, so stage percentages are trend indicators, not measurements.
A pixel-art crescent moon, mirrored across the vertical centre line so it opens
to the upper right. assets/moon.svg is the standalone file;
src/icon.ts inlines the same markup for addIcon().
It is traced one rect per grid cell rather than with potrace or Inkscape. Vector tracing smooths the staircase edges into curves, which destroys the pixel-art look that is the whole point. The source PNG turned out to be a 16×16 grid upscaled (the 32px file tiles cleanly at 2px blocks, and the 100px file agrees with it on all 256 cells), so each cell maps to exactly 6.25 units of Obsidian's 0 0 100 100 viewBox. Adjacent cells are merged into maximal blocks — 35 cells become 15 rects. Rasterizing the result back to 32px matches the mirrored source 1024/1024 pixels.
Icon from Icons8.
npm test # unit tests over the metric derivations and rendering
npm run dev # esbuild watch
npm run build # typecheck + production bundle
src/metrics.ts and src/render.ts are free of Obsidian imports, so the whole
derivation and rendering path is testable in plain Node.