Latch

Modules

Health

Readings from Apple Health and SparkyFitness earn points, take time off the clock and โ€” on a bad night โ€” make your Handler ease off instead of pushing.

On this page

๐Ÿ’š Health lets the evidence you are already collecting move the game. You wear a watch, you log meals, you record mood. This module reads those numbers once an hour and turns them into praise, points, time on or off the clock, and โ€” the part that matters most โ€” a Handler who notices when you are running on four hours of sleep.

Where the numbers come from

Latch does not reach out for them. A sync script on the apps pod reads TimescaleDB (Apple Health, via Health Auto Export and n8n) and SparkyFitness, then pushes a day's readings to POST /api/v1/health/daily.

Important

That direction is deliberate. It keeps this container free of a database driver and keeps "this app makes no outbound calls" something you can verify by reading the imports, instead of a firewall rule somebody has to remember to maintain.

Metrics understood: sleep (total, deep, REM, awake), steps, exercise minutes, stand hours, active energy, workout count and minutes, resting heart rate, HRV, mood valence, meals logged, weight.

Unknown is not zero

A quarter of days carry no reading at all โ€” the watch was off the wrist, the phone did not sync, you did not open the app. So every rule resolves to one of three states, never two:

State Meaning What happens
fired the reading met the threshold the rule applies
not fired the reading did not meet it nothing
unknown there is no reading nothing, in either direction

Warning

This is why a missing reading is stored as no row at all, and why the tiles show โ€” rather than 0. A fabricated zero would tell your Handler you took no steps on a day nobody counted them โ€” and once it is written, there is no way to work out later which zeros were real.

A measured zero is different and perfectly legal: meals logged = 0, written after SparkyFitness answered and had nothing, means you logged nothing today. The sync decides which it is; it simply omits the reading when a source cannot be read.

The rules

Each rule fires at most once a day and writes an ordinary event, so it appears in your log, counts toward activities, and can be undone.

Rule When Effect
Moved today steps โ‰ฅ 12,000 +5 XP
Exercise minutes exercise โ‰ฅ 60 min +10 XP, 5 min off
Workout logged any workout +25 XP, 15 min off
Slept well sleep โ‰ฅ 7 h +10 XP
Ate and logged it โ‰ฅ 2 meals +5 XP
Short sleep sleep < 6 h recovery day + check-in due
Low mood valence < 0 recovery day + check-in due
Barely moved steps < 3,500 10 min on (penalty)
No workout no workout 5 min on (penalty, off by default)

Thresholds are editable on Settings โ†’ ๐Ÿ’š Health.

Note

These defaults were measured against 90 days of your own readings, not guessed. Steps run at a median of 19,208/day, so the obvious 5,000-step bar would have fired every single day and meant nothing; 12,000 is the 25th percentile. Workouts land on 18 days in 90, which is why one is worth 25 XP โ€” and why "no workout" ships disabled: as a daily rule it would fire four days in five. Enable it only once you have defined what a training day is.

The sleep thresholds are the exception. They are conventional (7 h / 6 h) because the sleep pipeline was only repaired on 2026-09-19 and there was no history to baseline against. Re-derive them once a few weeks have accumulated.

Recovery days โ€” the interlock

If you slept badly or your mood is low, today is a recovery day:

Important

A withheld penalty writes its own penalty_withheld event. It is never a silent skip. If the game quietly went easy on you with no record, that would be indistinguishable from the rule being broken โ€” and you would have no way to tell whether your Handler made a decision or simply failed.

Recovery is derived every time it is read, never stored. A stored flag would outlive the reading that justified it and keep excusing you for a reason that is no longer true.

When the sync stops

If nothing has arrived for 36 hours the whole module goes stale: the card says so, the numbers are marked old, and the Handler stops quoting them.

Warning

Without this, a dead pipeline leaves the last rows it ever delivered sitting there looking perfectly current, and your Handler would go on discussing week-old sleep with total confidence. Rules do not fire on stale data.

Diet and mood get a second treatment: if nothing has been logged for several days the page says so in words. A blank tile reads as fine, and noticing that a stream stopped is most of the reason to track it.

Activities you get for free

Seeding this module adds three activities โ€” Moved, Workout, Slept well. Because an activity is a saved query over your event log rather than a separate record, they light up your calendar and streaks from the whole history the moment they exist, and each brings five stat tokens (streak_moved, count_workout, โ€ฆ) you can put on a share card.

The page

/health shows today's tiles, every rule and whether it fired, what was held back, and 30-day charts for sleep, steps and exercise. The module also puts one chart on Progress โ†’ Trends: steps, sleep and mood against one time axis.

Note

The lines break where data is missing rather than joining across the gap. A continuous line drawn through a fortnight nobody measured is a picture of steady progress invented out of nothing โ€” and about a quarter of days carry no reading.

Note

Every health chart ends at the last day your phone exported, not today. The export lands overnight, so today is empty until tomorrow; a chart drawn to today would show every metric falling off a cliff at the right-hand edge, every single day. The date it ends on is printed under it.

Apply today's rules re-runs them by hand. It is idempotent โ€” a rule that already fired today will not fire again โ€” so the worst it can do is nothing.


This page also ships inside the app, at /guide โ€” so your own instance always serves the guide for the version you are running, with the internet unplugged. Get Latch ยท Something wrong here? Tell me.