| 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. A kind under kinds_not_checked could
not be counted this run; it is neither empty nor full.
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.
binding — the local pairing state (which account, which server, whether
demo mode is serving synthetic data). No keys or tokens.
This runs a cloud round trip, so it takes a moment. |
| get_symptomsA | Decrypt recent symptoms locally, grouped by data owner — both sources. SENSITIVE. Each entry in owners carries owner_user_id and two lists:
· days — symptoms imported from Apple Health (cramps, headache, fatigue,
coughing…), one row per day with each sample's severity
(mild/moderate/severe/present/…). These reach this server only when the
user opted in on iOS — their own AI toggle, or the partner-AI toggle for
a partner's. symptom_counts counts logged DAYS per type.
· reported — episodes a person reported themselves, through
log_symptom or the Vaultbeat app: entry_id, symptom_type,
display_name (their own words), severity, local_date, onset_at /
end_at (end_recorded: false means no end was logged — ask, do not
assume it is still going), duration_minutes, body_location,
triggers, note. reported_counts counts EPISODES per type.
A type that exists in Apple Health is spelled the same in both lists
(healthkit_type: true), so "headache" from either source is one symptom.
To correct or remove a reported entry use update_symptom /
delete_symptom with its entry_id; imported days are edited in Apple Health.
Stays on-device, never re-exported. Returns YOUR symptoms by default; partner=true returns your partner's
Apple Health days only (if they share symptoms with you from their own
app). A partner's reported episodes never reach you: the app and
log_symptom seal them for their owner's own AI alone, so an empty
reported under partner=true means "not shareable", not "none logged". Carries a coverage block: quote coverage.days_covered (distinct days, not
the row count) and coverage.span_days beside any average or trend.
coverage.window_satisfied: false alone does NOT mean a short history: it is
also false when older days exist beyond your window. 🔴 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. Returns notes about YOU by default. partner=true returns notes about your
partner: the ones they wrote themselves and shared, plus the ones your AI
recorded about them (about: "partner", written with
log_note_append(partner=true)). Carries a coverage block: quote coverage.days_covered (distinct days, not
the row count) and coverage.span_days beside any average or trend.
coverage.window_satisfied: false alone does NOT mean a short history: it is
also false when older days exist beyond your window. 🔴 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.
duplicate_sessions, when present, lists stored entries that only repeat
another session of the same day; they are already left out of sessions
and session_count, so do not add their volume back. Carries a coverage block: quote coverage.days_covered (distinct days, not
the row count) and coverage.span_days beside any average or trend.
coverage.window_satisfied: false alone does NOT mean a short history: it is
also false when older days exist beyond your window. 🔴 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 running
uvx vaultbeat-apple-health@latest bind in a terminal.
|
| 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 by running uvx vaultbeat-apple-health@latest bind
in a terminal. |
| get_food_logA | Decrypt daily food-intake logs locally (newest first). limit counts LOGGED days: by default the newest 14 days that have a
log — not a calendar fortnight. Days with nothing logged are skipped, so
the 14 can reach further back; coverage.span_days and
coverage.days_missing_in_span say how far. since / until
("YYYY-MM-DD", local days, both inclusive) read a calendar window
instead — "what did I eat in the first week of August" — and then
limit applies only if you pass it. Each day's date is its local
calendar day (the same as local_date, and the form log_food_entry
and get_strength_log use). A logged day is roughly 0.5-1k
tokens (more with notes, and Chinese text counts more), so a month can
already be past what a client takes in one result — it then comes back
as result_too_large; ask for fewer days or a narrower window.
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.
coverage.window_satisfied: false alone does NOT mean a short history: it is
also false when older days exist beyond your window. 🔴 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. Symptoms do NOT go here: use log_symptom, one call per symptom. A
note is free text nothing can be matched against; a symptom entry keeps
its type, severity and timing so it can later be lined up with sleep,
food and heart rate. A note is for what surrounds it (mood, events, how
a day went). 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.
partner=true records a note ABOUT your partner ("she had a stomach ache
this morning"). It is stored in YOUR account and sealed to you and this
machine only — your partner's account is never touched, and it never
mixes into your own notes: plain reads leave it out, get_notes(partner=true)
returns it. Default false = a note about you.
|
| 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. The second note of a day is the normal case, not the exception — a
morning event and an evening one — and log_note would replace the
first with the second. Symptoms do NOT go here: use log_symptom, which
keeps type, severity and timing as fields a later analysis can match. 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. partner=true records a note ABOUT your partner ("she had a stomach ache
this morning"). It is stored in YOUR account and sealed to you and this
machine only — your partner's account is never touched, and it never
mixes into your own notes: plain reads leave it out, get_notes(partner=true)
returns it. Default false = a note about you.
|
| log_symptomA | Record one symptom the person reports — one call per symptom (agent write). This is where "我肚子疼" / "headache since lunch" / "拉肚子了" goes, not a
note: each episode is kept as fields (type, severity, timing, place,
suspected causes) so a later read can line it up with sleep, food, heart
rate and wrist temperature. Two symptoms at once are two calls. This tool
only ever adds; it cannot change or delete anything already recorded. symptom_type: a short English token. Use the Apple Health name when one
fits — abdominal_cramps, bloating, constipation, diarrhea, heartburn,
nausea, vomiting, headache, dizziness, fatigue, fever, chills, coughing,
sore_throat, runny_nose, lower_back_pain, chest_tightness_or_pain,
shortness_of_breath, acne, night_sweats… — so it merges with what Apple
Health imports; otherwise a plain descriptive one (rectal_bleeding,
stomach_spasm, eye_strain). Any spelling is folded to one form.
display_name: the person's own words, e.g. "便血" / "胃痉挛".
severity: mild | moderate | severe | unspecified. Use unspecified
unless they said how bad it is — never grade it for them.
onset_at: when it began, ISO 8601 with their UTC offset, e.g.
"2026-09-30T10:49+08:00". Omit it only for something happening NOW (it
defaults to the current time). If they named a day but no time
("昨天头疼"), pass that day as a bare "2026-09-29" — do not invent an hour.
end_at: when it eased, if they said; add it later with update_symptom.
date: the local day to file it under, only when it differs from the
onset's own day.
body_location, note: free text, only what they said.
triggers: what THEY suspect caused it, as short tokens
(["bbq_dinner", "spicy_food"], ["sleep_deprivation"]). Record their
suspicion; do not add causes of your own.
Returns entry_id (keep it for update_symptom / delete_symptom) and
the entry as read back. Read everything with get_symptoms. Sealed for
the owner and this machine only, end to end, before it leaves here. |
| update_symptomA | Change fields of one reported symptom (agent write) — e.g. it eased. The common call is update_symptom(entry_id, end_at="…") when the person
says it has stopped, or a new severity when it got worse. Only the
fields you pass change; omitted ones keep their value. Passing a field
OVERWRITES it: note replaces the old note rather than adding to it, and
triggers replaces the whole list — read the entry first and send the
combined list if you mean to add one. "" clears a text field or a
mistaken end_at; [] clears triggers. entry_id comes from log_symptom or from get_symptoms' reported
list. Symptoms imported from Apple Health cannot be edited here.
Returns previous — the old value of every field this call changed — and
the entry as read back. |
| delete_symptomA | Erase one reported symptom that was logged by mistake (agent write). Only for an entry that should never have existed — a duplicate, the wrong
person, a symptom they did not have. A symptom that simply ended is NOT
deleted: record its end with update_symptom(entry_id, end_at=…). The entry's content is overwritten in the cloud, so it disappears from
get_symptoms and from the app. The result carries deleted_entry — the
full entry as it was — so a mistaken delete can be logged again with
log_symptom. Symptoms imported from Apple Health cannot be deleted here. |
| 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_metric series "weight_kg"). Whether it also reaches Apple Health depends on an
iOS setting, OFF by default: the MCP tab → "Allow AI to update Apple
Health" (app 1.2.8 and earlier: Settings → Data & AI). 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. Reads YOUR cycle by default; if the cycle being
asked about is your partner's, pass partner=true (it reaches this server
only if they share it). Carries a coverage block: quote coverage.days_covered (distinct days, not
the row count) and coverage.span_days beside any average or trend.
coverage.window_satisfied: false alone does NOT mean a short history: it is
also false when older days exist beyond your window. 🔴 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_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 ~3k
characters; see the SIZE note below.
· get_sleep_nights for anything spanning time — "how did I sleep
this week/month/year", patterns, worst nights. One compact row per
night, a whole year in one read. 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.
· get_metric with the sleep series (bedtime_minutes,
deep_sleep_minutes, awakenings, ...) for averages computed for you. (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). Reads
YOUR nights by default; partner=true reads your partner's shared sleep. limit = the newest N nights, 2 by default. To read a particular night,
pass its date as since and until ("YYYY-MM-DD", the local day the
night ENDS on — the local_date / date column of get_sleep_nights);
a wider since / until reads every night in that window, and limit
then applies only if you pass it.
⚠️ SIZE: each night is ~3k characters as returned (a week ~21k). 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.
coverage.window_satisfied: false alone does NOT mean a short history: it is
also false when older days exist beyond your window. 🔴 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_sleep_nightsA | Every night as one compact row — the tool for reading a sleep HISTORY. One row per night, newest first: date, weekday, bed and wake time, time
asleep, deep / REM / core / awake minutes, deep and REM share, number of
awakenings, longest unbroken stretch of sleep, heart and breathing rate
while asleep, and any OTHER sleep that day (naps, the second half of a
broken night) — so a day's total sleep is asleep plus other. Column names come once in columns; each row is the
values in that order, so a whole year is ~40k characters. Read it when
the question is about a period or a pattern: weekends vs weekdays,
drifting bedtimes, which nights were worst, what changed after a date. Which sleep tool:
· THIS one for any span of time.
· get_metric with the sleep series (sleep_minutes, bedtime_minutes,
deep_sleep_minutes, awakenings, ...) when you want the arithmetic —
weekly/monthly averages computed for you, from these same rows.
· get_sleep_detail for one or two nights in depth (stage bands,
per-stage vitals). limit = newest N nights (default 120; pass 400 for everything, and
check coverage.more_available). since = "YYYY-MM-DD" keeps every
night on or after that day — use it for "last month" rather than
guessing a limit; with since, limit applies only if you pass it.
Read legend once. Five flags, comma-separated when a night has several:
unworn means sleep was never measured that night (not zero sleep),
motion_inferred means phone motion guessed it (leave it out of averages,
as get_metric does), daytime means the day's only sleep was a daytime
nap (real, but not a night — out of get_metric's nightly series, still
a row here), short means a main sleep under 3 h with
no stages — a nap or a partly recorded night, left out of get_metric's
sleep-duration and clock series — and no_stages means the total is
known but the stage columns are null. A null anywhere is "not measured",
never zero. Reads YOUR nights; partner=true reads your partner's
shared sleep. Carries a coverage block: quote coverage.days_covered beside any
average, and read coverage.more_available before saying how far back
the history goes. |
| get_workoutsA | Decrypt recent workout sessions (type, duration, calories, distance).
Reads YOUR workouts; workouts are never shared between partners, so this
tool has no partner option. Carries a coverage block: quote coverage.days_covered (distinct days, not
the row count) and coverage.span_days beside any average or trend.
coverage.window_satisfied: false alone does NOT mean a short history: it is
also false when older days exist beyond your window. 🔴 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_user_profileA | Read YOUR health profile: biological sex, age, date of birth, height. Use it before anything that depends on sex, age or height — basal
metabolic rate, heart-rate zones, VO2 max bands, BMI. It is never shared
between partners, so this tool has no partner option. profile.sex_source says where the sex came from: "chosen" (the person
picked it in the app) or "apple_health" (a field in Apple Health they may
never have looked at). Every field may be null; profile itself is null
when nothing was uploaded, and the reply's note explains why that
cannot be narrowed down. Never fill a null in with a guess.
A profile is not a series: its coverage.days_covered is 0 by
construction, and the reply carries no upload time — the stored row is
replaced in place on every edit, so nothing here says how current the
values are. |
| get_metricA | Read any one-number-per-day health series: steps, resting HR, HRV, weight, … series takes ONE name, a LIST of names, or nothing (= every series).
Names come from list_metric_series, which also says how much data backs
each — call it first rather than guessing. Activity is five series
(steps, active_energy, exercise_minutes, stand_minutes, distance_meters);
pass them as a list to get the whole day in one call.
days = the newest N days THAT HAVE DATA, per series (not N calendar days).
For a CALENDAR window — "last month", "this semester", "July" — pass
since / until ("YYYY-MM-DD", either or both) instead; days is then
ignored and the reply names the range it used.
aggregation is computed here, not by you: avg / sum / min / max /
latest over the window, or none (default) for the per-day points only.
sum is refused on state measurements (resting HR, weight, VO2max) — it
has no meaning there. granularity = day (default), week (ISO weeks)
or month; week/month return buckets aggregated with aggregation
(avg when none), each with days_with_data beside days_in_period.
period_in_progress: true marks the current, unfinished week/month and
clipped_by_window: true the oldest one your days cut through — never
compare either against a full period as if it were one.
granularity = weekday groups the window by day of the week (Mon..Sun)
— the direct answer to "weekends vs weekdays" and to social jet lag. Read
its weekday_note: sleep is dated by the morning it ends, so Sat is the
Friday-night sleep.
Honesty the reply already does for you, so do not undo it:
· On cumulative series (steps, energy, water) today is still accruing —
it is marked partial: true and excluded from every aggregate.
· excluded_days names days dropped because their data was short (basal
energy with the Watch off the wrist, TDEE without basal), with reasons.
· Missing days are absent, never zero. Each series' points (or buckets) is a table: columns names the
fields once and every entry of rows is one day (or period) in that
order — e.g. columns: ["date", "value", "partial"]. The flags above are
columns; a day without one has null there. Each series carries its own coverage block: quote coverage.days_covered
and coverage.span_days beside any number. 🔴 Before saying how far back
someone's data goes, read coverage.more_available: true means older days
exist that your days left behind — ask for more, or quote
coverage.oldest_available. Never report a window as the extent of their data. Reads YOUR data by default.
Pass partner=true for your partner's — of these series only sleep, water
and weight (with body composition) can be shared, so every other series
comes back empty for a partner by design. Sleep is here too — duration, timing (bedtime_minutes, wake_minutes,
sleep_midpoint_minutes), stages, awakenings, heart/breathing rate while
asleep; the nights behind those numbers are get_sleep_nights. Richer
kinds have their own tools: workouts, strength, food, notes, symptoms, cycle,
and the sex / age / height profile (get_user_profile).
Samples within a day (HRV spikes) → get_intraday. Trend / period
comparison / correlation → get_metric_trend, compare_metric_periods,
correlate_metric_series. |
| get_intradayA | Samples WITHIN a day, for the series that record several per day (HRV). granularity:
"hourly" (default) — one row per hour that HAS samples (UTC hour
boundaries, shown in local time), the mean SDNN of that hour and its
sample_count. Right for "how did my HRV move through the day".
"raw" — one row per sample, newest first.
The Watch measures HRV a handful of times a day, not continuously (more
during a Breathe session), so most days have about 8-14 rows either way.
limit counts ROWS, not days: the default 168 is roughly two to three
weeks, not one. How far back the history goes depends on what this
phone uploaded, not on a fixed window — read coverage.oldest_available
and coverage.more_available. For one number per day use get_metric.
Reads YOUR data only: HRV is never shared between partners. rows is a table in columns order (local_time, sdnn_ms, plus
sample_count for hourly). Carries a coverage block: quote
coverage.days_covered (distinct days, not the row count) beside any number.
|
| list_metric_seriesA | Every series, with how much data actually backs each one. Call this BEFORE guessing a series name, and before concluding a kind is
empty. Each row carries rows / first_date / last_date / latest, so
"does this person track VO2max at all" is answered here in one call instead
of by reading the kind and getting nothing back. rows = the days that HAVE a value for that series — counted the way
get_metric reads it, so rows: 2 means get_metric can return at most 2
points. A kind's series can differ: a partner with 14 weigh-ins may have
a BMI on only 2 of those days.
cumulative: true marks the series whose daily value ACCRUES over the day
(steps, energy, water). On those the newest value is routinely a partial day
and must not be compared against completed ones — that is what makes "steps
are down today" wrong at 9am. Measurements of a state (resting HR, VO2max)
carry false and need no such care.
Sleep appears here as numbers (duration, timing, stages, awakenings,
vitals); read the nights themselves with get_sleep_nights. Kinds with a
richer shape (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 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. since /
until ("YYYY-MM-DD") fit a calendar window instead.
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.
coverage.window_satisfied: false alone does NOT mean a short history — it is
also false when older days exist beyond the window; coverage.more_available
tells the two apart. |
| compare_metric_periodsA | Compare two periods of one series: the newest days vs the days before, or two named windows. To compare CALENDAR periods — "this semester vs the summer", "before and
after I started training" — pass period and baseline, each
"YYYY-MM-DD..YYYY-MM-DD" (an open end is allowed: "2026-09-01.."). days
is then ignored. Without them, the rule below applies. "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.
coverage.window_satisfied: false alone does NOT mean a short history — it is
also false when older days exist beyond the window; coverage.more_available
tells the two apart. |
| correlate_metric_seriesA | Pearson r between two daily series, over days that have BOTH recorded. lag_days pairs series_a on each day with series_b that many days
LATER (negative = earlier): "does a short night show up in HRV the day
after" is sleep vs HRV with lag_days=1. Sleep is already dated by the
morning it ends, so lag 0 is "last night vs today". since / until
("YYYY-MM-DD") restrict to a calendar window.
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.
coverage.window_satisfied: false alone does NOT mean a short history — it is
also false when older days exist beyond the window; coverage.more_available
tells the two apart. |