Skip to content

Core Concepts

openweight models strength training using a hierarchy of concepts that map to how athletes think about their training.

Data Hierarchy

Program
└── ProgramWeek[]
    └── WorkoutTemplate[]
        └── ExerciseTemplate[]
            └── SetTemplate[]

WorkoutLog (completed session)
└── ExerciseLog[]
    └── SetLog[]

Completed vs. Planned

openweight distinguishes between what you plan to do and what you actually did:

Planned (Templates)Completed (Logs)
WorkoutTemplateWorkoutLog
ExerciseTemplateExerciseLog
SetTemplateSetLog
targetReps, targetWeightreps, weight

A WorkoutLog can reference the template it was created from via templateId.

Exercise

An Exercise describes which movement was performed:

FieldTypeDescription
namestringHuman-readable name (required)
equipmentstringEquipment used: barbell, dumbbell, bodyweight, etc.
categorystringBody part: chest, back, legs, etc.
musclesWorkedstring[]Specific muscles targeted

The same Exercise definition is shared between logs and templates.

Sets

SetLog (Completed)

Records what actually happened in a set:

FieldTypeDescription
repsintegerRepetitions completed
weightnumberWeight used
unit"kg" | "lb"Weight unit (required if weight is present)
durationSecondsintegerTime for timed exercises
rpenumber (0-10)Rate of Perceived Exertion
ririntegerReps In Reserve
toFailurebooleanWhether the set was taken to failure
typestringSet type: working, warmup, dropset, etc.
tempostringTempo notation (e.g., "3-1-2-0")

SetTemplate (Planned)

Prescribes what to do in a set:

FieldTypeDescription
targetRepsintegerTarget rep count
targetRepsMin / targetRepsMaxintegerRep range (e.g., 8-12)
targetWeightnumberAbsolute weight target
percentage / percentageOfnumber/stringPercentage-based (e.g., 80% of 1RM)
targetRPEnumberTarget RPE
targetRIRintegerTarget RIR

Units

Weight Units

ValueDescription
kgKilograms
lbPounds

Rule: If weight is present, unit is required.

Distance Units

ValueDescription
mMeters
kmKilometers
ftFeet
miMiles
ydYards

Rule: If distance is present, distanceUnit is required.

Intensity Metrics

RPE (Rate of Perceived Exertion)

A 0-10 scale measuring how hard a set felt:

RPEDescription
10Maximum effort, no reps left
9Could do 1 more rep
8Could do 2 more reps
7Could do 3 more reps

Decimals are allowed (e.g., 8.5).

RIR (Reps In Reserve)

An alternative to RPE, counting reps left in the tank:

RIRDescription
0No reps left (failure)
11 rep left
22 reps left

Tempo Notation

Tempo is expressed as four numbers: eccentric-pause-concentric-pause

NotationDescription
3-1-2-03s down, 1s pause, 2s up, no pause at top
4-0-1-04s down, no pause, 1s up, no pause
2-0-X-02s down, no pause, explosive up, no pause

Use X for explosive (as fast as possible).

Supersets

Exercises can be grouped into supersets using supersetId:

json
{
  "exercises": [
    {
      "exercise": {
        "name": "Bench Press"
      },
      "supersetId": 1,
      "sets": [
        ...
      ]
    },
    {
      "exercise": {
        "name": "Bent Over Row"
      },
      "supersetId": 1,
      "sets": [
        ...
      ]
    }
  ]
}

Exercises with the same supersetId are performed together.

Extensibility

All openweight objects allow additional properties through index signatures ([key: string]: unknown). This enables apps to store proprietary metadata alongside standard fields without breaking compatibility with other openweight-compliant applications.

Namespaced Keys

Use a prefix like myapp: to avoid conflicts with future schema fields and other apps:

json
{
  "date": "2024-01-15T09:30:00Z",
  "exercises": [
    ...
  ],
  "myapp:sessionId": "abc123",
  "myapp:gymLocation": "Downtown",
  "myapp:heartRateAvg": 142,
  "myapp:caloriesBurned": 450
}

Best Practices for Custom Fields

  1. Always use a namespace prefix — Choose a unique prefix for your app (e.g., featherweight:, strongapp:, myapp:)
  2. Use consistent naming — Stick to camelCase after the prefix: myapp:customField
  3. Document your extensions — If others might import your data, document what your custom fields mean
  4. Keep it minimal — Only add custom fields when standard fields don't suffice

TypeScript Example

typescript
import { serializeWorkoutLogPretty, type WorkoutLog } from '@openweight/sdk'

// Create a workout with custom app-specific metadata
const workout: WorkoutLog = {
  date: new Date().toISOString(),
  name: 'Morning Workout',
  exercises: [
    {
      exercise: { name: 'Squat' },
      sets: [{ reps: 5, weight: 100, unit: 'kg' }],
      // Custom field on exercise
      'myapp:restTimerUsed': true,
    },
  ],
  // Custom fields on workout
  'myapp:sessionId': 'abc123',
  'myapp:gymLocation': 'Downtown Gym',
  'myapp:mood': 'energetic',
  'myapp:heartRateData': {
    avg: 142,
    max: 165,
    zones: [10, 25, 40, 20, 5],
  },
}

const json = serializeWorkoutLogPretty(workout)
// Custom fields are preserved in the JSON output

Compatibility Guarantees

  • Other apps will ignore your custom fields — Apps that don't recognize myapp:* fields will simply pass them through unchanged
  • Round-trip safe — Parsing and re-serializing preserves all custom fields
  • Schema validation passes — Custom fields don't cause validation errors because additionalProperties is allowed

When to Use Custom Fields vs. Standard Fields

Use CaseRecommendation
Heart rate, caloriesCustom field: myapp:heartRate
Session notesStandard field: notes
App-specific IDsCustom field: myapp:sessionId
RPE/RIRStandard field: rpe, rir
Gym locationCustom field: myapp:gymLocation
Exercise variationsStandard field: exercise.name with descriptive name

Handling Custom Fields When Importing

When importing data from another app, you can access or ignore custom fields:

typescript
import { parseWorkoutLog } from '@openweight/sdk'

const workout = parseWorkoutLog(jsonFromAnotherApp)

// Access standard fields (type-safe)
console.log(workout.date)
console.log(workout.exercises[0].exercise.name)

// Access custom fields (requires type assertion or checking)
const theirSessionId = workout['otherapp:sessionId'] as string | undefined
if (theirSessionId) {
  console.log('Imported session:', theirSessionId)
}

// Or simply ignore them - they won't affect your app's logic

Released under the Apache 2.0 License.