IronScript overview

Write the logic behind your training.

IronScript is the readable source behind every program. Define the schedule, setup fields, workouts, sets, and progression rules, then inspect exactly what will happen before you train.

01

Start from a program that compiles.

02

Change the schedule, state, or workout.

03

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

.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

.is
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]
  ...
}
  • id must be unique across all programs. If omitted the parser uses the name.
  • description is the compact one-line summary for cards and lists.
  • about is 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 with 1. style markers.
  • schedule is a repeating sequence: each entry is a workout id or the literal rest.
  • The scheduler walks the sequence circularly and skips rest entries when it selects the next workout.

State

IronScript

.is
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, and counter. values labels a numeric ordinal by zero-based index. Add index = expr after the values list when the display ladder depends on other state; without it, the field's own value is used. A select can use repeated option "Label" modifiers as a compact label list. Selects store only their scalar index and never patch other state keys. section chooses the editor hierarchy, while group groups related fields within that section. slot remains accepted as a compatibility alias for group.
  • setup includes a typed field in enrollment setup. All typed fields remain adjustable after enrollment. Bare declarations are the only internal state.
  • optional allows an exercise field to be empty. if = key hides a field unless another state key is truthy. suggest ranks 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 state block (see Exercise-scoped state).

Exercises: shorthand vs. explicit sets

Shorthand (one block of identical sets):

IronScript

.is
exercise "Squat" {
  count  = 5
  reps   = 5
  weight = squat_tm * 0.8
  rest   = 180
}

Explicit (different sets within one exercise):

IronScript

.is
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:

FieldTypeRequiredNotes
countnumbernoHow many identical repetitions of this set spec. Defaults to 1 when omitted.
repsnumberconditionalTarget reps per set. A set needs reps, time, or both.
timenumberconditionalTarget duration per set in seconds. A set needs reps, time, or both.
weightnumbernoNumeric load in the enrollment's active unit; concrete targets are rounded to the user's configured total-load jump
rpenumbernoRate of perceived exertion, 1–10
restnumbernoRest period in seconds
amrapbooleannoMarks the final repeated set from this set specification as AMRAP or max-time.
warmupbooleannoMarks 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

.is
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

.is
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 skipped
  • any_complete: at least one set completed
  • always: fires unconditionally
  • not_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 unfinished
  • amrap_reps: reps logged on the final AMRAP set
  • total_reps: total reps completed across all work sets, regardless of weight
  • reps_at_target_weight: reps completed at or above each set's prescribed weight
  • total_volume: total weight × reps across completed work sets
  • total_time / amrap_time: seconds for timed sets
  • completed_sets / number_of_sets: counts of done vs prescribed work sets
  • completed_reps[N], target_reps[N], completed_weight[N], target_weight[N], completed_time[N], and target_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

.is
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

.is
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.pressday1_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 the as alias, 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-level state block: 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_press in the compiled compatibility metadata), under the exercise's own name inside the workout's section. Adding group = ident inside the block subdivides that card further. The older slot = ident spelling compiles identically.
  • Templates. An exercise inside a template may declare state the same way. Its prefix comes from the slot id, which already carries the as alias, so a lone exercise in a template produces exactly the keys the template's own state block would have produced. The two spellings are interchangeable, and moving a block between them does not change a single compiled key:

IronScript

.is
  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_tm

Past 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

.is
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.txt

Draft 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