Start from a program that compiles.
Change the schedule, state, or workout.
Compile and inspect the projection.
A small working program
This is the same valid, minimal program loaded by the Designer. It has one workout and one progressing lift.
starter-squat.is
// For full IronScript documentation, see: https://www.irongains.app/docs
program "Starter Squat" id="starter-squat" {
schedule = [A, rest, A, rest, A, rest, rest]
workout A "Squat Day" {
exercise squat "Squat" {
state {
squat_w: weight "Weight" = 60kg setup
section = "Foo.bar"
}
count = 3
reps = 5
weight = squat_w
rest = 180
progress {
if all_complete {
squat_w = squat_w + 2.5
}
}
}
}
}Language guide
Core language pieces.
Learn the pieces in the order you are most likely to use them: program identity, state, sets, progression, and reusable templates.
Program identity and schedule
IronScript
program "My Program" id="my-program" {
description = "One sentence summary."
about = """
*Bold* highlights, _italic_ nuance, simple lists, and numbered steps are
rendered in program detail views.
- Use `-` or `*` for bullets.
1. Use `1.` style markers for ordered steps.
"""
schedule = [dayA, rest, dayB, rest, dayA, rest, rest]
...
}idmust be unique across all programs. If omitted the parser uses the name.descriptionis the compact one-line summary for cards and lists.aboutis longer guidance for detail views. It remains a plain string in the runtime, but web and mobile render a tiny Markdown-like subset:*bold*,_italic_, bullet lists starting with-or*, and ordered lists starting with1.style markers.scheduleis a repeating sequence: each entry is a workout id or the literalrest.- The scheduler walks the sequence circularly and skips
restentries when it selects the next workout.
State
IronScript
state {
squat_tm = 100
week: counter "Training week" = 1
section = "Progression.cycle"
group = timeline
hint = "Advances after the final workout in the week."
rep_scheme: select "Rep scheme" = 0 setup
option "5x3+"
option "6x2+"
option "10x1+"
warmups: boolean "Include warmups" = false setup
}- Values may be strings, numbers, booleans, or unit-bearing weight literals. Boolean state may be read directly as a condition.
- Bare declarations are internal runtime values. Annotated declarations add athlete-facing metadata without changing the flat state value stored on an enrollment.
- Annotated state accepts
exercise,weight,increment,select,boolean, andcounter.valueslabels a numeric ordinal by zero-based index. Addindex = exprafter the values list when the display ladder depends on other state; without it, the field's own value is used. A select can use repeatedoption "Label"modifiers as a compact label list. Selects store only their scalar index and never patch other state keys.sectionchooses the editor hierarchy, whilegroupgroups related fields within that section.slotremains accepted as a compatibility alias forgroup. setupincludes a typed field in enrollment setup. All typed fields remain adjustable after enrollment. Bare declarations are the only internal state.optionalallows an exercise field to be empty.if = keyhides a field unless another state key is truthy.suggestranks exercise catalog choices in the picker without restricting the selection.- Every variable used in expressions must be declared here, or in the declaring exercise's own
stateblock (see Exercise-scoped state).
Exercises: shorthand vs. explicit sets
Shorthand (one block of identical sets):
IronScript
exercise "Squat" {
count = 5
reps = 5
weight = squat_tm * 0.8
rest = 180
}Explicit (different sets within one exercise):
IronScript
exercise "Squat" {
set { reps = 5 weight = squat_tm * 0.65 rest = 180 }
set { reps = 5 weight = squat_tm * 0.75 rest = 180 }
set { reps = 5 weight = squat_tm * 0.85 rest = 240 amrap = true }
}Set fields:
| Field | Type | Required | Notes |
|---|---|---|---|
count | number | no | How many identical repetitions of this set spec. Defaults to 1 when omitted. |
reps | number | conditional | Target reps per set. A set needs reps, time, or both. |
time | number | conditional | Target duration per set in seconds. A set needs reps, time, or both. |
weight | number | no | Numeric load in the enrollment's active unit; concrete targets are rounded to the user's configured total-load jump |
rpe | number | no | Rate of perceived exertion, 1–10 |
rest | number | no | Rest period in seconds |
amrap | boolean | no | Marks the final repeated set from this set specification as AMRAP or max-time. |
warmup | boolean | no | Marks the set as warmup volume. Warmup sets are logged and displayed, but ignored by progress-rule completion checks. |
count may evaluate to 0 to omit a conditional set spec from the concrete workout. The legacy spelling sets is still accepted as an alias for count.
State-selected exercises
name = ident in the body lets the user pick which exercise fills this slot at enrollment (the state value behind ident is an exercise reference id, not a display string). enabled = ident lets the user toggle the slot on/off.
IronScript
exercise "T2 Squat" {
name = t2_squat_id
enabled = t2_squat_enabled
count = 3
reps = 10
weight = squat_tm * 0.5
}Progress rules
Triggered after each workout completion. All expressions evaluate against a snapshot of state taken before any updates in the block are applied. Order within the block does not matter.
IronScript
progress {
if all_complete {
week = week < 4 ? week + 1 : 1
squat_tm = week == 4 ? squat_tm + 5 : squat_tm // sees old week, not the updated one
}
}Conditions:
all_complete: every work set in the workout was completed or explicitly skippedany_complete: at least one set completedalways: fires unconditionallynot_all_complete: at least one work set was left unfinished
When a progress block lives inside an exercise, it fires per exercise and sees bindings scoped to that exercise's logged sets (warmups excluded):
complete/incomplete: all work sets were resolved (done or skipped), or at least one was left unfinishedamrap_reps: reps logged on the final AMRAP settotal_reps: total reps completed across all work sets, regardless of weightreps_at_target_weight: reps completed at or above each set's prescribed weighttotal_volume: total weight × reps across completed work setstotal_time/amrap_time: seconds for timed setscompleted_sets/number_of_sets: counts of done vs prescribed work setscompleted_reps[N],target_reps[N],completed_weight[N],target_weight[N],completed_time[N], andtarget_time[N]: 1-indexed per-set values.
All runtime-provided bindings use snake_case, matching authored state keys.
Array indexing for lookup tables
The [e0, e1, ...][idx] primary lets you build week/tier lookup tables inline. Index is clamped to bounds.
IronScript
reps = [5, 3, 1, 5][week - 1]
weight = squat_tm * [0.85, 0.90, 0.95, 0.60][week - 1]Exercise-scoped state
An inline exercise can declare its own state { } block. Its keys are hoisted into the flat program state under the exercise's slot prefix, and every reference to them inside that exercise is rewritten to match, so the block reads as private to the slot without any hand-prefixing:
IronScript
workout day1 "Press Day" {
exercise press "Overhead Press" {
state {
tm: weight "Training max" = 95lbs setup
inc: increment "Increase" = 5lbs setup
}
count = 5
reps = 5
weight = tm * 85%
progress {
if complete {
message = "Adding {inc}."
tm = tm + inc
}
}
}
}compiles to day1_press_tm and day1_press_inc. Two exercises may each declare tm without colliding.
- A stable slot id is required. State keys are derived from the slot id (
exercise press "Overhead Press"→day1.press→day1_press_), so an exercise that declares state and relies on the positional fallback is a parse error. The one exercise of a single-exercise template is exempt: its slot id is theasalias, which is already stable. - Shadowing. A local key hides an outer key of the same name (program-level, or the enclosing template's) inside that exercise only. Every other exercise still sees the outer one.
- One-way privacy. Program-level state stays readable from inside the block (
weight = tm * week), but nothing outside the exercise can reference a local key. Anything two exercises share belongs in the program-levelstateblock: a training max used by both a heavy slot and a supplemental slot,week,cycle, an increment applied to several lifts. - Grouping. Typed fields land on one setup card per exercise (
slotKey = day1_pressin the compiled compatibility metadata), under the exercise's own name inside the workout's section. Addinggroup = identinside the block subdivides that card further. The olderslot = identspelling compiles identically. - Templates. An exercise inside a
templatemay declare state the same way. Its prefix comes from the slot id, which already carries theasalias, so a lone exercise in a template produces exactly the keys the template's ownstateblock would have produced. The two spellings are interchangeable, and moving a block between them does not change a single compiled key:
IronScript
template t1(default_weight) label = "T1" {
exercise "T1 Lift" {
state { tm: weight "Working weight" = default_weight setup }
count = 5 reps = 3 weight = tm
}
}
workout A1 "Day A1" { use t1(60kg) as "A1.t1" } // → A1_t1_tmPast one exercise the keys split further, per exercise (A1_t1_main_tm), and each state-declaring exercise needs an explicit slot id so its keys do not move when the template body is reordered. A template-level state block is still the place for anything the template's exercises share; label still supplies the card group label either way. The built-in GZCLP programs use the exercise-declared form.
Full-stack templates
Templates declare state fields and exercises together. When expanded via use ... as "prefix", all identifiers are automatically prefixed.
IronScript
template t1(default_name, default_weight, default_inc) label = "T1" {
state {
name: exercise "T1 Exercise" = default_name setup
tm: weight "Start Weight" = default_weight setup
inc: increment "Increment" = default_inc setup
scheme: select "Rep scheme" = 0
option "5x3+"
option "6x2+"
option "10x1+"
}
exercise "T1 Lift" {
name = name
count = [5, 6, 10][scheme]
reps = [3, 2, 1][scheme]
weight = tm
amrap = true
progress {
if complete { tm = tm + inc }
if incomplete { scheme = scheme < 2 ? scheme + 1 : 0 }
}
}
}
workout A "Day A" {
use t1("Squat", 60, 5) as "A.t1"
}The as "A.t1" clause:
- Prefixes all identifiers:
tm->A_t1_tm,scheme->A_t1_scheme, etc. - Sets the state section for UI grouping.
- Template parameters (
default_name, etc.) are compile-time literal values substituted into declarations.
The optional template label is authored presentation metadata for every expanded section. When it is omitted, no section display label is compiled. Internal as aliases remain stable identifiers and clients must not derive user-facing copy from their spelling. The enclosing workout name supplies the parent section label.
Validated examples
Start from a complete program.
These examples are compiled during docs generation, so they are safer starting points than inventing syntax from scratch.
Assistant workflow
Give an assistant the public authoring guide.
Share the authoring guide and a close working example, then validate the result in the Designer before saving it.
Assistant guide
A compact index that points assistants to the public program-authoring guide.
Open llms.txtDraft it in the Designer
Paste the result into the Designer to compile it, inspect the projection, and fix invalid or surprising behavior.
Open the Designer