`.pose` file format specification
A .pose file is a single JSON document containing everything a
consumer needs to track an movement. This page is the byte-by-byte
spec.
The Dart authority is
Movement
(packages/pose_flow/lib/src/models/unified_exercise.dart). Anything
the docs and the model disagree about, trust the model.
Top-level shape
{ "id": "squat", "version": "1.0.0", "name": "Squat", "category": "fitness", "cameraAngle": "front", "formatVersion": 2, "createdAt": "2026-05-27T10:00:00.000Z", "positions": [], "phaseConfigs": [...], "trackingPoints": [...], "formRules": [...], "repDetection": {...}, "angleBands": {...}, "repScoring": {...}, "metadata": {...}}Required fields
| Field | Type | Notes |
|---|---|---|
id | string | Stable identifier, kebab-case recommended. |
version | string | Author-supplied semver (NOT the format version). |
name | string | Display name. |
positions | array | Reference pose frames for the optional vector-matching rep-counting mode. Empty for .pose files authored in PoseFlow Studio. |
repDetection | object | How a rep is counted. See below. |
createdAt | ISO 8601 string | UTC timestamp. |
Optional fields
| Field | Type | Default | Notes |
|---|---|---|---|
formatVersion | integer | 1 | Set to 2 for .pose files. Consumers branch on this. |
category | string | 'fitness' | Domain: fitness, rehab, mobility, etc. |
cameraAngle | 'front' | 'side' | 'any' | 'front' | High-level authoring hint. NOT a runtime lens selector. |
phaseConfigs | array | [] | Phases for the rule-based path. Required when positions is empty. |
trackingPoints | array | [] | Per-frame measurement channels. |
formRules | array | [] | Form analysis rules. |
angleBands | object | {} | Per-bucket measurement bands. Keyed by bucket id. |
repScoring | object | disabled | Anti-cheat rep quality gate. |
metadata | object | {} | Author-defined. CMS bakes its fields under metadata.cms.*. |
phaseConfigs[i]
{ "id": "down", "name": "Down", "type": "transition", "trackingPoints": ["left_knee", "right_knee"], "duration": { "min": 0, "max": 5000 }, "transitions": [ { "toPhase": "up", "conditions": [ { "trackingPoint": "left_knee", "operator": "gt", "value": 160 } ] } ]}| Field | Type | Notes |
|---|---|---|
id | string | Phase identifier (unique within the movement). |
name | string | Display name. |
type | 'transition' | 'rep' | 'hold' | Drives some auto-cycle behaviour. |
trackingPoints | string[] | IDs of channels relevant to this phase (UI hint). |
duration | { min, max } (ms) | Hold-time bounds. Max enforces a force-transition when exceeded. |
transitions | array | Outgoing transitions. Each has a toPhase + condition list. |
PhaseCondition
{ "trackingPoint": "left_knee", "operator": "lt", "value": 120, "holdTime": 200}| Operator | Compares | value shape |
|---|---|---|
gt | tracking value > threshold | number |
lt | tracking value < threshold | number |
eq | tracking value ≈ threshold (±2°) | number |
between | tracking value ∈ [min, max] | [number, number] |
holdTime (optional, ms): the condition must remain satisfied for
at least this many ms before the transition fires. Used to prevent
flicker on noisy frames.
trackingPoints[i]
{ "id": "left_knee", "name": "Left knee angle", "type": "angle", "side": "left", "landmarks": ["left_hip", "left_knee", "left_ankle"], "config": { "targetValue": 90, "tolerance": 20 }, "relativeToBaseline": false}| Field | Type | Notes |
|---|---|---|
id | string | Stable identifier for cross-references from formRules + angleBands. |
type | TrackingPointType | One of angle, distance, proximity, velocity, stability, ratio, position. |
side | 'left' | 'right' | 'both' | 'any' | Anatomical side. both evaluates both sides and emits two values. |
landmarks | string[] | pose landmark names. Length depends on type (angle: 3, distance/proximity: 2, ratio: 4, position: 1). |
config | object | Type-specific config, see below. |
relativeToBaseline | boolean | When true, emit value - baseline_at_phase_entry instead of raw value. |
Per-type config shapes
angle (3 landmarks):
{ "targetValue": 90, "tolerance": 20 }distance (2 landmarks):
{ "minDistance": 0.0, "maxDistance": 1.0 }ratio (4 landmarks: numerator distance + denominator distance):
{} // ratio = dist(L0,L1) / dist(L2,L3)position (1 landmark):
{ "axis": "x" } // or "y"velocity (1 landmark, derived from a base tracking point):
{ "baseTrackingPointId": "left_wrist_y", "windowMs": 200 }stability (1 channel, rolling max-min):
{ "baseTrackingPointId": "left_knee", "windowMs": 1000 }formRules[i]
{ "id": "knee-collapse-down", "trackingPoint": "left_knee_valgus", "operator": "lt", "value": [15], "appliesToPhase": "down", "trigger": "onViolation", "severity": "warning", "feedback": { "text": "Push your knees out", "audioCue": "knee-collapse.mp3" }}| Field | Type | Notes |
|---|---|---|
id | string | Stable rule identifier. |
trackingPoint | string | Channel to evaluate. |
operator | ConditionOperator | gt, lt, eq, between. |
value | number[] | Single threshold or [min, max] for between. |
appliesToPhase | string? | Phase id. Null = applies always. |
trigger | 'onViolation' | 'onCompliance' | 'always' | When to fire feedback. Defaults to onViolation. |
severity | 'info' | 'warning' | 'error' | Drives the host UI’s colour + filter. |
feedback | { text, audioCue?, hapticPattern? } | What to surface. |
retrospective | object? | When set, the rule fires at phase end against an aggregate (avg/peak/min/max) instead of per frame. |
repDetection
{ "strategy": "sequence", "countOn": "sequenceComplete", "phases": ["up", "down"], "phaseAlternatives": [["up", "down", "up"]], "primaryPhase": "up"}| Field | Type | Notes |
|---|---|---|
strategy | 'cycle' | 'sequence' | 'hold' | 'compound' | Counting mode. |
countOn | 'phaseEnter' | 'phaseExit' | 'sequenceComplete' | 'duration' | When in the lifecycle. |
phases | string[] | The cycle order (also used as the fallback sequence). |
phaseAlternatives | string[][] | Alternative sequences, a rep counts on any one match. |
primaryPhase | string | For cycle strategy: the phase entry that ticks. |
sequences | PhaseSequenceConfig[] | Richer config with per-sequence metadata (mask landmarks, hold durations). |
angleBands
{ "front_hip": [ { "phaseId": "down", "channelId": "spine_lean", "min": -8, "max": 8, "cameraBucket": "front_hip" } ], "90left_hip": [ { "phaseId": "down", "channelId": "spine_lean", "min": -5, "max": 15, "cameraBucket": "90left_hip" } ]}One entry per (channel, phase, bucket) triple. The runtime gates
view-dependent measurements on the live-detected bucket. See
camera buckets.
repScoring
{ "enabled": true, "minQualityScore": 60, "weights": { "formScore": 0.5, "rangeOfMotion": 0.3, "tempo": 0.2, "stability": 0.0 }, "dimensions": { "formScore": true, "rangeOfMotion": true, "tempo": true, "stability": false }}Optional. When enabled: false, every completed rep counts. When
true, a rep only counts if its weighted quality ≥ minQualityScore.
Disabling a dimension zero-weights it and renormalises the others.
metadata
Author-defined Map<String, dynamic>. Hosts that need to attach
managed fields alongside the tracking definition typically nest them
under a host-namespaced key:
"metadata": { "host": { "image_url": "...", "video_url": "...", "body_part": ["LOWER_BODY"], "muscle_groups": ["QUADS"], "modality": ["STRENGTH"], "difficulty": "BEGINNER", "category_ids": [...], "supported_buckets": ["front_hip", "90left_hip"] }, "authoredBy": "poseflow_studio"}Consumers that don’t care about host-specific fields can ignore the
whole metadata object, the runtime contract is fully defined by
the structural fields above.
Round-trip guarantee
Movement.fromJson(jsonDecode(file)).toJson() is canonical
JSON-stable (keys in deterministic order, no null fields written).
You can diff two .pose files byte-for-byte to detect authoring
drift.