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:
- Penalties are held back. Every penalty rule is suppressed.
- A check-in is pulled forward, regardless of the normal interval.
- Your Handler changes voice โ the nagging lines are replaced by an easier one.
- Rewards still count. Moving at all on a day like this is worth more, not less.
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.