| vaultbeat_statusA | Return local Vaultbeat binding state without exposing private keys or server tokens. |
| vaultbeat_doctorA | Diagnose this Vaultbeat MCP install, and report which data types are unavailable. Call this when a read tool returns nothing, or when anything fails, before
telling the user their data is missing. Two distinct things come back: checks — the install/binding chain: config present, keypair usable,
binding valid, cloud reachable, a real record decryptable end to end.
A failure here means the setup is broken, not that data is absent.
capabilities — which metric kinds actually have data, and which empty
ones are explained by an older iOS app (with the release date each
needs). An empty kind is NOT proof the user never recorded it: their app
may predate the feature entirely.
scope — what this report does NOT cover. Everything here can pass and
the setup still be broken on the client side: this server is a subprocess
of your MCP client, so it cannot read the client's config file, cannot see
which environment variables the client forwarded, and cannot tell whether
it was launched with the arguments the user believes. A green report never
clears the client. scope.env_overrides_received lists which Vaultbeat
variables actually arrived — check that before guessing at the client's
environment.
This runs a cloud round trip, so it is slower than vaultbeat_status
(local config only) — prefer status for a quick liveness check. |
| vaultbeat_sync_sleepA | Fetch encrypted Vaultbeat sleep records, decrypt them locally, and return
per-day primary session summaries matching the iOS app's display. Returns daily_summary (one primary session per local date, selected by
iOS priority: Watch > iPhone > inBedOnly) and sessions (all raw records).
The limit controls how many raw blobs are fetched; 50 covers ~2-3 weeks.
Use owner prefix to filter by person — take it from vaultbeat_status
(owner_user_id_prefix is the paired user; server 0.7.1+), or
vaultbeat_doctor for every owner present in the data. Without it, both
partners' data is mixed and per-day selection may pick the wrong
person's session. ⚠️ is_in_bed_only: true means sleep was NEVER MEASURED that night (the
Watch wasn't worn) — NOT that the person slept zero. On those nights
total_sleep_minutes is 0, duration_label reads "no sleep data", and
in_bed_minutes holds the time actually recorded in bed. Report such a
night as "no sleep data (in bed ~Xh)", never as "slept 0 hours". Results are served from a short-lived local cache (default 10 min);
pass fresh=True to force a cloud round trip. Carries a coverage block: quote coverage.days_covered (distinct days, not
the row count) and coverage.span_days beside any average or trend, and read
coverage.window_satisfied: false as a shorter history than asked, not as a
missing kind. 🔴 Before saying how far back someone's data goes, read
coverage.more_available: true means this server can decrypt days OLDER
than first_day that your limit left behind — re-read with a larger
limit, or quote coverage.oldest_available as the real start of their
history. Never report a limit-shaped window as the extent of their data. 🔑 Pass summary_only=True when you only want the SIDE EFFECT. A default
call returns every decrypted session — measured at 76,446 characters, which
overflows a typical tool-result limit and gets spilled to a file the caller
then has to read back. That is the right shape when you want the nights; it
is pure waste when you called this to make the server do something (force a
sync, check the link is alive, confirm a deploy took effect), which is a
large share of real calls. summary_only=True returns the counts, the day
range and the coverage block, and nothing else. For the nights themselves,
get_sleep_detail is the tool that exists for it. |
| get_water_intakeA | Decrypt recent daily water intake locally and compute the average. Returns one entry per day (newest first) with refill count, container volume, and
derived intake in liters, plus average_daily_intake_liters over the window.
Use owner prefix to filter by person — take it from
vaultbeat_status (owner_user_id_prefix is the paired user; server
0.7.1+), or vaultbeat_doctor for every owner present in the data.
Each record carries owner_user_id to identify whose data it is. Carries a coverage block: quote coverage.days_covered (distinct days, not
the row count) and coverage.span_days beside any average or trend, and read
coverage.window_satisfied: false as a shorter history than asked, not as a
missing kind. 🔴 Before saying how far back someone's data goes, read
coverage.more_available: true means this server can decrypt days OLDER
than first_day that your limit left behind — re-read with a larger
limit, or quote coverage.oldest_available as the real start of their
history. Never report a limit-shaped window as the extent of their data. |
| get_weight_trendA | Decrypt recent body-weight records locally and compute the trend. Returns one entry per day (newest first, kilograms) plus latest/average/min/max,
the OLS weekly rate (kg/week), and — when goal_kg is given — the distance to goal.
Use owner prefix to filter by person — take it from
vaultbeat_status (owner_user_id_prefix is the paired user; server
0.7.1+), or vaultbeat_doctor for every owner present in the data.
Each record carries owner_user_id to identify whose data it is. Carries a coverage block: quote coverage.days_covered (distinct days, not
the row count) and coverage.span_days beside any average or trend, and read
coverage.window_satisfied: false as a shorter history than asked, not as a
missing kind. 🔴 Before saying how far back someone's data goes, read
coverage.more_available: true means this server can decrypt days OLDER
than first_day that your limit left behind — re-read with a larger
limit, or quote coverage.oldest_available as the real start of their
history. Never report a limit-shaped window as the extent of their data. |
| get_symptomsA | Decrypt recent HealthKit symptom days locally, grouped by data owner. SENSITIVE: symptom data (cramps, headache, fatigue, coughing…) only reaches
this server when a user explicitly opted in on iOS — their own AI toggle for
their own data, or the partner-AI toggle for a partner's data. Both partners
can track symptoms, so each entry in owners carries owner_user_id plus
per-type counts and day-by-day samples with severity
(mild/moderate/severe/present/…). Stays on-device, never re-exported. Carries a coverage block: quote coverage.days_covered (distinct days, not
the row count) and coverage.span_days beside any average or trend, and read
coverage.window_satisfied: false as a shorter history than asked, not as a
missing kind. 🔴 Before saying how far back someone's data goes, read
coverage.more_available: true means this server can decrypt days OLDER
than first_day that your limit left behind — re-read with a larger
limit, or quote coverage.oldest_available as the real start of their
history. Never report a limit-shaped window as the extent of their data. |
| get_notesA | Decrypt recent free-text notes (day annotations) locally. SENSITIVE free text. Each note carries owner_user_id (who wrote it),
target_kind, and target_date (the local day it annotates) — join
against the same-day metric data for pattern analysis. Kinds:
"sleep" | "menstrual" are written manually in the iOS app by either
partner (e.g. "昨晚舍友很吵" on a sleep day); "mood" | "general" are
agent-authored via log_note. Pass target_kind to filter.
Stays on-device, never re-exported. Carries a coverage block: quote coverage.days_covered (distinct days, not
the row count) and coverage.span_days beside any average or trend, and read
coverage.window_satisfied: false as a shorter history than asked, not as a
missing kind. 🔴 Before saying how far back someone's data goes, read
coverage.more_available: true means this server can decrypt days OLDER
than first_day that your limit left behind — re-read with a larger
limit, or quote coverage.oldest_available as the real start of their
history. Never report a limit-shaped window as the extent of their data. |
| get_strength_logA | Decrypt recent strength-training sessions locally (newest first). Exercise-level detail HealthKit's workout type cannot carry: each
session lists exercises with their sets (weightKg × reps), an optional
session note, and total_volume_kg (Σ weight × reps). Logged manually
in Vaultbeat; owner's own sessions only — strength has no partner
fan-out. Join date against sleep/HRV/weight for training-load
analysis. Pass limit_days to cap how many sessions return. Carries a coverage block: quote coverage.days_covered (distinct days, not
the row count) and coverage.span_days beside any average or trend, and read
coverage.window_satisfied: false as a shorter history than asked, not as a
missing kind. 🔴 Before saying how far back someone's data goes, read
coverage.more_available: true means this server can decrypt days OLDER
than first_day that your limit left behind — re-read with a larger
limit, or quote coverage.oldest_available as the real start of their
history. Never report a limit-shaped window as the extent of their data. |
| log_strength_entryA | Log one strength-training session on the owner's behalf (agent write). ⚠️ THIS TOOL DELETES. The supplied exercises become the day's ENTIRE
session — any exercise you don't re-send is silently deleted. If you meant
to ADD to a day rather than replace it, STOP and call
log_strength_append instead; it cannot delete anything. (merge=True still does the same thing as log_strength_append and
keeps working for callers that already use it. New callers should use
the separate tool: which one you called is visible to the owner, a flag
buried in the arguments is not.) The result carries replaced_exercises — the names this call deleted.
If that list is non-empty and you did not intend to replace the day, you
just destroyed those exercises; re-send them with merge=True. note=None LEAVES THE EXISTING NOTE ALONE (pass note="" to clear it).
date is the LOCAL calendar day the session happened, "YYYY-MM-DD".
exercises is [{"name": "卧推", "sets": [{"weightKg": 30, "reps": 8}, ...]}, ...].
Encrypted end-to-end before it ever leaves this machine — this server
never sends plaintext. Requires a bind made after this feature shipped
(carries owner_user_id/owner_public_key_base64/owner_device_id from
the pairing handshake); an older bind must re-pair by calling
vaultbeat_start_binding then vaultbeat_poll_binding.
|
| log_strength_appendA | Add exercises to a session WITHOUT touching what is already logged (agent write). This tool cannot delete anything you did not send. Your exercises are
appended to the day's existing session; an exercise whose name matches an
existing one gets its sets appended to it. This is the right tool for "log
the set I forgot" or for logging a session in installments while the owner
is still in the gym — which is how strength data actually arrives. Reach for log_strength_entry ONLY when you intend the supplied exercises
to become the day's ENTIRE session and everything else to be deleted. date is the LOCAL calendar day the session happened, "YYYY-MM-DD".
exercises is [{"name": "卧推", "sets": [{"weightKg": 30, "reps": 8}, ...]}, ...].
This tool deliberately cannot set the session note: that field is
replace-only, and a tool that promises to delete nothing must not carry an
exception. Use log_strength_entry to change it. The result is the same shape log_strength_entry returns.
replaced_exercises is always [] here — that empty list is the receipt
that this call deleted nothing. Encrypted end-to-end before it ever leaves
this machine. Requires a bind made after the agent write path shipped; an
older bind must re-pair via vaultbeat_start_binding then
vaultbeat_poll_binding. |
| get_food_logA | Decrypt recent daily food-intake logs locally (newest first). Each day carries meals, each meal a list of items with food (name),
optional free-text portion ("1 根" / "300g" / "小份"), optional
per-item/per-meal note, and — when the logging agent estimated them —
optional structured nutrition numbers (kcal, proteinGrams,
fatGrams, carbGrams). Items without those fields need analysis-time
estimation from name + portion; items with them can be summed directly.
Owner's own days only. Pass limit_days to cap how many days return. Carries a coverage block: quote coverage.days_covered (distinct days, not
the row count) and coverage.span_days beside any average or trend, and read
coverage.window_satisfied: false as a shorter history than asked, not as a
missing kind. 🔴 Before saying how far back someone's data goes, read
coverage.more_available: true means this server can decrypt days OLDER
than first_day that your limit left behind — re-read with a larger
limit, or quote coverage.oldest_available as the real start of their
history. Never report a limit-shaped window as the extent of their data. |
| log_food_entryA | Log one day's food intake on the owner's behalf (agent write). ⚠️ THIS TOOL DELETES. The supplied meals become the day's ENTIRE log —
any meal you don't re-send is silently deleted. If you meant to ADD to a
day rather than replace it, STOP and call log_food_append instead; it
cannot delete anything. (merge=True still does the same thing as log_food_append and keeps
working for callers that already use it. New callers should use the
separate tool: which one you called is visible to the owner, a flag
buried in the arguments is not.) The result carries replaced_meals — the meals this call deleted. If
that list is non-empty and you did not intend to replace the day, you
just destroyed them; re-send them with merge=True. note=None LEAVES THE EXISTING NOTE ALONE in both modes (pass
note="" to clear it).
date is the LOCAL calendar day, "YYYY-MM-DD".
meals is a list of {name?, timeOfDay?, items: [...], note?} where each
item is {food, portion?, note?, kcal?, proteinGrams?, fatGrams?, carbGrams?},
e.g. [{"name": "lunch", "items": [{"food": "香蕉", "portion": "1 根", "kcal": 105}]}].
Everything but food is optional so a rushed "just log 香蕉" still works;
when you DO estimate nutrition at logging time, put the numbers in the
structured fields (snake_case aliases like protein_g are accepted) —
they persist for later sessions instead of being re-guessed each read.
ESTIMATING FROM A PHOTO: look for something of known size in the frame
first — a utensil, a hand, a coin, the rim of a standard plate — and
calibrate the portion against it. With no such reference an image cannot
settle portion size, and portion size is what the whole estimate rests
on. In that case say so in your reply and give a range rather than a
precise-looking number. These values are persisted and summed into daily
totals later, so a confident "650 kcal" that is wrong does more damage
than "roughly 500-700, nothing in frame to judge size by" — the first
silently poisons a week of trends, the second invites a correction.
[keep the photo-estimation paragraph above in sync with log_food_append's copy]
Encrypted end-to-end before it ever leaves this machine. |
| log_food_appendA | Add meals to a day WITHOUT touching what is already logged (agent write). This tool cannot delete anything you did not send. Your meals are appended
to whatever the day already holds; a meal whose name matches an existing
meal gets its items appended to that meal. This is the right tool for "log
the snack I forgot" / "add dinner to today" — which is almost every
follow-up write of the day. Reach for log_food_entry ONLY when you intend the supplied meals to become
the day's ENTIRE log and everything else to be deleted. date is the LOCAL calendar day, "YYYY-MM-DD".
meals is a list of {name?, timeOfDay?, items: [...], note?} where each
item is {food, portion?, note?, kcal?, proteinGrams?, fatGrams?, carbGrams?},
e.g. [{"name": "lunch", "items": [{"food": "香蕉", "portion": "1 根", "kcal": 105}]}].
Everything but food is optional so a rushed "just log 香蕉" still works;
when you DO estimate nutrition at logging time, put the numbers in the
structured fields (snake_case aliases like protein_g are accepted) — they
persist for later sessions instead of being re-guessed each read.
ESTIMATING FROM A PHOTO: look for something of known size in the frame
first — a utensil, a hand, a coin, the rim of a standard plate — and
calibrate the portion against it. With no such reference an image cannot
settle portion size, and portion size is what the whole estimate rests on.
In that case say so in your reply and give a range rather than a
precise-looking number. These values are persisted and summed into daily
totals later, so a confident "650 kcal" that is wrong does more damage than
"roughly 500-700, nothing in frame to judge size by" — the first silently
poisons a week of trends, the second invites a correction.
[keep the photo-estimation paragraph above in sync with log_food_entry's copy] This tool deliberately cannot set the day's note: that field is
replace-only, and a tool that promises to delete nothing must not carry an
exception. Use log_food_entry to change it. The result is the same shape log_food_entry returns. replaced_meals is
always [] here — that empty list is the receipt that this call deleted
nothing. Encrypted end-to-end before it ever leaves this machine. |
| log_noteA | Log a free-text note on the owner's behalf (agent write). For narratives that belong next to the metric data instead of in chat
history: kind="mood" for emotional state ("为什么今天情绪低落"),
kind="general" for day events worth joining against sleep/HRV later.
(sleep/menstrual notes stay iOS-authored — this tool refuses them.) ⚠️ THIS TOOL DELETES. There is one note per (kind, day), and your text
becomes its ENTIRE contents — anything already written for that kind+day
is silently deleted. If you meant to ADD to a day rather than replace it,
STOP and call log_note_append instead; it cannot delete anything. (merge=True still does the same thing as log_note_append and keeps
working for callers that already use it. New callers should use the
separate tool: which one you called is visible to the owner, a flag
buried in the arguments is not.) The result carries replaced_text — the note this call deleted. If it is
non-null and you did not intend to replace, you just destroyed that text;
re-send it with merge=True. For symptoms, use log_note_append. Discomfort shows up in installments
across a day (nausea at noon, dizziness at night), so the second write
of the day is the normal case, not the exception — and this tool would
replace the morning's entry with the evening's. date = LOCAL calendar day "YYYY-MM-DD" (default today). Read back via
get_notes (optionally target_kind="mood"/"general"). Encrypted
end-to-end before it ever leaves this machine.
|
| log_note_appendA | Add a line to a day's note WITHOUT erasing what is already there (agent write). There is one note per (kind, day). This tool appends your text to it on a
new line and cannot delete what is already written. USE THIS FOR SYMPTOMS. Discomfort arrives in installments across a day —
nausea at noon, dizziness at night — so the second write of the day is the
normal case, not the exception. log_note would replace the morning's
entry with the evening's. Reach for log_note ONLY when you intend your text to become the note's
ENTIRE contents and whatever is there now to be deleted (e.g. correcting
something you yourself wrote a minute ago). kind="general" for day events worth joining against sleep/HRV later,
kind="mood" for emotional state. (sleep/menstrual notes stay iOS-authored
— this tool refuses them.) date = LOCAL calendar day "YYYY-MM-DD"
(default today). Read back via get_notes.
The result is the same shape log_note returns. replaced_text is always
null here — that null is the receipt that this call deleted nothing.
Encrypted end-to-end before it ever leaves this machine. |
| log_weight_entryA | Log the owner's weight (kg) on their behalf (agent write, 2026-07-21). weight_kg: kilograms (positive, ≤500). date: LOCAL calendar day
"YYYY-MM-DD" (default = today). Same-day upsert-in-place semantics
(dayID = "body-{dayStart.epoch}") — re-logging the same day overwrites.
Encrypted end-to-end before it ever leaves this machine.
Written data always lands in Vaultbeat cloud + MCP (visible to
get_weight_trend). Whether it also reaches Apple Health depends on an
iOS setting: Settings → Data & AI → "Allow AI to update Apple Health"
(OFF by default). When it is on, weigh-ins logged here sync back into
Apple Health on the next app sync. ⚠️ If the owner wants this number in the Apple Health app, tell them to
turn that toggle ON — do NOT tell them to re-enter it by hand in the
Vaultbeat weight card. Logging it in both places produces two entries
for the same day from different sources and corrupts the trend line.
(Before 2026-07-28 this docstring said propagation was impossible and
instructed exactly that manual double-entry; the toggle shipped
2026-07-22.) |
| get_menstrual_cycleA | Decrypt recent menstrual cycle data locally and predict the next period. SENSITIVE: menstrual data only reaches this server if the user explicitly opted
in on iOS; it stays on-device and is never re-exported. Returns recent samples plus
a next-period prediction. Use owner prefix to filter by person. Carries a coverage block: quote coverage.days_covered (distinct days, not
the row count) and coverage.span_days beside any average or trend, and read
coverage.window_satisfied: false as a shorter history than asked, not as a
missing kind. 🔴 Before saying how far back someone's data goes, read
coverage.more_available: true means this server can decrypt days OLDER
than first_day that your limit left behind — re-read with a larger
limit, or quote coverage.oldest_available as the real start of their
history. Never report a limit-shaped window as the extent of their data. |
| vaultbeat_start_bindingA | Initialize a binding session: generates a keypair (if needed) and returns a
QR payload that the user scans in the Vaultbeat iOS app to authorize this AI server. ⚠️ If the user says they cannot see the QR code, believe them. Many
terminals and most agent transcripts drop the block characters it is
drawn with, so the payload can reach you intact while their screen shows
a blank gap — do not assert that it is there. Have them run
uvx vaultbeat-apple-health bind in a real terminal instead. Returns qr_payload_json — a JSON string the AI should render as a QR code
for the user to scan, plus poll_id to pass to vaultbeat_poll_binding.
After the user scans, call vaultbeat_poll_binding to complete authorization.
The iOS path is Settings → Data & AI → Connect an AI server. Opening a session does NOT disturb an existing binding: the current
credentials keep working until a scan actually lands, and are only
replaced at that moment. (Before 2026-08-11 this call wiped them up
front, so an agent "just re-checking" silently unbound its owner.) |
| vaultbeat_poll_bindingA | Check whether the user has scanned the QR code and authorized this server. Call this after vaultbeat_start_binding. THREE possible status values: · "pending" — the pairing is alive and simply has not been scanned yet.
Keep polling with short delays.
· "bound" — success. The server can now decrypt health data.
· "expired" — TERMINAL. Stop polling; no amount of retrying recovers it.
Run uvx vaultbeat-apple-health bind for a fresh QR code. 🔴 "pending" is positive evidence that nothing is wrong. The endpoint
looks the pairing row up by pollID and answers "expired" when it is gone,
so a long run of "pending" means the row is still there and nobody has
scanned — NOT that it went stale while you waited. That distinction decides what you tell the user, and getting it backwards
is destructive: re-running bind mints a NEW pollID, which invalidates
the QR they are looking at — so "just run bind again" turns a pairing
that was one scan away from working into one that cannot complete. Only
do it on "expired". So while it stays "pending", the useful action is to get the code
scanned, not to restart anything. Connecting is open on every plan, so
there is no tier to check; the scanner is at Settings → Data & AI →
"Connect an AI server". Do not send them to check the network, reinstall
this server, or run diagnostics — none of those are implicated. If they cannot see a QR code at all, it is your output that failed, not
their phone — see vaultbeat_start_binding. |
| get_sleep_detailA | Per-night sleep stages with per-stage HR/RR — depth on a few nights. Which sleep tool to use:
· THIS one for going deep on one or two specific nights (stage bands,
per-stage vitals). It defaults to 2 nights because each is ~1-2k
characters; see the SIZE note below.
· vaultbeat_sync_sleep for anything spanning time — "how did I sleep
this week/month", trends, averages. Its default of 50 covers ~2-3
weeks. Reach for it whenever the question is about a period rather
than a night, and do not conclude from THIS tool's two rows that only
two nights exist. (This used to call itself "the primary tool for detailed sleep analysis",
which read as "use this one for sleep" and handed back two days to
anyone who asked how their week went.) Returns stage_intervals (contiguous stage bands with start/end),
stage_minutes, and stage_vitals (per-stage HR/RR min/mean/max). Use
owner prefix to filter by person — the first characters of a user id.
vaultbeat_doctor lists them under capabilities.owner_prefixes; there
is no other way to discover one, and omitting owner on a paired
account blends both people into one result. ⚠️ SIZE: each night is ~1-2k characters as returned. Setting
include_timeline=True adds the raw per-sample array (hr, rr, stage,
time) — about 13k characters PER NIGHT, which will overflow a typical
25k-token client budget after 2-3 nights. Ask for it only when you need
sample-level vitals (e.g. "when exactly did HR spike"); stage_vitals
already answers per-stage questions. Raise limit for trends, but keep
it low whenever include_timeline=True. ⚠️ is_in_bed_only: true means sleep was NEVER MEASURED that night (the
Watch wasn't worn) — NOT that the person slept zero. duration_label
reads "no sleep data" and in_bed_minutes holds the time actually
recorded in bed. Report it as "no sleep data (in bed ~Xh)", never as
"slept 0 hours". Carries a coverage block: quote coverage.days_covered (distinct days, not
the row count) and coverage.span_days beside any average or trend, and read
coverage.window_satisfied: false as a shorter history than asked, not as a
missing kind. 🔴 Before saying how far back someone's data goes, read
coverage.more_available: true means this server can decrypt days OLDER
than first_day that your limit left behind — re-read with a larger
limit, or quote coverage.oldest_available as the real start of their
history. Never report a limit-shaped window as the extent of their data. |
| get_activityA | Decrypt recent daily activity rings (steps, active energy kcal, exercise minutes,
stand hours, distance km). One entry per day, newest first.
Use owner prefix to filter by person. Each record carries owner_user_id. Carries a coverage block: quote coverage.days_covered (distinct days, not
the row count) and coverage.span_days beside any average or trend, and read
coverage.window_satisfied: false as a shorter history than asked, not as a
missing kind. 🔴 Before saying how far back someone's data goes, read
coverage.more_available: true means this server can decrypt days OLDER
than first_day that your limit left behind — re-read with a larger
limit, or quote coverage.oldest_available as the real start of their
history. Never report a limit-shaped window as the extent of their data. |
| get_resting_hrA | Decrypt recent resting heart rate samples (bpm). Returns per-day records
plus average over the window. Use owner prefix to filter by person. Carries a coverage block: quote coverage.days_covered (distinct days, not
the row count) and coverage.span_days beside any average or trend, and read
coverage.window_satisfied: false as a shorter history than asked, not as a
missing kind. 🔴 Before saying how far back someone's data goes, read
coverage.more_available: true means this server can decrypt days OLDER
than first_day that your limit left behind — re-read with a larger
limit, or quote coverage.oldest_available as the real start of their
history. Never report a limit-shaped window as the extent of their data. |
| get_workoutsA | Decrypt recent workout sessions (type, duration, calories, distance).
Use owner prefix to filter by person. Carries a coverage block: quote coverage.days_covered (distinct days, not
the row count) and coverage.span_days beside any average or trend, and read
coverage.window_satisfied: false as a shorter history than asked, not as a
missing kind. 🔴 Before saying how far back someone's data goes, read
coverage.more_available: true means this server can decrypt days OLDER
than first_day that your limit left behind — re-read with a larger
limit, or quote coverage.oldest_available as the real start of their
history. Never report a limit-shaped window as the extent of their data. |
| get_hrvA | Decrypt recent HRV (SDNN in ms) — returns records plus average over the window. granularity selects between two backing kinds:
"hourly" (default) — routes to hrv_hourly kind: one bucket per
UTC hour (arithmetic mean of every raw SDNN sample in the hour).
30-day rolling window, ≤720 records, includes sample_count
per bucket. Records also carry a sdnn_ms alias equal to the
hourly mean, so callers migrating from the pre-build-77 raw
default keep working without a field rename. Right for trend /
aggregate queries — SAVES CONTEXT vs raw.
"raw" — routes to hrv kind: one record per SDNN sample (Apple
Watch emits every 5-15min). 3-day rolling window; older raw
history lives in prior-recipient envelopes plus the
VaultbeatHistoryBackfillCoordinator-driven historical push
(advances 30d/24h on device wake-ups, up to 5 years). Use for
spike-precision questions (e.g. "HRV during the 3 minutes I
opened a stressful message"). Note: single-day count is often
30-100+ records.
⚠️ average_sdnn_ms from the two granularities is NOT directly
comparable — they observe different windows (3d vs 30d) and, on
the raw side, also include legacy per-sample blobs from before
build 77. Use hourly for "what has my HRV been lately?" trend
answers; use raw only when you need per-sample precision inside
the last ~3 days. The equivalence claim in previous doc versions
was retracted 2026-07-22 after an adversarial review pointed out
the window mismatch. Use owner prefix to filter by person — take it from
vaultbeat_status (owner_user_id_prefix is the paired user; server
0.7.1+), or vaultbeat_doctor for every owner present in the data. Carries a coverage block: quote coverage.days_covered (distinct days, not
the row count) and coverage.span_days beside any average or trend, and read
coverage.window_satisfied: false as a shorter history than asked, not as a
missing kind. 🔴 Before saying how far back someone's data goes, read
coverage.more_available: true means this server can decrypt days OLDER
than first_day that your limit left behind — re-read with a larger
limit, or quote coverage.oldest_available as the real start of their
history. Never report a limit-shaped window as the extent of their data. |
| get_wrist_tempA | Decrypt recent sleeping wrist temperature samples — ABSOLUTE °C. ⚠️ These are absolute skin temperatures (~35.5-36.5 °C), NOT baseline
deltas — the legacy temperature_delta_celsius field name is a wire-
contract misnomer (kept for compatibility; prefer the honest twin
wrist_temperature_celsius). For cycle analysis, derive the deviation
yourself: reading minus that person's rolling baseline. One sample per
night. Use owner prefix to filter. Carries a coverage block: quote coverage.days_covered (distinct days, not
the row count) and coverage.span_days beside any average or trend, and read
coverage.window_satisfied: false as a shorter history than asked, not as a
missing kind. 🔴 Before saying how far back someone's data goes, read
coverage.more_available: true means this server can decrypt days OLDER
than first_day that your limit left behind — re-read with a larger
limit, or quote coverage.oldest_available as the real start of their
history. Never report a limit-shaped window as the extent of their data. |
| get_basal_energyA | Decrypt recent basal-energy-burned samples (Apple Watch BMR estimate, kcal).
Watch typically emits hourly samples; unlimited limit + daily aggregation
returns per-day BMR (~1500-2000 kcal for active young adults) + average.
Use owner prefix to filter by person. READ hours_covered BEFORE QUOTING ANY SINGLE DAY. Basal arrives as one
blob per hour, so a day the Watch spent off the wrist comes back as a
real-looking row that is short in exact proportion — 883 kcal at 12 of
24 hours is half a day of data, NOT a collapsed metabolism. Rows with
incomplete: true are already excluded from average_daily_basal_kcal
(average_over_days is its denominator); if you quote such a day, say
how many hours it covers. Carries a coverage block: quote coverage.days_covered (distinct days, not
the row count) and coverage.span_days beside any average or trend, and read
coverage.window_satisfied: false as a shorter history than asked, not as a
missing kind. 🔴 Before saying how far back someone's data goes, read
coverage.more_available: true means this server can decrypt days OLDER
than first_day that your limit left behind — re-read with a larger
limit, or quote coverage.oldest_available as the real start of their
history. Never report a limit-shaped window as the extent of their data. |
| get_total_energy_burnedA | TDEE (total daily energy expenditure) = basal + active per day, last N days. The truthful daily calorie burn from Watch's actual measurements — not
a formula. Diet targets need to aim BELOW this to lose weight (e.g.
eating avg_tdee - 500 = ~0.5 kg/week loss). Returns per-day breakdown
{day, basal_kcal, active_kcal, total_kcal, basal_missing, partial,
basal_hours_covered, basal_hours_expected, basal_incomplete} + average
TDEE. Three kinds of day are excluded from the average and each is
listed with its reason in average_excluded_days: today (partial,
still accumulating), days with no basal data (basal_missing), and
days whose Watch coverage was short (basal_incomplete — e.g. 16 of 24
hours). A short day's kcal is low in proportion to the hours it missed,
so including it drags the average down and, since the error is
one-directional, never cancels out. Quote average_tdee_kcal for diet
targets, and if you quote a single day, check basal_incomplete first.
Use owner prefix to filter by person. Carries a coverage block: quote coverage.days_covered (distinct days, not
the row count) and coverage.span_days beside any average or trend, and read
coverage.window_satisfied: false as a shorter history than asked, not as a
missing kind. 🔴 Before saying how far back someone's data goes, read
coverage.more_available: true means this server can decrypt days OLDER
than first_day that your limit left behind — re-read with a larger
limit, or quote coverage.oldest_available as the real start of their
history. Never report a limit-shaped window as the extent of their data. |
| get_vo2maxA | Decrypt recent VO2Max samples (Apple Watch cardiorespiratory fitness).
Unit: mL/(kg·min); higher = better. Male 20-29 reference: <35 poor,
35-42 fair, 42-46 good, 46-50 excellent, 50+ superior. Returns
newest-first records plus latest / peak / trough / average over the
window. Use owner prefix to filter by person. VO2Max is sparse (Watch
computes it during outdoor brisk walk/run bouts, days apart), so a
limit of 30 usually covers many months. Carries a coverage block: quote coverage.days_covered (distinct days, not
the row count) and coverage.span_days beside any average or trend, and read
coverage.window_satisfied: false as a shorter history than asked, not as a
missing kind. 🔴 Before saying how far back someone's data goes, read
coverage.more_available: true means this server can decrypt days OLDER
than first_day that your limit left behind — re-read with a larger
limit, or quote coverage.oldest_available as the real start of their
history. Never report a limit-shaped window as the extent of their data. |
| get_mindfulnessA | Decrypt recent daily mindfulness summaries (session count, total minutes).
Use owner prefix to filter by person. Carries a coverage block: quote coverage.days_covered (distinct days, not
the row count) and coverage.span_days beside any average or trend, and read
coverage.window_satisfied: false as a shorter history than asked, not as a
missing kind. 🔴 Before saying how far back someone's data goes, read
coverage.more_available: true means this server can decrypt days OLDER
than first_day that your limit left behind — re-read with a larger
limit, or quote coverage.oldest_available as the real start of their
history. Never report a limit-shaped window as the extent of their data. |
| list_metric_seriesA | List every series the trend / compare / correlate tools accept, with units. Call this BEFORE guessing a series name. Kinds with a richer shape
(sleep stages, workouts, strength sets, food, notes, symptoms, cycle) are
deliberately absent — flattening them to one number per day would answer a
question you did not ask; read them with their own get_* tool. |
| get_metric_trendA | Least-squares trend for one daily series: slope per day, endpoints, spread. series is a name from list_metric_series. days selects the newest N days
THAT HAVE DATA, not the last N calendar days — compare n_days with
span_days to see whether the history is dense or sparse.
Returns arithmetic only. slope_per_day is in the series' own unit per day and
carries no threshold, band or verdict; fewer than 3 days returns a null slope
with a reason rather than a number fitted to noise. Carries a coverage block over the days the arithmetic used: quote
coverage.days_covered and coverage.span_days beside any number here, and
read coverage.window_satisfied: false as a shorter history than you asked
for rather than as a missing kind. |
| compare_metric_periodsA | Compare the newest days days of a series against the days before them. "Before" means the next-oldest days WITH DATA, so a gap makes the earlier
window older rather than emptier — read previous.first_day / previous.last_day
to see which period you actually got, and quote them. Returns both windows' mean/median/min/max and the differences. It does not say
which window is better; that depends on the metric and on the person. Carries a coverage block over the days the arithmetic used: quote
coverage.days_covered and coverage.span_days beside any number here, and
read coverage.window_satisfied: false as a shorter history than you asked
for rather than as a missing kind. |
| correlate_metric_seriesA | Pearson r between two daily series, over days that have BOTH recorded. Days missing on either side are dropped, never interpolated and never read as
zero, so n_pairs is usually smaller than either series — quote it with r.
Fewer than 3 shared days returns null with a reason: any two points are
perfectly collinear, so a coefficient there is an artefact. The result carries a caveat field about causation. Repeat its substance in
your answer, and do not translate r into a word like "strong". Carries a coverage block over the days the arithmetic used: quote
coverage.days_covered and coverage.span_days beside any number here, and
read coverage.window_satisfied: false as a shorter history than you asked
for rather than as a missing kind. |