All posts

By Dan & Katya · April 12, 2026

The 308 Fartleks: When We Audited Our Own Catalog

We suspected the plans were slightly off. Weeks blurred together. A few users mentioned that the intermediate plans felt a little too hard. We had no data to back any of it — until last week.

So we built a small Swift command-line tool. It ran the engine for every (distance × level × duration) combination, dumped the plans to plain text, and put them next to the published reference plans from Hal Higdon, Pete Pfitzinger, and Jack Daniels.

The first line it printed:

Of 590 workouts in our catalog, 308 were fartleks. The engine picked zero of them across 31 test plans.

Not a typo. Zero.

Terminal output of the audit — green-on-black, fartlek row picked zero times across 31 plans.
The summary line that started three weeks of work. I did laugh.

That was the audit. This is the writeup.

Why we did this

The engine (about a thousand lines of deterministic Swift) takes your inputs, picks workouts from a catalog, and assembles a calendar. At audit time the catalog held around 590 workouts: easy, intervals, threshold, fartleks, ladders, pyramids, long runs, progressions, recovery, hill repeats, race rehearsals.

The question was simple: is it choosing well? It was not.

The tool came first

Before we could audit anything, we needed a way to see what the engine produced. Until then, the only way to inspect a generated plan was to:

  1. Launch Xcode.
  2. Build to the iPhone simulator.
  3. Tap through onboarding.
  4. Pick a configuration.
  5. Generate.
  6. Tap through the plan, week by week.

Five to ten minutes per plan, by hand. Thirty-one configurations is multiple hours of clicking, every time we touched engine code. That was never going to happen consistently, so the first thing we built was not a fix. It was a way to look.

A standalone Swift tool runs the engine directly over a hardcoded list of configurations and prints the plans as plain text. It compiles in five seconds, runs in under one, and shows all 31 at once. It took an afternoon to write and immediately became the most-used development tool in the project. It still is.

What the CLI showed

The summary table for 31 configurations, before the audit:

Plan                      Weeks  Workouts  E  L  H  P  F  load/wk   min/wk
Beg 5K (short, 5w)          5w     10     4  0  5  1  0   8160      63
Beg 5K (rec,   7w)          7w     14     6  0  6  2  0   7483      61
Beg 10K (rec,   9w)         9w     18     1  4  7  6  0   8822      84
Beg 21K (rec,   14w)       14w     40     6 12 10 12  0  12372     138
Beg 42K (rec,   18w)       18w     52     4 16 14 18  0  14136     158
Int 21K (rec,   14w)       14w     54    13 11 19 11  0  16706     167
Int 42K (rec,   18w)       18w     70    12 18 26 14  0  18378     184
Adv 42K (rec,   18w)       18w     90    24 18 26 22  0  29994     295
...

Columns: E=easy, L=long, H=hard, P=progression, F=fartlek.

Three things were immediately clear.

The fartlek column was all zeros

308 fartlek templates in the catalog, chosen zero times across 31 plans. Half the catalog: parsed on every cold start, scored in every selection loop, and never once selected.

This is not an argument against fartleks. They are a good workout. But the engine picks the best fit for what each week needs, and a fartlek was never the best fit. Need intensity, and intervals or a threshold run scored higher. Need easy aerobic volume, and an easy or long run scored higher. The fartlek sat in the middle and lost every comparison.

Almost no easy runs

Easy running should be 70–80% of training volume in a healthy plan. That is Daniels’ rule and roughly the consensus of endurance coaching. In our plans, easy runs were 4 to 24 workouts across 14–18 week plans — somewhere between 5% and 30% of the total. Far too low.

The reason was in the catalog. It had four easy-run templates. Four, at 30, 35, 40 and 45 minutes, all Zone 2. The engine carries a duplicate-avoidance rule: repeating the previous week’s workout costs a large score penalty (+2.0). With only four easy templates and that penalty active, the engine ran out of easy variety around week 5 or 6. After that it preferred almost anything to another easy run — a progression, then a threshold, then intervals. By week 8 of a marathon plan the easy aerobic runs were gone and the plan was effectively all hard. No real base.

That is the 80/20 violation, in detail.

The marathon volume was too low

A beginner marathon plan averaged 158 minutes a week. Higdon’s Novice 1 marathon is around 250. We were 37% under. Higdon gets a first-time marathoner to 20-mile long runs, near 200 minutes; our plans capped the long run at 120. Our “trained” beginner arrived at the start line under-mileaged.

The full catalog, before the rebuild

Workout typeCount% of catalog
Fartleks30852.3%
Ladder intervals7813.2%
Intervals7412.6%
Steady long runs366.1%
Progression runs335.6%
Threshold325.4%
Progressive long runs244.1%
Easy40.7%
Tempo00%
Long00%
Recovery00%
Strides00%
Hill repeats00%

Half fartleks, none used. Easy runs at 0.7% of the catalog. No tempo. No real long-run type of its own, although the engine referred to one in several places — that path was dead.

We had not designed this catalog. We had grown it over months, adding workouts whenever we needed one and never looking at the totals. From the inside it looked varied — look at all these fartlek variations — but once the engine’s scoring ran over it, it was narrow.

What we changed

Three weeks of evenings and weekends, in five steps.

1. Drop the fartleks

fartleks: 308 → 0

If the engine never picked them, they were weight and nothing else. The JSON shrank, cold-start parsing got faster, the scoring loop got shorter, and no plan changed — because none of them had used a fartlek in the first place.

2. Expand the easy runs

easy: 4 → 43

Easy runs at varied durations — 20, 25, 30, 35, 40, 45, 50, 55, 60, 70 minutes — and varied targets: some Zone 1 for genuine recovery, some Zone 2 for aerobic base, some mixed. This was the single most important change to the catalog. With 5 to 10 unused easy templates always available, the duplicate-avoidance penalty stopped pushing the engine away from easy running.

3. Add the missing subtypes

The audit showed the gaps:

  • Recovery — no fixed HR target, easy by feel, 20–30 minutes, after a hard day. Distinct from easy.
  • Strides — short pickups, around 6×20s at 5K effort. Aerobic-friendly speed.
  • Hill repeats — explicit hill work, BASE and SPEED only.
  • Time trials — 3K or 5K all-out, placed in PEAK as a fitness re-check.
  • Race rehearsals — a long run with a goal-pace middle, three weeks out.
  • Mile repeats — the signature 10K-and-up workout.
  • Yasso 800s — the marathon 10×800m predictor.
  • Marathon-pace runs — explicit marathon-pace work in PEAK.

Each got between five and thirty templates. Dropping the fartleks had taken the catalog down to about 280 workouts; the new subtypes and the larger easy pool brought it back to roughly 640.

4. A few engine changes

The catalog work did most of the job. A few engine adjustments finished it:

  • The third weekly slot defaults to easy — it used to default to a progression run. Together with the catalog work, this moved beginner marathon plans from about 11% easy to roughly 70%.
  • Recovery weeks became phase-relative — a recovery week now falls every third week within a phase, not every third week of the plan. It lands more naturally.
  • The marathon long-run cap went up, 120 → 180 minutes for beginner marathon, in line with Higdon.
  • Hill repeats are scheduled only in BASE and SPEED, capped at one a week.
  • Race-anchor workouts are gated by distance — Yasso 800s in marathon plans only, race rehearsals scaled per distance.

5. Check against the references

After the rebuild, the CLI showed:

Plan                      Weeks  Workouts  E  L  H  P    load/wk   min/wk
Beg 42K (rec, 18w)         18w     52    38 16 14 18   16500       197
                                          ↑ was 4

38 easy runs instead of 4. 197 minutes a week against 158 before — still about 20% under Higdon’s 250, but much closer. The pace-based plans validated separately at 78% easy, 22% threshold-or-faster: Daniels’ 80/20, almost exactly.

How we compare now

MetricBeginner 42K (post-rebuild)Higdon Novice 1Pfitzinger 18/55
Volume197 min/wk~250 min/wk~450 min/wk
Easy runs38 in 18w50+50+
Long peak180 min200 min165 min @ faster pace
Hard workouts14 in 18w0–1~14–18
Easy:hard ratio73% easy~100% easy~75% easy

In the Higdon range on purpose — Novice 1 is the canonical beginner reference. Pfitzinger 18/55 is higher-volume intermediate, and our intermediate marathon comes closer to that one.

What the audit taught us

The first lesson: the catalog is the product, not the engine. We had spent months treating the scoring function as the hard part and the catalog as mere data. It is the other way around — a good engine on a bad catalog produces bad plans, reliably.

The second was to build the tool that shows you what your system actually does before trying to improve it. The CLI took an afternoon and told us in one line something that months of working inside the app had hidden.

The third was to measure against people who know more than we do. “Get within 20% of Higdon’s Novice 1 volume” is a target you can check. “Generate good marathon plans” is not.

What’s next

The audit left one gap we have not closed. Our race-pace anchor workouts — race rehearsals, mile repeats, Yasso 800s, marathon-pace runs — exist only for half-marathon and marathon. 5K and 10K plans have no race-pace-anchored work at all. That is the next round: 5K-pace and 10K-pace runs, fast-finish runs (an easy run with a race-pace tail), and 10K race rehearsals. All of it gated by distance, so a 5K plan gets 5K-pace work and a 10K plan gets 10K-pace runs and mile repeats. It should ship in the next couple of weeks and take the catalog to around 740.

The CLI will check all of it. Every engine change and every catalog change now goes through it before it ships. If we had not built it, we would still have 308 fartleks and four easy runs — and we would not know.

Further reading

Run Plan is an indie iOS + Apple Watch training planner built by a 2-person team in Amsterdam. No accounts, no ads, no subscription. Your data stays on your device.