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.
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 |
|---|---|---|---|
programId | string | required | Unique identifier for the program. Used internally to track progress. |
programTitle | string | required | Display name shown in the app. |
contentVersion | string | required | Semver string (e.g. "1.0.0"). Used to detect updates when the program is hosted on GitHub. |
kind | "program" | "routine" | optional | Defaults to "program". Use "routine" for standalone, always-available workouts with no fixed schedule. |
shortDescription | string | optional | One-line description shown in program listings. |
iconName | string | optional | SF Symbol name for the program icon (e.g. "figure.run", "dumbbell.fill"). |
categories | string[] | optional | Category tags for grouping (e.g. ["strength", "barbell"]). |
isMinorUpdate | boolean | optional | Set 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. |
updateNotes | string | optional | Human-readable changelog shown when an update is available. |
phases | array | required for programs | Array of phases (see below). Not needed for kind: "routine". |
Phase
| Field | Type | Description |
|---|---|---|
id | string | Phase identifier (e.g. "P1", "P2"). Displayed to the user. |
title | string | Phase display name (e.g. "Phase 1: Foundation"). |
weeks | array | Array of week definitions. |
Week
| Field | Type | Description |
|---|---|---|
weekNumber | number | Week number within the phase (1, 2, 3…). Used for display only. |
pattern | string[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"
}
}
]
}
]
}
| Field | Type | Required | Description |
|---|---|---|---|
id | string | required | Must match the filename and the pattern reference (e.g. "1A" → workouts/1A.json). |
title | string | required | Display name shown in the workout player header. |
blocks | array | required | Array 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.
| Field | Type | Required | Description |
|---|---|---|---|
type | "straight" | "circuit" | "emom" | "accumulation" | required | Block execution format. |
rounds | number | required | Number of rounds (or minutes for EMOM). |
items | array | required | Array of exercises in this block. |
restBetweenRoundsSec | number | optional | Rest in seconds between rounds of this block. |
postBlockRestSec | number | optional | Rest in seconds after the entire block, before the next block starts. |
emomIntervalMin | number | optional | EMOM 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
| Mode | Description | Target 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 }
}
}
| Field | Type | Required | Description |
|---|---|---|---|
mode | "reps" | "time" | "totalReps" | required | Prescription type. |
target.reps | {"min": n, "max": n} | optional | Rep range. Omit entirely for max reps. |
target.timeSec | {"min": n, "max": n} | optional | Time range in seconds. Use equal min/max for a fixed duration. |
target.totalReps | number | optional | Total rep target for totalReps mode. |
rpe | 1–10 | optional | Rate 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". |
tempo | object | optional | Movement 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.
| Field | Description |
|---|---|
down | Eccentric / lowering phase. Number of seconds, or "X" for explosive. |
pause | Pause at the bottom. Number of seconds, or "X". |
up | Concentric / 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.
| Field | Where it lives | When it fires |
|---|---|---|
restAfterSec | On each item | After an exercise, before the next one in the same round. Mainly useful in circuits to create unequal rest between exercises. |
restBetweenRoundsSec | On the block | After completing one full round of the block, before starting the next round. |
postBlockRestSec | On the block | After 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"
}
}
| Field | Type | Required | Description |
|---|---|---|---|
description | string | required | Brief exercise description. |
tips | string[] | required | Form cues shown as bullet points. |
muscles | string[] | required | Primary muscles targeted. |
difficulty | string | required | Free-form difficulty label (e.g. "Beginner", "Intermediate", "Advanced"). |
videoUrl | string (URL) | optional | Link to a demo video (MP4). Shown inline in the exercise info sheet. |
imageUrl | string (URL) | optional | Link 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.