Open Challenge Protocol (OCP) | Ride Cave
Specification

Open Challenge Protocol

A declarative JSON format for expressing fitness challenges across platforms.

DraftVersion 0.1.0

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

FieldTypeDescription
idstringUnique identifier for this challenge
versionstringProtocol version (semver)
namestringHuman-readable challenge name
phasesarrayOrdered list of challenge phases (OR criteria for simple challenges)

Optional Fields

FieldTypeDescription
descriptionstringLonger description of the challenge
sportstringSport type (cycling, running, rowing, any)
modestringrecord (best effort) or pass-fail (binary success). Default: pass-fail
configobjectChallenge-wide configuration and computed values
criteriaobjectSuccess (and optional failure) criteria for simple challenges
loopsarrayRepeating evaluation blocks
directivesarrayRuntime behaviors (notifications, suggestions)
recordobjectWhat metrics to capture as the challenge result
completionobjectHow the challenge ends and what to display
metadataobjectDisplay 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

PathTypeDescription
time.elapsednumberSeconds since challenge started
time.sessionnumberSeconds since ride session started

Power

PathTypeDescription
power.instantnumberCurrent power (watts)
power.avgnumberAverage power for session
power.normalizednumberNormalized power
power.wkgnumberCurrent watts per kilogram
power.zonenumberCurrent power zone (1-7)
power.percentFtpnumberCurrent power as % of FTP

Speed, Cadence, Heart Rate

PathTypeDescription
speed.instantnumberCurrent speed (m/s or km/h per host)
cadence.instantnumberCurrent cadence (RPM)
heartRate.instantnumberCurrent heart rate (BPM)
heartRate.zonenumberCurrent HR zone (1-5)

Distance, Lap, Track

PathTypeDescription
distance.totalnumberTotal distance (meters)
lap.indexnumberCurrent lap number
lap.countnumberTotal laps completed
track.lengthnumberTrack length (meters)

Race/Multiplayer Metrics

These are only available during race contexts:

PathTypeDescription
position.currentnumberCurrent race position (1 = first)
gap.aheadnumberGap to rider ahead
gap.behindnumberGap to rider behind
overtakes.countnumberNumber 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": [ ]
}
FieldTypeRequiredDescription
idstringYesUnique identifier for this phase
namestringYesDisplay name
durationnumber/stringNoPhase duration in seconds, or "unlimited"
distancenumberNoPhase distance in meters (alternative to duration)
targetsobjectNoTarget metrics for this phase
exitConditionsobjectNoConditions 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"
}
FieldTypeRequiredDescription
metricstringYesMetric path to evaluate
operatorstringYesComparison operator
valuenumber/arrayYesTarget value(s)
durationnumberNoTime constraint in seconds
windowstringNoHow duration is evaluated

Operators

Comparison

  • == Equal to
  • != Not equal to
  • > Greater than
  • < Less than
  • >= Greater than or equal
  • <= Less than or equal

Range

  • between Inclusive range
  • outside Outside range

Change

  • increasedBy Delta increase
  • decreasedBy Delta decrease

Duration Windows

WindowDescription
sustainedCondition must be true continuously for the duration (default)
cumulativeCondition must be true for total duration (can be non-continuous)
rollingEvaluated over a rolling window of the specified duration
anyCondition must be true at any point for the duration

Logical Gates

Conditions can be combined using logical gates.

Gate Types

GateDescription
allAll conditions must be true (AND)
anyAt least one condition must be true (OR)
noneNo conditions can be true (NOT ANY)
sequenceConditions 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    │
                                         └─────────────┘
PhaseDescription
pendingChallenge loaded but not started
armingWaiting for start conditions
activeEvaluating success/failure criteria
completeSuccess criteria met
failedFailure 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

VariableDescription
{{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 TypePrimary MetricExample
Max effortpower.max with window5-second max power
Time trialduration for phase200m sprint time
Endurance holdduration for phaseTime held at target
Distancedistance.totalMeters 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

ConditionDescription
phases-completeChallenge ends when all phases complete
exit-or-phases-completeChallenge ends on phase exit condition OR all phases complete
criteria-metChallenge 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
    }
  }
}
FieldTypeDescription
difficultynumber1-5 difficulty rating
estimatedDurationnumberExpected duration in seconds
tagsarrayCategorization tags
authorstringChallenge creator
iconstringIcon identifier
colorstringAccent color (hex)
requirementsobjectRequired 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 metrics
  • running.* - Running-specific metrics
  • strength.* - Strength training metrics
  • custom.* - 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

VersionDateChanges
0.1.0-draft2025-01Initial draft specification