P247 Feature Spec: MET Minutes Tracking
P247 Feature Spec: MET Minutes Tracking
Version: 1.1 Date: 27 March 2026 Author: Myles Bruggeling, Founder Status: Ready for development Depends on: P247 Phase 2 HealthKit Sync (workout data flowing to backend) Origin: Early tester feedback (Egoteen, Stage 2 DM candidate, r/Semaglutide)
0. Summary
MET minutes (Metabolic Equivalent of Task × duration) is the most scientifically grounded measure of physical activity, used by the WHO to define their physical activity guidelines. No consumer fitness app surfaces it well. Apple Health records the raw data but only shows a daily MET range, never accumulated weekly totals or progress against WHO targets.
P247 will calculate, store, and display weekly MET minutes across all three layers (backend, admin dashboard, iOS app). This positions P247 as the app that speaks in health outcomes, not vanity metrics.
No new data sources required. The HealthKit sync payload already includes energy_kcal and duration_seconds per workout. MET minutes is derived from data we already ingest.
1. What Are MET Minutes?
MET (Metabolic Equivalent of Task) measures the energy cost of an activity relative to rest.
- 1 MET = energy expenditure at rest (sitting quietly)
- Walking = ~3.5 METs (3.5× resting effort)
- Running 10 min/km = ~9 METs
- Strength training = ~3-6 METs depending on intensity
- Cycling hard = ~8-12 METs
MET minutes = MET value × duration in minutes.
Example: A 45-minute strength session at 5 METs = 225 MET minutes.
WHO Physical Activity Guidelines (2020)
The WHO expresses recommendations in MET minutes per week:
| Weekly MET Minutes | WHO Classification | Plain Language |
|---|---|---|
| < 500 | Insufficient | Below minimum guidelines |
| 500-1000 | Sufficient | Meets recommended activity levels |
| > 1000 | Optimal | Additional health benefits observed |
The commonly quoted “150 minutes of moderate activity per week” is the simplified version of the 500 MET-minute target (moderate activity ≈ 3-6 METs, so 150 min × ~3.3 METs ≈ 500 MET minutes).
2. Calculation Method
2.1 Per-Workout MET Value
Calculate MET from data already present in the HealthKit sync payload:
MET = energy_kcal / (duration_hours × BMR_hourly)
Where:
energy_kcal=workout.energy_kcalfrom the sync payload (Apple Watch total energy burned for the workout)duration_hours=workout.duration_seconds / 3600BMR_hourly= athlete’s daily BMR / 24
BMR source (in priority order):
- HealthKit
basalEnergyBurneddaily value (already in sync payload asbasal_calories) - Calculated from athlete profile using Mifflin-St Jeor equation:
- Male: BMR = (10 × weight_kg) + (6.25 × height_cm) - (5 × age) + 5
- Female: BMR = (10 × weight_kg) + (6.25 × height_cm) - (5 × age) - 161
2.2 Per-Workout MET Minutes
met_minutes = MET × (duration_seconds / 60)
2.3 Timezone and Date Bucketing
All date boundaries and week boundaries use the athlete’s local timezone (from their profile, defaulting to Australia/Sydney). This applies to:
- Which date a workout belongs to (a 23:30 workout finishing at 00:15 belongs to the start date)
- Week boundaries (Monday 00:00 to Sunday 23:59:59 local time)
- Trend period calculations
2.4 Daily and Weekly Aggregation
daily_met_minutes = sum of met_minutes for all workouts on that date (athlete local TZ)
weekly_met_minutes = sum of daily_met_minutes for Monday-Sunday (ISO week, athlete local TZ)
Week definition: ISO weeks (Monday to Sunday), not rolling 7 days. The API period=7d parameter returns the current ISO week. The UI, charts, and trend data all align to Mon-Sun boundaries.
2.4 WHO Zone Classification
def classify_who_zone(weekly_met_minutes: float) -> str:
if weekly_met_minutes < 500:
return "insufficient"
elif weekly_met_minutes <= 1000:
return "sufficient"
else:
return "optimal"
2.5 Activity Type Breakdown
Group MET minutes by workout type. Map HealthKit workout types to P247 categories:
| P247 Category | HealthKit Workout Types |
|---|---|
| Strength | FunctionalStrengthTraining, TraditionalStrengthTraining, CrossTraining |
| Running | Running, TrailRunning |
| Cycling | Cycling, IndoorCycling |
| Rowing | Rowing |
| HIIT | HighIntensityIntervalTraining, MixedCardio |
| Swimming | Swimming, OpenWaterSwimming |
| Walking | Walking, Hiking |
| Other | Everything else |
2.7 Workout Deduplication
Dedupe key: HealthKit workout UUID (HKObject.uuid). The iOS app must include this in the sync payload as healthkit_uuid. If a workout with the same UUID already exists in workout_met, skip it (upsert on UUID). This prevents re-syncs from double-counting MET minutes.
If healthkit_uuid is missing (legacy or third-party data), fall back to matching on user_id + workout_type + start_time (within a 60-second tolerance window).
2.8 Energy Semantics
energy_kcal is total workout energy (Apple Watch totalEnergyBurned), which includes both active and basal expenditure during the workout. This is the correct input for MET calculation because MET is defined as total metabolic rate relative to resting metabolic rate. Using active-only energy would systematically underestimate MET values.
2.9 BMR Partial-Day Handling
basal_calories from HealthKit may be a partial-day value if the sync happens mid-day. Rules:
- If
basal_caloriesis present and the sync timestamp is after 20:00 local time, use it directly as the daily BMR - If
basal_caloriesis present but sync is before 20:00, do not extrapolate. Fall back to the Mifflin-St Jeor calculation from the athlete’s profile - If no profile data exists, use the 1800 kcal/day default
Rationale: extrapolating partial-day basal values introduces compounding error. A profile-based calculation is more reliable than a scaled-up partial reading.
2.10 Backfill Scope
On first deployment, backfill MET minutes from all existing workout data in the database. No time limit on how far back. If the workout history is incomplete (e.g., the athlete only started syncing recently), the trend data will simply show fewer weeks. The API response includes a data_from field indicating the earliest date with MET data.
2.11 Edge Cases
- Zero energy workouts: If
energy_kcalis 0 or null (some third-party apps don’t report energy), fall back to a standard MET value lookup table based on workout type. Use the Compendium of Physical Activities values (e.g., FunctionalStrengthTraining = 5.0 METs, Running = 8.0 METs). - Very short workouts: Exclude workouts under 5 minutes (likely accidental starts/stops).
- Very high MET values: Cap MET at 20.0 (anything higher indicates a calculation error, likely from incorrect energy or BMR data).
- Missing BMR: If no basal_calories and no profile data, use a default BMR of 1800 kcal/day (reasonable midpoint for active adults). Flag in the response that MET accuracy is reduced.
3. Backend Changes (app.p247.io)
3.1 Database Schema
Add to the daily metrics table (or create a new met_minutes table):
ALTER TABLE daily_metrics ADD COLUMN met_minutes_daily REAL DEFAULT NULL;
ALTER TABLE daily_metrics ADD COLUMN met_minutes_by_type JSON DEFAULT NULL;
-- Or create a dedicated table for per-workout MET data:
CREATE TABLE workout_met (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
healthkit_uuid TEXT, -- dedupe key (see section 2.7)
date TEXT NOT NULL, -- athlete local TZ date (see section 2.3)
workout_type TEXT NOT NULL,
p247_category TEXT NOT NULL,
start_time TIMESTAMP NOT NULL, -- UTC, fallback dedupe key
duration_seconds INTEGER NOT NULL,
energy_kcal REAL,
met_value REAL NOT NULL,
met_minutes REAL NOT NULL,
bmr_source TEXT NOT NULL, -- 'healthkit' | 'profile' | 'default'
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE UNIQUE INDEX idx_workout_met_uuid ON workout_met(healthkit_uuid) WHERE healthkit_uuid IS NOT NULL;
CREATE INDEX idx_workout_met_user_date ON workout_met(user_id, date);
3.2 Processing Pipeline
When workout data arrives via POST /sync/healthkit:
- For each workout in
payload.workouts[]: a. Look up athlete’s BMR (basal_calories from same day’s sync, or profile, or default) b. Calculate MET value (capped at 20.0) c. Calculate MET minutes d. Map workout type to P247 category e. Store inworkout_mettable - Update
daily_metrics.met_minutes_dailywith sum for that date - Update
daily_metrics.met_minutes_by_typewith JSON breakdown
3.3 New API Endpoint
GET /metrics/met-minutes
Query params:
period(optional):7d(default),30d,90d
Response:
{
"current_week": {
"total": 1245.0,
"target": 500,
"target_pct": 249.0,
"who_zone": "optimal",
"days_remaining": 2,
"by_type": {
"strength": 450.0,
"running": 540.0,
"cycling": 255.0
},
"by_day": [
{"date": "2026-03-21", "total": 225.0, "workouts": 1},
{"date": "2026-03-22", "total": 315.0, "workouts": 2},
{"date": "2026-03-23", "total": 180.0, "workouts": 1},
{"date": "2026-03-24", "total": 0.0, "workouts": 0},
{"date": "2026-03-25", "total": 270.0, "workouts": 1},
{"date": "2026-03-26", "total": 255.0, "workouts": 1},
{"date": "2026-03-27", "total": null, "workouts": 0}
]
},
"trend": [
{"week_start": "2026-03-17", "total": 1245.0, "who_zone": "optimal"},
{"week_start": "2026-03-10", "total": 980.0, "who_zone": "sufficient"},
{"week_start": "2026-03-03", "total": 1100.0, "who_zone": "optimal"},
{"week_start": "2026-02-24", "total": 870.0, "who_zone": "sufficient"}
],
"bmr_source": "healthkit",
"bmr_daily": 1854,
"data_from": "2026-02-20",
"timezone": "Australia/Sydney"
}
Nullability rules for by_day.total:
null= future date (no data expected yet)0or0.0= past date with no workouts recorded- Positive number = actual MET minutes for that date
This distinction lets the UI render future days as empty/greyed-out vs past rest days as zero-value bars.
Auth: `x-api-key` header (same as all P247 endpoints).
### 3.4 Brief Integration
Add MET minutes context to the `GET /briefs/today` response:
```json
{
"...existing brief fields...",
"met_minutes": {
"weekly_total": 1245.0,
"who_zone": "optimal",
"target_pct": 249.0,
"today_total": 255.0
}
}
This allows the daily brief text and AI coaching agent to reference MET minutes naturally.
3.5 AI Coaching Agent Context
Add to the coaching agent’s system prompt data injection (when generating responses):
MET Minutes Context:
- This week: {weekly_total} MET minutes ({who_zone})
- WHO target: 500-1000 MET minutes/week
- Target achievement: {target_pct}%
- Breakdown: {by_type summary}
- Trend: {4-week direction}
The agent can then reference MET minutes in coaching responses. Examples:
Optimal athlete: “You’ve hit 890 MET minutes this week with two days left. That’s well within WHO optimal range. Your strength sessions are contributing about 35% of that total, which is solid.”
Under-target athlete: “You’re at 320 MET minutes with one day left in the week. That’s below the 500 minimum the WHO recommends. Even a 30-minute brisk walk would add about 100 MET minutes.”
Over-reliance on one type: “1,200 MET minutes this week, 85% from running. Your cardiovascular load is high but you’re getting minimal stimulus from strength work. Worth considering a swap for one of tomorrow’s sessions.”
4. Admin Dashboard Changes
4.1 MET Minutes Card
Add a new card to the athlete dashboard view. Position: below the training load card.
Card layout:
┌─────────────────────────────────────────┐
│ MET Minutes This Week │
│ │
│ ┌───────┐ │
│ │ │ 1,245 MET min │
│ │ RING │ 249% of WHO target │
│ │ │ Zone: Optimal ✅ │
│ └───────┘ │
│ │
│ ┌─┐ ┌──┐ ┌─┐ ┌──┐ ┌──┐ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ · │ │ │ │ · · │
│ └─┘ └──┘ └─┘ └──┘ └──┘ │
│ Mon Tue Wed Thu Fri Sat Sun │
│ │
│ ■ Strength ■ Running ■ Other │
└─────────────────────────────────────────┘
Ring/Gauge:
- Red fill when < 500 (insufficient)
- Yellow fill when 500-1000 (sufficient)
- Green fill when > 1000 (optimal)
- Shows numeric total and percentage of 500 target
Bar Chart:
- 7 bars (Mon-Sun), stacked by P247 category
- Colour coding: Blue = Running/Cycling/Cardio, Orange = Strength, Grey = Other
- Empty bars for future days in the current week
4.2 MET Minutes Trend View
Below the weekly card, a 4-week trend line:
┌─────────────────────────────────────────┐
│ 4-Week Trend │
│ │
│ 1200 ┤ ╭──── │
│ 1000 ┤ ──────╮ ╭────╯ ── WHO │
│ 800 ┤ ╰─────╯ optimal│
│ 500 ┤─────────────────────────── WHO │
│ ┤ min │
│ 0 ┤ │
│ └──W1──W2──W3──W4────────── │
│ │
│ Green zone (>1000) shaded above │
│ Yellow zone (500-1000) shaded middle │
│ Red zone (<500) shaded below │
└─────────────────────────────────────────┘
5. iOS App Changes
5.1 HealthKit Sync Payload
One new field required: healthkit_uuid must be included in the workout sync payload. Everything else should already be present.
Verify that the workout payload matches this structure:
{
"workouts": [
{
"healthkit_uuid": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
"type": "FunctionalStrengthTraining",
"start": "2026-03-23T06:00:00+11:00",
"end": "2026-03-23T06:55:00+11:00",
"duration_seconds": 3300,
"energy_kcal": 420,
"avg_heart_rate": 145,
"max_heart_rate": 172,
"distance_m": null,
"source": "Apple Watch"
}
]
}
healthkit_uuid (new, required): The HealthKit workout’s UUID string. Used as the dedupe key to prevent re-syncs from double-counting MET minutes (see section 2.7).
let healthkitUuid = workout.uuid.uuidString
energy_kcal (existing): This is HKWorkout.totalEnergyBurned (total energy, not active-only). See section 2.8 for rationale.
let energyKcal = workout.totalEnergyBurned?.doubleValue(for: .kilocalorie())
5.2 Today Tab: MET Minutes Summary Card
Add a compact card to the Today tab, positioned below the main daily decision card.
Design:
┌─────────────────────────────────────────┐
│ 📊 MET Minutes │
│ │
│ This week: 1,245 WHO: Optimal ✅ │
│ ████████████████████░░░ 249% of target│
│ │
│ Strength 450 · Running 540 · Other 255 │
└─────────────────────────────────────────┘
Behaviour:
- Progress bar fills based on percentage of 500 target (caps visually at 200%)
- Colour matches WHO zone (red/yellow/green)
- Type breakdown shows top 3 categories
- Tap to navigate to full MET Minutes view (section 5.3)
Data source: GET /briefs/today → met_minutes object (see section 3.4)
5.3 Profile/Stats: Full MET Minutes View
Accessible from the Today card tap or from Profile > Stats.
Layout (scrollable):
┌─────────────────────────────────────────┐
│ ← MET Minutes │
│ │
│ ┌───────────────────────────────────┐ │
│ │ THIS WEEK │ │
│ │ │ │
│ │ ┌──────────┐ │ │
│ │ │ │ 1,245 │ │
│ │ │ RING │ MET minutes │ │
│ │ │ │ Optimal ✅ │ │
│ │ └──────────┘ │ │
│ │ │ │
│ │ WHO recommends 500-1000/week │ │
│ └───────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────┐ │
│ │ DAILY BREAKDOWN │ │
│ │ │ │
│ │ ┌─┐ ┌──┐ ┌─┐ ┌──┐ ┌──┐ │ │
│ │ │S│ │SR│ │S│ │R │ │SR│ │ │
│ │ │ │ │ │ │ │ · │ │ │ │ · │ │
│ │ └─┘ └──┘ └─┘ └──┘ └──┘ │ │
│ │ Mon Tue Wed Thu Fri Sat Sun │ │
│ │ │ │
│ │ ■ Strength ■ Running ■ Other │ │
│ └───────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────┐ │
│ │ 4-WEEK TREND │ │
│ │ │ │
│ │ 1200 ┤ ╭──── │ │
│ │ 1000 ┤ ────╮ ╭────╯ │ │
│ │ 800 ┤ ╰──╯ │ │
│ │ 500 ┤─────────────── target │ │
│ │ 0 ┤ │ │
│ │ └─W1──W2──W3──W4── │ │
│ │ │ │
│ │ Avg: 1,048 MET min/week │ │
│ └───────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────┐ │
│ │ WHAT ARE MET MINUTES? │ │
│ │ │ │
│ │ MET (Metabolic Equivalent of │ │
│ │ Task) measures how hard your │ │
│ │ body works relative to rest. │ │
│ │ MET minutes = intensity × │ │
│ │ duration. The WHO recommends │ │
│ │ 500-1000 MET minutes per week │ │
│ │ for health benefits. │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
Data source: GET /metrics/met-minutes?period=30d
5.4 Period Selector
Allow the user to toggle between 7-day, 30-day, and 90-day views on the full MET Minutes screen. The trend chart adjusts accordingly (weekly bars for 30d, monthly bars for 90d).
6. Implementation Checklist
Backend (app.p247.io)
- Add
workout_mettable withhealthkit_uuidunique index (schema per section 3.1) - Add MET calculation to workout processing pipeline (section 3.2)
- Implement idempotent upsert: skip if
healthkit_uuidexists, fallback dedupe onuser_id + type + start_time ±60s(section 2.7) - Implement timezone-aware date bucketing using athlete profile timezone (section 2.3)
- Implement BMR partial-day handling: use profile calc if sync before 20:00 local (section 2.9)
- Implement MET fallback lookup table for workouts missing energy data
- Add
met_minutes_dailyandmet_minutes_by_typeto daily_metrics - Build
GET /metrics/met-minutesendpoint with ISO week boundaries (section 3.3) - Add
met_minutesobject toGET /briefs/todayresponse (section 3.4) - Add MET context to AI coaching agent prompt injection (section 3.5)
- Backfill MET minutes from all existing workout data (section 2.10)
Admin Dashboard
- MET Minutes weekly card with ring/gauge (section 4.1)
- Daily stacked bar chart by activity type (section 4.1)
- 4-week trend line with WHO zone shading (section 4.2)
iOS App
- Add
healthkit_uuid(HKObject.uuid) to workout sync payload (section 5.1) - Verify
energy_kcal(totalEnergyBurned, not activeEnergyBurned) is included in workout sync payload (section 5.1) - Add MET Minutes summary card to Today tab (section 5.2)
- Build full MET Minutes view in Profile/Stats (section 5.3)
- Period selector (7d/30d/90d) (section 5.4)
- Educational “What are MET minutes?” section
7. MET Fallback Lookup Table
For workouts where energy_kcal is null or 0, use standard MET values from the Compendium of Physical Activities:
| HealthKit Workout Type | Default MET Value |
|---|---|
| Walking | 3.5 |
| Hiking | 6.0 |
| Running | 8.0 |
| TrailRunning | 9.0 |
| Cycling | 7.5 |
| IndoorCycling | 6.5 |
| Swimming | 6.0 |
| OpenWaterSwimming | 7.0 |
| Rowing | 7.0 |
| FunctionalStrengthTraining | 5.0 |
| TraditionalStrengthTraining | 5.0 |
| CrossTraining | 6.0 |
| HighIntensityIntervalTraining | 8.0 |
| MixedCardio | 7.0 |
| Yoga | 3.0 |
| Pilates | 3.0 |
| Flexibility | 2.5 |
| Dance | 5.5 |
| Elliptical | 5.0 |
| StairClimbing | 9.0 |
| Other | 4.0 |
Source: Ainsworth BE et al., “Compendium of Physical Activities” (2011 update). Values are conservative midpoints for each activity category.
8. Testing
Backend
- Basic calculation: Submit a workout with
energy_kcal: 420,duration_seconds: 3300, athlete BMR 1854 kcal/day. Expected MET = 420 / (0.917 × 77.25) ≈ 5.93. Expected MET minutes = 5.93 × 55 ≈ 326. - Weekly aggregation: Submit workouts across 5 days. Verify
GET /metrics/met-minutesreturns correct weekly total and breakdown. - WHO zones: Verify correct zone classification at boundary values (499, 500, 1000, 1001).
- Fallback METs: Submit a workout with
energy_kcal: null. Verify the lookup table value is used. - MET cap: Submit a workout with unrealistically high energy. Verify MET is capped at 20.0.
- Short workout exclusion: Submit a workout with
duration_seconds: 120. Verify it is excluded from calculations. - Idempotency: Submit the same workout twice (same
healthkit_uuid). Verify MET minutes are counted only once. - Fallback dedupe: Submit a workout without
healthkit_uuid, sameuser_id + type + start_time. Verify no duplicate. - Timezone bucketing: Submit a workout at 23:50 AEST finishing at 00:10 AEST. Verify it’s bucketed to the start date.
- ISO week boundaries: Verify
GET /metrics/met-minutesreturns Mon-Sun weeks, not rolling 7 days. - BMR partial-day: Sync with
basal_caloriesat 14:00 local. Verify profile-based BMR is used instead of the partial value. - Nullability: Verify
by_day.totalreturnsnullfor future dates and0for past dates with no workouts.
iOS
- Card renders: Verify MET Minutes card appears on Today tab when data is available.
- Empty state: Verify graceful handling when no workout data exists (show “No workouts this week”).
- Tap navigation: Verify Today card tap navigates to full MET Minutes view.
- Period selector: Verify 7d/30d/90d toggle updates the chart correctly.
- WHO zone colours: Verify ring/progress bar colour matches zone (red < 500, yellow 500-1000, green > 1000).
9. Future Considerations (Not in Scope)
- MET minutes goals: Allow athletes to set custom weekly MET minute targets beyond WHO guidelines.
- Intensity distribution analysis: Show percentage of MET minutes from moderate (3-6 METs) vs vigorous (>6 METs) activity, mapped to the WHO recommendation of at least 75 minutes vigorous per week.
- MET minutes in push notifications: “You’re 150 MET minutes away from your weekly target. A 30-min run would get you there.”
- MacroFactor expenditure cross-reference: If/when MacroFactor exposes an API, compare their calculated expenditure against MET-derived expenditure for accuracy validation.
- Apple Watch complication: Show weekly MET minutes progress as a Watch complication.
Changelog
| Version | Date | Changes |
|---|---|---|
| 1.0 | 27 Mar 2026 | Initial spec |
| 1.1 | 27 Mar 2026 | iOS team review feedback: locked week definition (ISO Mon-Sun, not rolling), timezone bucketing rule, workout dedupe via HealthKit UUID, energy semantics (total not active-only), BMR partial-day handling, backfill scope, API nullability rules. Added sections 2.3, 2.7-2.10. Updated schema, checklist, and test scenarios. |