Build Your Own Program

CrispyCore uses an open JSON format for workout programs and routines. Write your own programs (strength, mobility, boxing, cardio, yoga, anything), host them on GitHub, and import them straight into the app.

Overview

A program in CrispyCore is a folder of JSON files with a specific structure. The app reads these files to build a guided, scheduled workout experience: timers, rep targets, rest periods, tempo cues, and progression tracking.

You don't need to touch any code. Everything is defined in JSON.

Any sport, any format. CrispyCore is a player and timer. Your program defines the exercises and structure; the app handles the rest. Works just as well for a boxing conditioning block, a 12-week powerlifting program, a daily mobility routine, or anything else.

Programs vs Routines

There are two kinds of content in CrispyCore, set by the kind field in program.json:

📅 Program kind: "program"

Has phases and a weekly schedule. The app tracks your progress day by day and always shows you the next workout. Great for multi-week training blocks.

⚡ Routine kind: "routine"

A standalone workout or set of workouts with no fixed schedule. Always visible in the Today tab. Great for warm-ups, mobility sessions, or standalone conditioning workouts you do on demand.

If kind is omitted, it defaults to "program".

Folder Structure

Every program lives in its own folder. The folder name is the slug, a URL-safe identifier used throughout the app.

my-program/ ├── program.json # Program definition (required) ├── workouts/ │ ├── 1A.json # Workout template for day 1A │ ├── 1B.json # Workout template for day 1B │ └── 1C.json # etc. └── exercises.json # Optional: exercise info (descriptions, tips, etc.)

Routine structure is simpler. For a kind: "routine", the app loads all JSON files in workouts/ alphabetically. No phases or week patterns needed, just the workout files.

GitHub Import

Programs are imported into CrispyCore directly from a public GitHub repository. The repo just needs to follow the folder structure above at its root (or inside a subfolder).

In the app, tap Import and paste the GitHub URL — either the repo URL or a direct link to a folder inside the repo. The app fetches all the JSON files, validates them, and installs the program. If you push updates to the repo, the app can detect and pull the changes automatically.

The repo must be public. CrispyCore uses the GitHub raw content API. Private repos are not currently supported.

program.json

The main definition file for your program. This is what the app reads to understand the structure, schedule, and metadata.

{
  "programId": "my-strength-program",
  "programTitle": "12-Week Strength Block",
  "shortDescription": "A progressive strength program focused on the big 3.",
  "contentVersion": "1.0.0",
  "kind": "program",
  "iconName": "dumbbell.fill",
  "categories": ["strength", "barbell"],
  "phases": [
    {
      "id": "P1",
      "title": "Phase 1: Foundation",
      "weeks": [
        {
          "weekNumber": 1,
          "pattern": ["1A", "REST", "1B", "REST", "1C", "REST", "REST"]
        }
      ]
    }
  ]
}
Field Type Required Description
programIdstringrequiredUnique identifier for the program. Used internally to track progress.
programTitlestringrequiredDisplay name shown in the app.
contentVersionstringrequiredSemver string (e.g. "1.0.0"). Used to detect updates when the program is hosted on GitHub.
kind"program" | "routine"optionalDefaults to "program". Use "routine" for standalone, always-available workouts with no fixed schedule.
shortDescriptionstringoptionalOne-line description shown in program listings.
iconNamestringoptionalSF Symbol name for the program icon (e.g. "figure.run", "dumbbell.fill").
categoriesstring[]optionalCategory tags for grouping (e.g. ["strength", "barbell"]).
isMinorUpdatebooleanoptionalSet to true when updating content without breaking progress (e.g. fixing typos, adding notes). If false or absent, updating resets the user's progress cursor.
updateNotesstringoptionalHuman-readable changelog shown when an update is available.
phasesarrayrequired for programsArray of phases (see below). Not needed for kind: "routine".

Phase

FieldTypeDescription
idstringPhase identifier (e.g. "P1", "P2"). Displayed to the user.
titlestringPhase display name (e.g. "Phase 1: Foundation").
weeksarrayArray of week definitions.

Week

FieldTypeDescription
weekNumbernumberWeek number within the phase (1, 2, 3…). Used for display only.
patternstring[7]A 7-element array representing each day of the week. Each entry is either a workout ID (matching a filename in workouts/, without the .json extension) or "REST".

Workout Templates

Each file in the workouts/ folder defines a single workout session. The filename (without .json) must match the IDs used in the weekly pattern array in your program.json.

{
  "id": "1A",
  "title": "Upper Body — Push",
  "blocks": [
    {
      "type": "straight",
      "rounds": 4,
      "restBetweenRoundsSec": 180,
      "postBlockRestSec": 120,
      "items": [
        {
          "id": "item-1",
          "exerciseId": "bench-press",
          "prescription": {
            "mode": "reps",
            "target": { "reps": { "min": 5, "max": 5 } },
            "rpe": 8,
            "tempo": { "down": 3, "pause": 1, "up": "X" },
            "toFailure": "none"
          }
        }
      ]
    }
  ]
}
FieldTypeRequiredDescription
idstringrequiredMust match the filename and the pattern reference (e.g. "1A"workouts/1A.json).
titlestringrequiredDisplay name shown in the workout player header.
blocksarrayrequiredArray of workout blocks. See Block Types below.

Block Types

Each block has a type field that controls how the player sequences exercises. A workout can have multiple blocks of different types.

straight Straight Sets

Complete all rounds of the first exercise, then move to the second, and so on. Classic set-by-set format. Use for strength work where you need full rest between sets of the same movement.

circuit Circuit

Cycle through all exercises in order, then repeat for the specified number of rounds. Use restAfterSec on items to add rest between exercises within a round.

emom EMOM

Every Minute on the Minute. All exercises in the block are performed within each minute interval. Use emomIntervalMin for multi-minute intervals (e.g. every 2 minutes). Defaults to 1 minute.

accumulation Accumulation

A volume-focused format where you accumulate a total number of reps across as many sets as needed. Use the totalReps prescription mode with this block type.

FieldTypeRequiredDescription
type"straight" | "circuit" | "emom" | "accumulation"requiredBlock execution format.
roundsnumberrequiredNumber of rounds (or minutes for EMOM).
itemsarrayrequiredArray of exercises in this block.
restBetweenRoundsSecnumberoptionalRest in seconds between rounds of this block.
postBlockRestSecnumberoptionalRest in seconds after the entire block, before the next block starts.
emomIntervalMinnumberoptionalEMOM only. Interval length in minutes. Defaults to 1.

Prescriptions

Each exercise item has a prescription object that tells the player how to guide the user through the exercise.

Modes

ModeDescriptionTarget fields needed
"reps"Rep-based. If no target.reps range is given, the player shows MAX REPS.target.reps (or omit for max)
"time"Timed exercise — holds, isometrics, interval work. The player counts down the timer automatically.target.timeSec
"totalReps"Accumulate a target number of reps total across however many sets it takes. Displays as e.g. "50 reps, 8–12 per set".target.totalReps + target.reps

Prescription fields

// Reps — with range
"prescription": {
  "mode": "reps",
  "target": { "reps": { "min": 8, "max": 12 } },
  "rpe": 7,
  "toFailure": "last",
  "tempo": { "down": 3, "pause": 1, "up": "X" }
}

// Reps — max reps (no range)
"prescription": {
  "mode": "reps",
  "target": {},
  "toFailure": "each"
}

// Time — 30–45 second hold
"prescription": {
  "mode": "time",
  "target": { "timeSec": { "min": 30, "max": 45 } }
}

// Total reps — accumulate 50 reps in sets of 5-10
"prescription": {
  "mode": "totalReps",
  "target": {
    "totalReps": 50,
    "reps": { "min": 5, "max": 10 }
  }
}
FieldTypeRequiredDescription
mode"reps" | "time" | "totalReps"requiredPrescription type.
target.reps{"min": n, "max": n}optionalRep range. Omit entirely for max reps.
target.timeSec{"min": n, "max": n}optionalTime range in seconds. Use equal min/max for a fixed duration.
target.totalRepsnumberoptionalTotal rep target for totalReps mode.
rpe1–10optionalRate of Perceived Exertion target. Displayed as a cue in the player.
toFailure"none" | "last" | "each"optional"last" = push the final set to failure. "each" = every set. Defaults to "none".
tempoobjectoptionalMovement tempo. See Tempo section below.

Tempo

Tempo specifies the speed of each phase of a movement. The player displays it as a compact string like 3-1-X.

FieldDescription
downEccentric / lowering phase. Number of seconds, or "X" for explosive.
pausePause at the bottom. Number of seconds, or "X".
upConcentric / lifting phase. Number of seconds, or "X" for explosive.
// 3-second lower, 1-second pause, explosive press
"tempo": { "down": 3, "pause": 1, "up": "X" }

// Fully controlled — 2-2-2
"tempo": { "down": 2, "pause": 2, "up": 2 }

// Omit tempo entirely for uncontrolled / as-fast-as-possible

Rest Periods

There are three levels of rest, applied at different points in the workout. All values are in seconds.

FieldWhere it livesWhen it fires
restAfterSecOn each itemAfter an exercise, before the next one in the same round. Mainly useful in circuits to create unequal rest between exercises.
restBetweenRoundsSecOn the blockAfter completing one full round of the block, before starting the next round.
postBlockRestSecOn the blockAfter all rounds of the block are done, before the next block starts.

Any of these can be 0 or omitted to skip the rest step entirely. The player only shows a rest screen when the value is greater than zero.

exercises.json

An optional single JSON file at the root of your program folder. It's a dictionary keyed by exerciseId (the same IDs you use in your workout items). When present, the app shows an info button next to each exercise in the player so the user can view descriptions, tips, muscles, and media.

{
  "bench-press": {
    "description": "A horizontal pushing movement targeting the chest, front delts, and triceps.",
    "tips": [
      "Keep shoulder blades retracted and depressed throughout",
      "Drive feet into the floor for leg drive",
      "Bar should touch the lower chest, not the neck"
    ],
    "muscles": ["chest", "front delts", "triceps"],
    "difficulty": "Intermediate",
    "videoUrl": "https://example.com/bench-press.mp4",
    "imageUrl": "https://example.com/bench-press.jpg"
  },
  "squat": {
    "description": "The king of lower body movements.",
    "tips": ["Brace your core before descent"],
    "muscles": ["quads", "glutes", "hamstrings"],
    "difficulty": "Intermediate"
  }
}
FieldTypeRequiredDescription
descriptionstringrequiredBrief exercise description.
tipsstring[]requiredForm cues shown as bullet points.
musclesstring[]requiredPrimary muscles targeted.
difficultystringrequiredFree-form difficulty label (e.g. "Beginner", "Intermediate", "Advanced").
videoUrlstring (URL)optionalLink to a demo video (MP4). Shown inline in the exercise info sheet.
imageUrlstring (URL)optionalLink to a demo image. Shown if no video is provided.

exerciseId naming. The key in exercises.json must exactly match the exerciseId in your workout items. Use kebab-case (e.g. "bench-press", "box-jump"). The app automatically prettifies the ID for display if no exercises.json entry is found.

Example: Straight Sets

4 sets of 5 bench press, 3-minute rest between sets.

{
  "id": "day-1",
  "title": "Strength Day A",
  "blocks": [
    {
      "type": "straight",
      "rounds": 4,
      "restBetweenRoundsSec": 180,
      "postBlockRestSec": 120,
      "items": [
        {
          "id": "i1",
          "exerciseId": "bench-press",
          "prescription": {
            "mode": "reps",
            "target": { "reps": { "min": 5, "max": 5 } },
            "rpe": 8,
            "tempo": { "down": 3, "pause": 1, "up": "X" },
            "toFailure": "none"
          }
        }
      ]
    }
  ]
}

Example: Circuit

3 rounds of a conditioning circuit with 30s rest between each exercise.

{
  "id": "conditioning-a",
  "title": "Conditioning Circuit",
  "blocks": [
    {
      "type": "circuit",
      "rounds": 3,
      "restBetweenRoundsSec": 90,
      "items": [
        {
          "id": "i1",
          "exerciseId": "burpee",
          "restAfterSec": 30,
          "prescription": {
            "mode": "reps",
            "target": { "reps": { "min": 10, "max": 10 } }
          }
        },
        {
          "id": "i2",
          "exerciseId": "kettlebell-swing",
          "restAfterSec": 30,
          "prescription": {
            "mode": "reps",
            "target": { "reps": { "min": 15, "max": 15 } }
          }
        },
        {
          "id": "i3",
          "exerciseId": "plank-hold",
          "prescription": {
            "mode": "time",
            "target": { "timeSec": { "min": 30, "max": 30 } }
          }
        }
      ]
    }
  ]
}

Example: EMOM

Every 2 minutes for 10 rounds: 5 thrusters + 10 box jumps.

{
  "id": "emom-a",
  "title": "E2MOM — 20 Minutes",
  "blocks": [
    {
      "type": "emom",
      "rounds": 10,
      "emomIntervalMin": 2,
      "items": [
        {
          "id": "i1",
          "exerciseId": "thruster",
          "prescription": {
            "mode": "reps",
            "target": { "reps": { "min": 5, "max": 5 } }
          }
        },
        {
          "id": "i2",
          "exerciseId": "box-jump",
          "prescription": {
            "mode": "reps",
            "target": { "reps": { "min": 10, "max": 10 } }
          }
        }
      ]
    }
  ]
}

Example: Routine (single standalone workout)

A morning mobility routine that lives in the Today tab, always available, with no schedule.

program.json

{
  "programId": "morning-mobility",
  "programTitle": "Morning Mobility",
  "shortDescription": "10-minute daily mobility flow.",
  "contentVersion": "1.0.0",
  "kind": "routine",
  "iconName": "figure.flexibility",
  "phases": []
}

The workouts/ folder can contain one or more JSON files. For a routine, all workout files are loaded alphabetically and listed as a flat collection — no phases or week patterns needed.

Example: Full Multi-Phase Program

A 2-phase, 4-week program with 3 training days per week.

{
  "programId": "push-pull-legs-12w",
  "programTitle": "Push / Pull / Legs — 12 Weeks",
  "shortDescription": "Classic PPL split for hypertrophy.",
  "contentVersion": "1.2.0",
  "kind": "program",
  "iconName": "figure.strengthtraining.traditional",
  "categories": ["hypertrophy", "gym"],
  "phases": [
    {
      "id": "P1",
      "title": "Phase 1: Volume",
      "weeks": [
        { "weekNumber": 1, "pattern": ["push-a", "REST", "pull-a", "REST", "legs-a", "REST", "REST"] },
        { "weekNumber": 2, "pattern": ["push-b", "REST", "pull-b", "REST", "legs-b", "REST", "REST"] }
      ]
    },
    {
      "id": "P2",
      "title": "Phase 2: Intensification",
      "weeks": [
        { "weekNumber": 3, "pattern": ["push-a", "REST", "pull-a", "REST", "legs-a", "REST", "REST"] },
        { "weekNumber": 4, "pattern": ["push-b", "REST", "pull-b", "REST", "legs-b", "REST", "REST"] }
      ]
    }
  ]
}

You'd then have files: workouts/push-a.json, workouts/push-b.json, workouts/pull-a.json, workouts/pull-b.json, workouts/legs-a.json, workouts/legs-b.json.