A user’s sleep score is 65 today. It was 68 yesterday and 62 the day before. Look at any single day and there’s nothing to act on — that’s normal variance. Look at the last four weekly averages — 75, 72, 67, 65 — and the picture changes entirely. Sleep is sliding. The score itself is unremarkable; the trajectory is the story.
Trends are designed for that second view. They turn point-in-time scores, factors, and biomarkers into directional signals — increasing, decreasing, or stable — so your product can react to real patterns instead of overreacting to a single bad night.
| State | What It Means |
|---|---|
| Increasing | Meaningful upward movement over the rolling window |
| Stable | No significant change — the metric is holding |
| Decreasing | Meaningful downward movement over the rolling window |
The state alone doesn’t tell you whether the movement is good. A decreasing resting heart rate is a positive trend. A decreasing step count is not. Direction is one half of the answer; polarity is the other. Each trend includes an isHigherBetter field that resolves this — more on that below.
At a glance: A trend is computed at the end of each week from a rolling 4-week window of weekly averages. Output includes a state label, the four weekly values, the percent change between consecutive weeks, and the polarity metadata you need to interpret it.
How Trends Are Calculated
Each trend is produced from the last four complete weeks of data for a given metric. Sahha:
- Takes the rolling 4-week window
- Computes a weekly average for each week
- Compares the latest week to the previous week to determine direction
- Applies a metric-specific significance threshold so noise doesn’t get labeled as change
The significance threshold is the part that makes trends useful as triggers. A single noisy week — a vacation, a sick day, a tracked outlier — won’t flip the state on its own. The state captures real movement, not statistical wiggle. This is why branching on state is much safer than branching on raw percentChangeFromPrevious.
A trend is regenerated every week on a rolling basis, so the 4-week window slides forward with time. The latest week’s average becomes the most recent reference point; the oldest week falls off.
The Output
Every trend is scoped to a single profile and a single metric. Here’s a worked example for a user whose sleep has been gradually declining:
{
"type": "trend",
"category": "score",
"name": "sleep",
"state": "decreasing",
"isHigherBetter": true,
"valueRange": [0.0, 1.0],
"unit": "index",
"periodicity": "weekly",
"trendStartDateTime": "2026-04-01T00:00:00Z",
"trendEndDateTime": "2026-04-29T00:00:00Z",
"data": [
{ "startDateTime": "2026-04-01", "value": 0.75, "percentChangeFromPrevious": null },
{ "startDateTime": "2026-04-08", "value": 0.72, "percentChangeFromPrevious": -0.04 },
{ "startDateTime": "2026-04-15", "value": 0.67, "percentChangeFromPrevious": -0.07 },
{ "startDateTime": "2026-04-22", "value": 0.65, "percentChangeFromPrevious": -0.03 }
],
"createdAtUtc": "2026-04-29T09:30:00Z"
}Three layers to use:
state— for decisions. The classification already accounts for significance, so you can branch on it directly without computing your own thresholds.data— for charts. Four weekly points are enough for a sparkline that shows trajectory at a glance, and they’re already aggregated for you.percentChangeFromPrevious— for copy that needs specifics (“down 14% over the past month”) and for in-depth analysis. The first entry is alwaysnull— there’s no prior week to compare it to.
The category field tells you whether the trend is for a score, a factor, or a biomarker. valueRange and unit tell you how to render the underlying values (a sleep score is [0, 1] in index units; resting heart rate is unbounded in bpm).
Polarity: Reading State Together with isHigherBetter
increasing isn’t always good. decreasing isn’t always bad. Whether a trend is favorable depends on the metric — and that’s what isHigherBetter captures.
isHigherBetter | Increasing means… | Decreasing means… |
|---|---|---|
true (sleep duration, HRV, steps, sleep regularity, wellbeing score) | improvement | regression |
false (resting heart rate, sleep debt, extended inactivity) | regression | improvement |
null (walking strain capacity, exercise strain capacity) | informational only | informational only |
The null case is worth understanding. Strain capacity factors aren’t “good” when high or “bad” when low — they reflect how much load the user’s body is currently absorbing. A high walking strain capacity isn’t praiseworthy on its own; it just means the user has been doing more low-intensity activity recently. Don’t auto-frame these trends as positive or negative — treat them as context the rest of the system can use.
In code: branch on the combination of state and isHigherBetter to decide whether a trend warrants celebration, intervention, or no action at all.
function isFavorable(trend) {
if (trend.isHigherBetter === null) return null;
if (trend.state === "stable") return null;
return (trend.state === "increasing") === trend.isHigherBetter;
}That single function is enough to drive most product logic — favorable trends celebrate, unfavorable trends intervene, null informs.
What’s Eligible
Trends today cover three categories of metrics:
- Scores (5) —
activity,sleep,readiness,wellbeing,mental_wellbeing. The most common starting point, since one trend per score gives you five high-level signals per user. - Factors — the explainable drivers inside scores. Sleep factors (duration, regularity, debt, continuity, circadian alignment, physical recovery, mental recovery), activity factors (steps, active hours, active calories, intense activity duration, floors climbed, extended inactivity, activity regularity), and recovery factors (resting heart rate, heart rate variability, walking strain capacity, exercise strain capacity).
- Biomarkers — depending on availability and eligibility.
The supported metric set continues to grow. Refer to the Trends docs page for the live, complete list and the latest schema details — that’s the source of truth.
When to Use Trends vs Raw Scores
Use scores for display. Use trends for decisions.
A daily score answers “where are they today?” — perfect for the front of an app screen, a daily card, a number to hand to the user. But scores fluctuate. Triggering an email or a journey on a single day is fragile.
A trend answers “what’s actually happening over the last few weeks?” — and the answer is robust enough to drive automation. If your product is deciding whether to send a re-engagement email, surface a recovery journey, or escalate to a coach, a decreasing trend is a much safer trigger than a single low day.
A simple rule of thumb: scores in the UI, trends in the product logic. Show the score so the user has a number to react to today; branch on the trend so your system reacts to weeks-long patterns, not to noise.
Working with the 4-Week Series
The data array is a small but rich artifact. Four weeks isn’t enough for fancy time-series analysis, but it is enough for two important product surfaces:
Sparklines. Four points show direction at a glance without crowding the UI. A small inline chart next to a score or factor lets users see the trajectory without leaving the screen they’re on.
Recap copy. The first entry has percentChangeFromPrevious: null, but the remaining three give you week-over-week deltas you can fold into messaging: “your sleep duration has declined 11% over the past three weeks.” That reads better than a single percentage and grounds the message in the actual data.
If you want a single percent change to summarize the whole window — say, for a chart label — the most useful number is the change from the first valid week’s value to the latest week’s value, not the sum of the per-week deltas (which compound).
Missing Data
A user’s history isn’t always continuous. Trends handle that gracefully:
- A skipped week (no data for that period) is excluded from the rolling window. The trend still computes from the remaining weeks.
- Minimum two valid weeks are needed for a trend to be produced at all. Below that, the API returns no trend for that metric.
- The first entry’s
percentChangeFromPreviousis alwaysnull— there’s no prior week to compare against. This is structural, not a missing-data issue.
In product logic, always check whether a trend exists before branching on it. The absence of a trend almost always means insufficient history — don’t treat it as “stable” or fire any logic on it. A user with two weeks of patchy data is in a different state from a user with four weeks of consistent data showing no change.
Production tip: When you store the latest trend per metric per user, also store the trendEndDateTime. If the timestamp falls more than a week or two behind the current date, the trend is stale and the user has likely stopped contributing data — useful information in itself for re-engagement logic.
Limits Today
- Cadence: weekly. Trends are recalculated at the end of each rolling window, not in real time.
- Window: fixed 4-week rolling period.
- Minimum data: two valid weeks within the window.
- No customization: window length and cadence are fixed across all trends today.
What’s next: Monthly trend periodicity is on the product roadmap. The same state / data / percentChangeFromPrevious model will apply at a monthly cadence, giving you a longer-horizon view alongside the weekly one. Build product logic against the state field rather than hard-coded weekly assumptions, and the eventual transition will be seamless.
Further Reading
- Sahha Insights Explained — when to use trends vs comparisons, and how the two pair together
- Comparison Insights Explained — the daily benchmarking insight that complements trends
- What are Health Scores and Score Factors — the underlying metrics trends are computed from
- Trends — Docs — full eligible metric list, schema, API reference