Specification
Open Challenge Protocol
A declarative JSON format for expressing fitness challenges across platforms.
Overview
The Open Challenge Protocol (OCP) is a declarative JSON format for expressing fitness challenges. It defines what constitutes success, not how a host application should implement the experience.
A challenge is a structured goal that evaluates rider performance against defined criteria, with optional lifecycle phases, runtime directives, and iteration/loop logic.
Challenges vs Workouts
OCP is specifically designed for Challenges, not Workouts. Understanding this distinction is critical.
Challenges
- Dynamic and goal-oriented: Success is determined by meeting specific criteria
- Logic-based: Built around conditions, thresholds, and exit criteria
- Often indeterminate duration: Many end when you fail to maintain a condition
- Pass/fail or record-breaking: Either you achieve the goal or compete against a personal best
Examples
- Hold 5.0 w/kg for 10 seconds
- Sprint 200m as fast as possible
- Ride as far as possible while keeping HR below zone 4
- Accelerate from standstill to 40 km/h
Workouts
- Structured and predetermined: Follow a fixed sequence of intervals
- Time/duration-based: Intervals have specific durations
- Completion = success: If you finish all intervals, you "completed" the workout
- Training-focused: Designed for physiological adaptation
Not Suitable for OCP
- 5x (30 seconds on / 30 seconds off)
- Tabata: 8x (20 seconds / 10 seconds)
- 1-2-3-2-1 minute ladder intervals
- 2x20 minutes at FTP
When in Doubt: Ask "Does success depend on how the rider performs, or just that they complete the structure?" If success depends on maintaining conditions → Challenge. If success is just completing intervals → Workout.
Challenge Structure
A challenge is a JSON object with the following top-level fields:
{
"id": "string",
"version": "0.1.0",
"name": "string",
"description": "string (optional)",
"sport": "cycling | running | rowing | any (optional)",
"mode": "record | pass-fail",
"config": { },
"phases": [ ],
"criteria": { },
"loops": [ ],
"directives": [ ],
"record": { },
"completion": { },
"metadata": { }
}Required Fields
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier for this challenge |
version | string | Protocol version (semver) |
name | string | Human-readable challenge name |
phases | array | Ordered list of challenge phases (OR criteria for simple challenges) |
Optional Fields
| Field | Type | Description |
|---|---|---|
description | string | Longer description of the challenge |
sport | string | Sport type (cycling, running, rowing, any) |
mode | string | record (best effort) or pass-fail (binary success). Default: pass-fail |
config | object | Challenge-wide configuration and computed values |
criteria | object | Success (and optional failure) criteria for simple challenges |
loops | array | Repeating evaluation blocks |
directives | array | Runtime behaviors (notifications, suggestions) |
record | object | What metrics to capture as the challenge result |
completion | object | How the challenge ends and what to display |
metadata | object | Display hints, difficulty, tags, etc. |
Challenge Modes
pass-fail (default): Traditional challenge with success/failure criteria. Binary outcome.
record: Best-effort challenge where the goal is to achieve the best possible result. No failure state: the challenge records your performance.
Metrics
Metrics are the canonical data points that challenges evaluate. Host applications map their raw data to these paths.
Metric Path Format
Metrics use dot-notation: category.metric
power.instant → Current power in watts power.wkg → Current power in watts per kilogram lap.index → Current lap number (0-indexed or 1-indexed per host) position.current → Race position (1 = first place)
Core Metrics
Time
| Path | Type | Description |
|---|---|---|
time.elapsed | number | Seconds since challenge started |
time.session | number | Seconds since ride session started |
Power
| Path | Type | Description |
|---|---|---|
power.instant | number | Current power (watts) |
power.avg | number | Average power for session |
power.normalized | number | Normalized power |
power.wkg | number | Current watts per kilogram |
power.zone | number | Current power zone (1-7) |
power.percentFtp | number | Current power as % of FTP |
Speed, Cadence, Heart Rate
| Path | Type | Description |
|---|---|---|
speed.instant | number | Current speed (m/s or km/h per host) |
cadence.instant | number | Current cadence (RPM) |
heartRate.instant | number | Current heart rate (BPM) |
heartRate.zone | number | Current HR zone (1-5) |
Distance, Lap, Track
| Path | Type | Description |
|---|---|---|
distance.total | number | Total distance (meters) |
lap.index | number | Current lap number |
lap.count | number | Total laps completed |
track.length | number | Track length (meters) |
Race/Multiplayer Metrics
These are only available during race contexts:
| Path | Type | Description |
|---|---|---|
position.current | number | Current race position (1 = first) |
gap.ahead | number | Gap to rider ahead |
gap.behind | number | Gap to rider behind |
overtakes.count | number | Number of overtakes made |
Phases
Phases define the structure of a challenge as an ordered sequence of segments. Each phase has its own duration, targets, exit conditions, and directives.
Phase Structure
{
"id": "effort",
"name": "Max Effort",
"duration": 30,
"description": "Give everything for 30 seconds",
"targets": { },
"exitConditions": { },
"directives": [ ]
}| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique identifier for this phase |
name | string | Yes | Display name |
duration | number/string | No | Phase duration in seconds, or "unlimited" |
distance | number | No | Phase distance in meters (alternative to duration) |
targets | object | No | Target metrics for this phase |
exitConditions | object | No | Conditions that end this phase early |
Targets
Targets define the expected or required metrics during a phase:
{
"targets": {
"power": { "min": 80, "max": 150 },
"cadence": { "min": 90, "max": 100 }
}
}Ramping Targets
{
"power": {
"mode": "ramp",
"start": 100,
"increment": 1,
"per": "second"
}
}Conditions
A condition is a single testable expression that evaluates to true or false.
Condition Structure
{
"metric": "power.wkg",
"operator": ">=",
"value": 5.0,
"duration": 10,
"window": "sustained"
}| Field | Type | Required | Description |
|---|---|---|---|
metric | string | Yes | Metric path to evaluate |
operator | string | Yes | Comparison operator |
value | number/array | Yes | Target value(s) |
duration | number | No | Time constraint in seconds |
window | string | No | How duration is evaluated |
Operators
Comparison
==Equal to!=Not equal to>Greater than<Less than>=Greater than or equal<=Less than or equal
Range
betweenInclusive rangeoutsideOutside range
Change
increasedByDelta increasedecreasedByDelta decrease
Duration Windows
| Window | Description |
|---|---|
sustained | Condition must be true continuously for the duration (default) |
cumulative | Condition must be true for total duration (can be non-continuous) |
rolling | Evaluated over a rolling window of the specified duration |
any | Condition must be true at any point for the duration |
Logical Gates
Conditions can be combined using logical gates.
Gate Types
| Gate | Description |
|---|---|
all | All conditions must be true (AND) |
any | At least one condition must be true (OR) |
none | No conditions can be true (NOT ANY) |
sequence | Conditions must become true in order |
Example: Nested Gates
{
"all": [
{ "metric": "time.elapsed", "operator": ">=", "value": 60 },
{
"any": [
{ "metric": "power.wkg", "operator": ">=", "value": 5.0 },
{ "metric": "position.current", "operator": "==", "value": 1 }
]
}
]
}"After 60 seconds, either hit 5.0 w/kg OR be in first place"
Lifecycle
The lifecycle defines when challenge evaluation begins and ends.
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ PENDING │ ──▶ │ ARMING │ ──▶ │ ACTIVE │ ──▶ │ COMPLETE │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│ │
│ ▼
│ ┌─────────────┐
└────────────▶ │ FAILED │
└─────────────┘| Phase | Description |
|---|---|
pending | Challenge loaded but not started |
arming | Waiting for start conditions |
active | Evaluating success/failure criteria |
complete | Success criteria met |
failed | Failure criteria met or abandoned |
Lifecycle Structure
{
"lifecycle": {
"arming": {
"all": [
{ "metric": "time.elapsed", "operator": ">=", "value": 120 }
]
},
"timeout": 600,
"maxAttempts": 3
}
}Success and Failure Criteria
Criteria Structure
{
"criteria": {
"success": {
"all": [
{ "metric": "power.wkg", "operator": ">=", "value": 5.0, "duration": 10 }
]
},
"failure": {
"any": [
{ "metric": "power.instant", "operator": "==", "value": 0, "duration": 5 }
]
}
}
}Success-Only Challenges
Most challenges only define success criteria. The challenge continues until success or user abandonment.
Challenges with Failure Conditions
{
"criteria": {
"success": {
"all": [
{ "metric": "lap.count", "operator": ">=", "value": 5 }
]
},
"failure": {
"any": [
{ "metric": "position.current", "operator": ">", "value": 3 }
]
}
}
}"Complete 5 laps while staying in top 3. Drop to 4th or worse = fail."
Loops
Loops enable repeating evaluation blocks with iteration-aware logic. This is essential for challenges that involve multiple intervals, progressive difficulty, or repeated actions.
Loop Structure
{
"loops": [
{
"id": "sprint_intervals",
"count": 5,
"trigger": {
"metric": "lap.index",
"operator": "increasedBy",
"value": 1
},
"onIteration": {
"directives": [ ],
"criteria": { }
},
"onComplete": {
"directives": [ ]
}
}
]
}Iteration Context Variables
| Variable | Description |
|---|---|
{{loopIndex}} | Current iteration (0-indexed) |
{{loopCount}} | Total iterations configured |
{{loopRemaining}} | Iterations remaining |
Progressive Difficulty Example
{
"loops": [
{
"id": "progressive_power",
"count": 5,
"trigger": {
"metric": "time.elapsed",
"operator": ">=",
"value": "{{loopIndex * 60}}"
},
"onIteration": {
"directives": [
{
"type": "notify",
"message": "Interval {{loopIndex + 1}}/{{loopCount}}: Hold {{3.0 + loopIndex * 0.5}} w/kg"
}
]
}
}
]
}"5 intervals with increasing power: 3.0, 3.5, 4.0, 4.5, 5.0 w/kg"
Directives
Directives are optional runtime behaviors that the host application may execute. They are advisory: the host decides whether to honor them.
Directive Structure
{
"directives": [
{
"id": "gap_warning",
"type": "notify",
"when": {
"metric": "gap.ahead",
"operator": ">",
"value": 3.0
},
"params": {
"message": "Gap widening! Push harder.",
"priority": "high"
},
"cooldown": 10
}
]
}Directive Types
notify
Display a message to the rider
{
"type": "notify",
"params": {
"message": "Push harder!",
"priority": "normal | high | critical"
}
}sound
Play an audio cue
{
"type": "sound",
"params": {
"sound": "bell | chime | warning | success"
}
}suggest
Suggest a target to the rider (advisory)
{
"type": "suggest",
"params": {
"metric": "power.instant",
"target": 250
}
}highlight
Highlight a metric in the UI
{
"type": "highlight",
"params": {
"metric": "power.wkg",
"style": "pulse | glow | flash"
}
}Record (Best Effort Challenges)
For mode: "record" challenges, the record object defines what metrics to capture as the challenge result.
Record Structure
{
"record": {
"primary": {
"metric": "power.max",
"window": 5,
"label": "5-Second Max Power",
"unit": "watts",
"phase": "effort"
},
"secondary": [
{ "metric": "power.peak", "label": "Peak Power", "unit": "watts" },
{ "metric": "cadence.max", "label": "Max Cadence", "unit": "rpm" }
]
}
}Common Primary Metrics
| Challenge Type | Primary Metric | Example |
|---|---|---|
| Max effort | power.max with window | 5-second max power |
| Time trial | duration for phase | 200m sprint time |
| Endurance hold | duration for phase | Time held at target |
| Distance | distance.total | Meters covered |
Completion
The completion object defines how a challenge ends and what to display in the summary.
Completion Structure
{
"completion": {
"condition": "phases-complete | exit-or-phases-complete | criteria-met",
"summary": {
"headline": "{{record.primary.value}}W",
"subheadline": "5-Second Max Power"
}
}
}Completion Conditions
| Condition | Description |
|---|---|
phases-complete | Challenge ends when all phases complete |
exit-or-phases-complete | Challenge ends on phase exit condition OR all phases complete |
criteria-met | Challenge ends when success criteria are met (pass-fail mode) |
Metadata
Metadata provides display hints and categorization for host applications.
{
"metadata": {
"difficulty": 4,
"estimatedDuration": 120,
"tags": ["sprint", "power", "short"],
"author": "Ride Cave",
"icon": "bolt",
"color": "#FF6B00",
"shortDescription": "Hold 5.0 w/kg for 10 seconds",
"requirements": {
"powerMeter": true,
"heartRateMonitor": false
}
}
}| Field | Type | Description |
|---|---|---|
difficulty | number | 1-5 difficulty rating |
estimatedDuration | number | Expected duration in seconds |
tags | array | Categorization tags |
author | string | Challenge creator |
icon | string | Icon identifier |
color | string | Accent color (hex) |
requirements | object | Required sensors/equipment |
Complete Example
{
"id": "power-surge",
"version": "0.1.0",
"name": "Power Surge",
"description": "After a 2-minute warmup, hold 5.0 w/kg for 10 continuous seconds.",
"sport": "cycling",
"lifecycle": {
"arming": {
"all": [
{ "metric": "time.elapsed", "operator": ">=", "value": 120 }
]
},
"timeout": 300
},
"criteria": {
"success": {
"all": [
{
"metric": "power.wkg",
"operator": ">=",
"value": 5.0,
"duration": 10,
"window": "sustained"
}
]
}
},
"directives": [
{
"type": "notify",
"params": {
"message": "Warmup complete. Go for 5.0 w/kg!"
},
"once": true
},
{
"type": "notify",
"when": {
"metric": "power.wkg",
"operator": ">=",
"value": 4.5
},
"params": {
"message": "Almost there! Push to 5.0!",
"priority": "high"
},
"cooldown": 5
}
],
"metadata": {
"difficulty": 4,
"estimatedDuration": 180,
"tags": ["sprint", "power", "short"],
"author": "Ride Cave",
"shortDescription": "Hold 5.0 w/kg for 10 seconds"
}
}Evaluation Model
Evaluation Loop
The challenge evaluator runs at a fixed frequency (recommended: 10Hz).
┌─────────────────────────────────────────────────────────────┐ │ EVALUATION TICK │ ├─────────────────────────────────────────────────────────────┤ │ 1. Collect current metrics from host │ │ 2. Check lifecycle phase │ │ - If PENDING: check arming conditions │ │ - If ARMING: check arming conditions → transition │ │ - If ACTIVE: evaluate criteria + loops + directives │ │ 3. Update loop states │ │ 4. Check success criteria │ │ 5. Check failure criteria (if defined) │ │ 6. Fire applicable directives │ │ 7. Return evaluation result │ └─────────────────────────────────────────────────────────────┘
Evaluation Result
{
"phase": "active",
"success": false,
"failed": false,
"progress": {
"percent": 75,
"conditions": [
{
"metric": "power.wkg",
"target": 5.0,
"current": 5.2,
"durationElapsed": 7.5,
"durationRequired": 10,
"met": false
}
]
},
"loops": [
{
"id": "sprint_intervals",
"iteration": 2,
"count": 5,
"status": "active"
}
],
"firedDirectives": ["gap_warning"],
"elapsed": 145.3
}Appendices
A. Environment & Portability
Challenges may reference environment metrics, values provided by the host application that describe the context in which the challenge runs.
Portability Spectrum
- Fully Portable: Challenges that only reference core metrics (power, cadence, heart rate, time, distance) work on any compliant host
- Environment-Dependent: Challenges that reference environment metrics require the host to provide those values
- Platform-Specific: Some challenges may reference features unique to a specific platform
Fallback Values
{
"config": {
"approachDistance": {
"value": "{{track.length}}",
"fallback": 250
}
}
}B. Reserved Metric Paths
The following paths are reserved for future use:
rowing.*- Rowing-specific metricsrunning.*- Running-specific metricsstrength.*- Strength training metricscustom.*- Host-defined custom metrics
C. Host Implementation Notes
Hosts must implement a metric adapter that maps their internal data to OCP metric paths:
interface MetricAdapter {
resolve(path: string): number | undefined;
getProfile(): { ftp?: number; weight?: number; maxHr?: number };
}D. Version History
| Version | Date | Changes |
|---|---|---|
| 0.1.0-draft | 2025-01 | Initial draft specification |
