This tool allows you to quickly build your weightlifting programs, ensure you have proper weekly volume per muscle group, and balance it with the time you spend in a gym. You can build multi-week programs, plan your mesocycles, deload weeks, testing 1RM weeks, and see the weekly undulation of volume and intensity of each exercise on a graph.
Set the program name, create weeks and days, type the list of exercises for each day, putting each exercise on a new line, along with the number of sets and reps after slash (/) character, like this:
Squat /3x3-5
Romanian Deadlift /3x8
Autocomplete will help you with the exercise names. You can also create custom exercises if they're missing in the library.
On the right you'll see Weekly Stats, where you can see the number of sets per week per muscle group, whether you're in the recommended range (indicated by color), strength/hypertrophy split, and if you hover a mouse over the numbers - you'll see what exercises contribute to that number, and how much.
The exercise syntax supports RPEs , percentage of 1RM, rest timers, various progressive overload types, etc. Read more about the features in the docs!
When you're done, you can convert this program to Liftosaur program, and run what you planned in the gym, using the Liftosaur app!
To use this program:
Install Liftosaur app
Copy the link to this program by clicking on below
Import the link in the app, on the Choose Program screen.
{"exportedProgram":{"customExercises":{},"program":{"vtype":"program","id":"vilhyqkx","name":"Dynamic double progression","url":"","author":"","shortDescription":"","description":"","nextDay":1,"weeks":[],"isMultiweek":false,"days":[{"id":"cwzjtwfi","name":"Day 1","exercises":[]}],"exercises":[],"tags":[],"deletedDays":[],"deletedWeeks":[],"deletedExercises":[],"clonedAt":1777737966737,"planner":{"vtype":"planner","name":"Dynamic double progression","weeks":[{"name":"Week 1","days":[{"name":"Day 1","exerciseText":"/// Dynamic double progression\n/// Double progression for strength exercises with reps. Analyzes each set independently and determines the next session's weight based on three\n/// factors: rep range completion, RPE, and the difference between completed and programmed weight.\n///\n/// Parameter `step`:\n/// 0 kg (default) — uses the RPE table formula: estimates e1RM from the completed set and projects the optimal weight for the target rep range and RPE.\n/// The increment adapts to how far from failure you were working.\n/// > 0 kg — fixed step; formula is ignored. Weight increases or decreases by exactly the specified amount.\n/// Parameter `workingSetToUpdateRm1`:\n/// After finishing the exercise, the app will ask which working set to use for updating 1RM.\n/// - enter the set index (e.g. 1, 2, 3) whose e1RM estimate should be used to update rm1;\n/// the most representative set is typically the one taken to failure (RPE 10);\n/// - enter <= 0 to skip the 1RM update for this exercise.\n/// Parameter `defaultWorkingSetToUpdateRm1`:\n/// The value workingSetToUpdateRm1 is reset to after each progression execution. Must equal workingSetToUpdateRm1.\n/// - set to a representative working set index if 1RM updates are the norm (e.g. 2 for the second set);\n/// - set <= 0 if 1RM updates should be skipped by default.\n/// Parameter `bodyweightFraction`:\n/// Fraction of bodyweight that contributes to the total load on the working muscles.\n/// Used to correctly display the assist load (or added load) during the workout via the Update Prints block.\n/// 0 (default) — bodyweight does not contribute; use for standard barbell/dumbbell/machine exercises\n/// where the working muscles do not support bodyweight.\n/// 0–1 — partial bodyweight contribution; use for exercises where only part of bodyweight is loaded,\n/// e.g. incline push-ups or ring rows where the angle reduces effective bodyweight.\n/// 1 — full bodyweight contribution; use for exercises where the muscles support the full bodyweight:\n/// pull-ups, dips, push-ups, etc.\n/// When set, the Update block prints at workout start:\n/// Line 1: effective bodyweight contribution (bodyweightFraction × bodyweight)\n/// Per set: total programmed weight, and the assist/added load (weight − bodyweightFraction × bodyweight)\n/// Convention: programmed weight represents the real load on the muscles (bodyweight + added, or bodyweight − assist).\n/// A positive value means added weight (weighted pull-up); a negative assist load means the machine reduces total load.\n/// This keeps the progression graph on a single consistent scale across the full assisted → weighted continuum.\n///\n/// Tuning notes:\n/// RPE sensitivity thresholds scale with proximity to failure — conservative by design. Works best with consistent, precise RPE logging.\n/// Rep failure threshold is set at 80% of minReps. Minor shortfalls are treated as noise; only clear failures trigger weight reduction.\n///\n/// ABBREVIATIONS AND DECISION TABLE\n/// PW Programmed Weight (current target weight)\n/// CW Completed Weight (actual lifted weight)\n/// IW Increased Weight — formula-based or step-based weight increase\n/// DW Decreased Weight — formula-based or step-based weight decrease\n/// W= Completed weight equals programmed weight\n/// W↑ Completed weight above programmed weight\n/// W↓ Completed weight below programmed weight\n/// R= Actual RPE within tolerance of target RPE\n/// R↓ Actual RPE lower than target RPE (easier / deliberate deload)\n/// R↑ Actual RPE higher than target RPE (harder than planned)\n/// ≥top Completed reps greater than or equal to top of rep range\n/// <min Completed reps below adjusted minimum threshold\n/// In range Completed reps in range\n///\n/// # Weight RPE Reps Decision\n/// -------------------------------------\n/// 1 W= R= ≥top IW\n/// 2 W= R= In range PW (CW = PW)\n/// 3 W= R= <min DW\n/// 4 W= R↓ ≥top IW\n/// 5 W= R↓ In range PW (CW = PW)\n/// 6 W= R↓ <min PW (CW = PW)\n/// 7 W= R↑ ≥top PW (CW = PW)\n/// 8 W= R↑ In range PW (CW = PW)\n/// 9 W= R↑ <min DW\n/// 10 W↑ R= ≥top IW\n/// 11 W↑ R= In range CW\n/// 12 W↑ R= <min DW\n/// 13 W↑ R↓ ≥top IW\n/// 14 W↑ R↓ In range CW\n/// 15 W↑ R↓ <min PW\n/// 16 W↑ R↑ ≥top CW\n/// 17 W↑ R↑ In range CW\n/// 18 W↑ R↑ <min DW\n/// 19 W↓ R= ≥top IW\n/// 20 W↓ R= In range CW\n/// 21 W↓ R= <min DW\n/// 22 W↓ R↓ ≥top PW\n/// 23 W↓ R↓ In range PW\n/// 24 W↓ R↓ <min PW\n/// 25 W↓ R↑ ≥top CW\n/// 26 W↓ R↑ In range CW\n/// 27 W↓ R↑ <min DW\nddp / used: none / update: custom() {~\n if (setIndex == 0) {\n if (state.bodyweightFraction > 0) {\n print(state.bodyweightFraction * bodyweight)\n }\n for (var.i in completedReps) {\n /// Ask the user what the actual number of reps, RPE, and weight were\n amraps[var.i] = 1\n logrpes[var.i] = 1\n askweights[var.i] = 1\n \n if (state.bodyweightFraction > 0) {\n print(weights[var.i], weights[var.i] - state.bodyweightFraction * bodyweight)\n }\n }\n }\n~} / progress: custom(step: 0kg, workingSetToUpdateRm1+: 2, defaultWorkingSetToUpdateRm1: 2, bodyweightFraction: 0) {~\n for (var.i in completedReps) {\n /// Use logged RPE if available, otherwise fall back to programmed RPE\n var.actualRPE = completedRPE[var.i] > 0 ? completedRPE[var.i] : RPE[var.i]\n\n /// Estimate 1RM from completed set, then project to target rep/RPE zone\n var.e1rm = completedWeights[var.i] / rpeMultiplier(completedReps[var.i], var.actualRPE)\n var.projectedWeight = roundWeight(var.e1rm * rpeMultiplier(minReps[var.i], RPE[var.i]))\n\n /// Formula-driven weight targets for each direction:\n /// formulaIncrease: formula result if it's actually higher than completed weight, otherwise one increment above completed weight\n var.formulaIncrease = var.projectedWeight > completedWeights[var.i] ? var.projectedWeight : increment(completedWeights[var.i])\n /// formulaDecrease: formula result if it's actually lower than completed weight, otherwise one decrement below completed weight\n var.formulaDecrease = var.projectedWeight < completedWeights[var.i] ? var.projectedWeight : decrement(completedWeights[var.i])\n\n /// Final weight targets used in the decision table.\n /// If a fixed weight step (in kg) is specified, it overrides the formula; otherwise the formula-driven values are used.\n var.increasedWeight = state.step != 0 ? roundWeight(completedWeights[var.i] + state.step) : var.formulaIncrease\n var.decreasedWeight = state.step != 0 ? roundWeight(completedWeights[var.i] - state.step) : var.formulaDecrease\n\n /// Consider RPE decreased (meaningfully easier than planned) if:\n /// RPE <= 8: drop > 1 (far from failure — strict noise filter)\n /// 8 < RPE <= 9: drop >= 1 (approaching failure — moderate sensitivity)\n /// 9 < RPE <= 10: drop > 0 (near failure — any drop is meaningful)\n ///\n /// Consider RPE increased (meaningfully harder than planned) if:\n /// RPE <= 7: rise > 1 (far from failure — strict noise filter)\n /// 7 < RPE <= 8: rise >= 1 (approaching failure — moderate sensitivity)\n /// 8 < RPE < 10: rise > 0 (near failure — any rise is meaningful)\n ///\n /// rpeShift: -1 = meaningfully easier than planned\n /// 0 = within noise\n /// 1 = meaningfully harder than planned\n if (RPE[var.i] <= 8 && var.actualRPE < RPE[var.i] - 1) {\n var.rpeShift = -1\n } else if (RPE[var.i] > 8 && RPE[var.i] <= 9 && var.actualRPE <= RPE[var.i] - 1) {\n var.rpeShift = -1\n } else if (RPE[var.i] > 9 && RPE[var.i] <= 10 && var.actualRPE < RPE[var.i]) {\n var.rpeShift = -1\n } else if (RPE[var.i] <= 7 && var.actualRPE > RPE[var.i] + 1) {\n var.rpeShift = 1\n } else if (RPE[var.i] > 7 && RPE[var.i] <= 8 && var.actualRPE >= RPE[var.i] + 1) {\n var.rpeShift = 1\n } else if (RPE[var.i] > 8 && RPE[var.i] < 10 && var.actualRPE > RPE[var.i]) {\n var.rpeShift = 1\n } else {\n var.rpeShift = 0\n }\n\n /// weightShift: -1 = completed weight lighter than prescribed\n /// 0 = on target\n /// 1 = heavier than prescribed\n if (completedWeights[var.i] <= decrement(weights[var.i])) {\n var.weightShift = -1\n } else if (completedWeights[var.i] >= increment(weights[var.i])) {\n var.weightShift = 1\n } else {\n var.weightShift = 0\n }\n\n /// Clear failure threshold: completed < 80% of minReps\n /// Integer math (no floats): completedReps * 5 < minReps * 4\n /// Scales naturally with rep range:\n /// minReps=5 → fail if < 4 (1 rep short)\n /// minReps=10 → fail if < 8 (2 reps short)\n /// minReps=20 → fail if < 16 (4 reps short)\n ///\n /// repsShift: -1 = clearly too few reps\n /// 0 = in range (including up to 20% short)\n /// 1 = at or above upper bound\n if (completedReps[var.i] * 5 < minReps[var.i] * 4) {\n var.repsShift = -1\n } else if (completedReps[var.i] >= reps[var.i]) {\n var.repsShift = 1\n } else {\n var.repsShift = 0\n }\n\n if (var.repsShift == 1) {\n /// Reps at or above upper bound (≥ top)\n if (var.rpeShift == 0) {\n /// Rows 1, 10, 19 — W= R= Reps≥top → IW, W↑ R= Reps≥top → IW, W↓ R= Reps≥top → IW\n weights[var.i] = var.increasedWeight\n } else if (var.rpeShift == -1) {\n /// Row 22 — W↓ R↓ Reps≥top → PW (deload with lighter weight; hold prescribed)\n if (var.weightShift != -1) {\n /// Row 4 — W= R↓ Reps≥top → IW, Row 13 — W↑ R↓ Reps≥top → IW\n weights[var.i] = var.increasedWeight\n }\n } else {\n /// rpeShift == 1 — always CW regardless of weight shift (W= → CW = PW)\n /// Row 7 — W= R↑ Reps≥top → CW, Row 16 — W↑ R↑ Reps≥top → CW, Row 25 — W↓ R↑ Reps≥top → CW\n weights[var.i] = completedWeights[var.i]\n }\n } else if (var.repsShift == 0) {\n /// Reps in working range\n if (var.rpeShift == -1) {\n /// Row 5 — W= R↓ In range → PW, Row 23 — W↓ R↓ In range → PW\n if (var.weightShift == 1) {\n /// Row 14 — W↑ R↓ In range → CW (heavier weight, lower RPE: anchor to what was done)\n weights[var.i] = completedWeights[var.i]\n }\n } else {\n /// rpeShift == 0 or 1 — always CW regardless of weight shift (W= → CW = PW)\n /// Row 2 — W= R= In range → CW\n /// Row 8 — W= R↑ In range → CW\n /// Row 11 — W↑ R= In range → CW\n /// Row 17 — W↑ R↑ In range → CW\n /// Row 20 — W↓ R= In range → CW\n /// Row 26 — W↓ R↑ In range → CW\n weights[var.i] = completedWeights[var.i]\n }\n } else {\n /// repsShift == -1 — clearly too few reps (< minReps)\n if (var.rpeShift == -1) {\n /// Row 6 — W= R↓ Reps<min → PW, Row 15 — W↑ R↓ Reps<min → PW, Row 24 — W↓ R↓ Reps<min → PW\n /// RPE dropped (intentional deload or fatigue management); hold prescribed weight\n } else {\n /// rpeShift == 0 or 1 — genuine failure to complete reps\n /// Row 3 — W= R= Reps<min → DW\n /// Row 9 — W= R↑ Reps<min → DW\n /// Row 12 — W↑ R= Reps<min → DW\n /// Row 18 — W↑ R↑ Reps<min → DW\n /// Row 21 — W↓ R= Reps<min → DW\n /// Row 27 — W↓ R↑ Reps<min → DW\n weights[var.i] = var.decreasedWeight\n }\n }\n\n /// If the current set matches the user-specified working set — update rm1 from its e1RM estimate, then reset the prompt to its default value\n if (state.workingSetToUpdateRm1 == var.i) {\n rm1 = var.e1rm\n }\n }\n state.workingSetToUpdateRm1 = state.defaultWorkingSetToUpdateRm1\n~}"}]}]}},"version":"20260304084247","settings":{"timers":{"warmup":90,"workout":180,"reminder":900},"units":"kg"}},"shouldSyncProgram":false,"isMobile":false,"revisions":[]}