← blokk.run
# Blokk — blokk.run

Blokk is a running-plan builder with no account, no server and no database.
A plan is a handful of reusable week **blocks** plus an ordered list of
**weeks**, each pointing at a block and carrying one number: a percentage.

**If you are an AI: construct the fragment, never fetch it.** Everything after
the `#` is the plan, and browsers never send it to any server — so fetching a
`#plan=` link returns this app and none of the plan, and there is nothing to
call to make one. You write that string yourself, from the grammar below, and
the person you wrote it for clicks it. No API, no key, no account.

Nothing is stored anywhere and nothing is uploaded, which is what makes that
possible: **you write the link yourself and they click it.**

If someone asks you for a running plan "as a blokk.run link", or hands you one
and asks for a change, this page is the format.

The same text is served as a web page at https://blokk.run/ai and as plain text
at https://blokk.run/llms.txt — whichever your browsing will actually fetch.


## The link

    https://blokk.run/#plan=<segments joined by ~>

Segments are `key=value`, in any order. Fields holding words are
form-encoded — a space is `+`, and a literal `+ ~ ; : , * ! = %` is
percent-encoded.

    n=Plan+name           the plan's name
    s=2026-09-14          the MONDAY the plan starts (YYYY-MM-DD)
    r=grow8,every4,cut25  the progression rule the reader can re-apply:
                          growth % per week, a down week every N weeks, and
                          how deep the down week cuts (%). Each number is
                          named, so their order does not matter.
    b=Name:d;d;d;d;d;d;d  a BLOCK: one reusable week, exactly 7 days,
                          Monday first. Repeat b= once per block.
    w=Base,Peak*110,Peak*75!    the WEEKS, in order
    u=km                  km or mi. ALWAYS write it, even for km, and write
                          it LAST: see the rule about how the link ends.

Only `b=` and `w=` are required *by the parser*. **Write `u=` anyway.** A
missing one reads as km, so a link without it opens perfectly well, and that
is the whole trap: with no `u=` the URL ends on `w=`, and a `w=` ending in a
locked week ends in `!`. The rule about how the link ends is what happens
next, and it happens silently. A missing `s=` starts the plan this Monday, a
missing `n=` leaves the plan unnamed.

### Weeks

Each entry in `w=` names a block, then optionally:

    *110   run that block at 110% this week. 50-250, and 100 if left off.
    !      lock the week, so re-applying the rule leaves it alone

The percentage is how a plan is built. `Base,Base*110,Base*120,Base*85!` is
four weeks off one block: three steps up and a down week the rule will not
touch.

### Days

A day is a type, then its distance, then an optional `:title`.

    rest
    easy10
    quality14:6+x+1km+@+threshold
    long24:Long+run
    race42.2:Berlin+Marathon

Types are `rest`, `easy`, `quality`, `long`, `cross` (the app shows it as
Strength), `mobility` and `race`. Distance is in the plan's unit, numbers
only — no "10k", no ranges, and leave it off entirely for rest, strength and
mobility days.

`quality` covers tempo, intervals, threshold and track work. `race` is the
goal race and any tune-up race.


## Rules for writing one

- **No spaces anywhere in the URL.** A space inside a name or title is `+`.
  A link with a raw space in it is a broken link in most chat windows.
- **End the link on the `u=` segment, so the last characters are `km` or
  `mi`.** A chat window turns a bare URL into a link, and every markdown
  linkifier drops a trailing `!` `"` `'` `)` `*` `,` `.` `:` `;` `?` `_` or
  `~` on the grounds that it is the sentence's punctuation and not the URL's.
  A locked week ends in `!`, and the weeks you are told to lock below are the
  taper and race week, which are last. So a link that ends in `!` arrives at
  the reader with its final lock missing. It still opens, it is just no longer
  the plan you wrote, and nothing anywhere reports it. So write `u=` even when
  the plan is in km and leaving it out would have meant km anyway: it is
  optional to the parser and mandatory to you, because it is the thing that
  stops the URL ending on `w=`.
- **Exactly 7 days in every block**, Monday through Sunday, `;` between them.
- **Every name in `w=` must match a `b=` name exactly**, and every block name
  must be different from the others.
- **A race is a fixed distance that nothing scales.** Neither the week's
  percentage nor the reader's own scaling of the plan touches it, so put the
  true distance on it: a 10K is `race10` in every week it appears.
- **Build volume with percentages, not with a new block per week.** Two to
  four blocks and a week list is the entire point of the tool. A plan with
  sixteen blocks for sixteen weeks is a plan nobody can edit.
- **Lock (`!`) the weeks that are deliberate**: down weeks, the taper, race
  week. The reader can re-apply their own progression rule at any time, and
  those are the weeks it must skip.
- **Anything you want to say goes before the link.** Explain the plan: why
  that week is a down week, what the quality session is for, what to do if a
  week goes badly. That is the part a runner actually reads, and a bare URL is
  not a training plan. The rule below is about the link's shape, not about
  your voice.
- **End the reply with the URL, unbroken, on a line of its own, with nothing
  after it.** Do not shorten it, wrap it, or put it inside anything that could
  swallow a character. Re-read it before sending: 7 days per block, and every
  week naming a block that exists.
- A plan you write is a draft, not coaching. Say so, in the words before the
  link.


## If any of this is in what you are about to send, stop

Three different models have invented a format here rather than using this one,
and each invention had a tell you can check your own output against:

- a segment named `block0`, `week0`, `start`, `weeks`, `progression` — **the
  only segment names are `n` `s` `u` `r` `b` `w`**, and `b` repeats
- `|` between the days of a block — the separator is `;`
- `%20` between the words of a title — a space is `+`
- a run of random-looking letters — that is the compressed `#p=` codec, which
  you cannot produce
- a URL whose last character is `!` — put the `u=` segment last and it cannot
  happen. The `!` will be eaten by whatever renders your reply as a link
- `r=8,4,25`, three bare numbers — write `r=grow8,every4,cut25`. The bare form
  still reads, which is the problem: put 8 where 4 belongs and you have said
  "grow 4% a week, down week every 8 weeks", both of which are ordinary
  numbers, so nothing rejects it and the reader sees the wrong rule
- one block per session, or one block per week — **a block is a whole week of
  seven days**, reused across many weeks and tuned by a percentage. One model
  wrote thirty-four blocks for a twelve-week plan. Two to four is the point.

A plan that needs a new block every week is a plan the reader cannot edit, and
it means the model above was not the one being used.


## Worked example — eight weeks to a 10K, explained

    https://blokk.run/#plan=n=Road+to+10K~s=2026-09-14~r=grow8,every4,cut25~b=Base:rest;easy5:Easy+run;cross;easy5:Easy+run;rest;easy4:Easy+run;long8:Long+run~b=Sharpen:rest;easy5:Easy+run;quality6:5+x+800m+@+10K+pace;easy5:Easy+run;rest;easy4:Easy+run;long10:Long+run~b=Race+week:rest;easy4:Easy+run;rest;easy3:Shakeout;rest;rest;race10:10K+race~w=Base,Base*110,Base*120,Base*85!,Sharpen,Sharpen*110,Sharpen*120,Race+week!~u=km

Three blocks describe eight weeks. Base builds, week four is a locked down
week at 85%, Sharpen adds the quality session, and race week is its own block
because its shape is different rather than because its volume is.


## Four more, complete

These four are the plans blokk.run ships, written out in the format —
so they are not illustrations of it, they are the thing itself, and the app
reads every one of them back. Copy the shape, not the distances.

All four are in km; `u=mi` means the same numbers are read as miles. The start
dates are only examples — use the Monday the plan you are writing begins.

Where you see %C3%97 or %E2%80%93 in these, that is a × or an – inside a title,
percent-encoded. Titles written in plain ASCII need none of that: only a space
(which is +) and the delimiters + ~ ; : , * ! = % ever have to be escaped.

### My first 5K — 8 weeks

From nothing to running 5K, on three days a week.
3 blocks describe 8 weeks: 101 km in total, peaking at 17.5 km.

    https://blokk.run/#plan=n=My+first+5K~s=2026-09-14~r=grow8,every4,cut20~b=Run%E2%80%93walk:rest;easy3:Run+2+min+%2F+walk+2+min+%C3%97+8;cross;easy3:Run+2+min+%2F+walk+2+min+%C3%97+8;rest;rest;easy4:Longer+run%E2%80%93walk~b=Steady+runs:rest;easy4:Easy+run;cross;easy4:Easy+run;rest;rest;long6:Long+run~b=Race+week:rest;easy3:Easy+run;rest;easy2:Shakeout;rest;rest;race5:5K+race~w=Run%E2%80%93walk,Run%E2%80%93walk*115,Run%E2%80%93walk*130,Run%E2%80%93walk*90,Steady+runs,Steady+runs*115,Steady+runs*125,Race+week~u=km

### Road to 10K — 10 weeks

You can already run 5K. This doubles it, with one quality day a week.
3 blocks describe 10 weeks: 306 km in total, peaking at 42.6 km.

    https://blokk.run/#plan=n=Road+to+10K~s=2026-09-14~r=grow10,every4,cut15~b=Base:rest;easy5:Easy+run;cross;easy5:Easy+run;rest;easy4:Easy+run;long10:Long+run~b=Build:rest;easy6:Easy+run;quality7:5+%C3%97+800m+%40+10K+pace;easy5:Easy+run;rest;easy4:Easy+run;long12:Long+run~b=Race+week:rest;easy5:Easy+run;easy4:Easy+%2B+strides;rest;easy3:Shakeout;rest;race10:10K+race~w=Base,Base*110,Base*120,Base*85,Build,Build*110,Build*120,Build*85,Build*125,Race+week~u=km

### Mastering Half Marathon — 12 weeks

A 10K base turned into 21.1 km, with threshold work and a real taper.
5 blocks describe 12 weeks: 542 km in total, peaking at 57.2 km.

    https://blokk.run/#plan=n=Mastering+Half+Marathon~s=2026-09-14~r=grow8,every4,cut20~b=Base:rest;easy8:Easy+run;cross;easy8:Easy+run;rest;easy6:Easy+run;long16:Long+run~b=Build:rest;easy8:Easy+run;quality10:4+%C3%97+1.5km+%40+threshold;easy6:Easy+run;rest;easy4:Easy+run;long18:Long+run~b=Peak:rest;easy8:Easy+run;quality12:3+%C3%97+3km+%40+half+pace;easy8:Easy+run;rest;easy5:Easy+run;long19:Long+run~b=Taper:rest;easy6:Easy+run;quality7:4+%C3%97+1km+%40+half+pace;easy5:Easy+run;rest;easy4:Easy+run;long12:Long+run~b=Race+week:rest;easy6:Easy+run;quality6:3+%C3%97+1km+%40+race+pace;rest;easy4:Shakeout;rest;race21.1:Half+marathon~w=Base,Base*110,Base*115,Build,Build*110,Build*85,Build*120,Peak,Peak*110,Peak*90,Taper,Race+week~u=km

### Conquer a Marathon — 16 weeks

Half-marathon fitness carried to 42.2 km, peaking at a 32 km long run.
5 blocks describe 16 weeks: 978 km in total, peaking at 83.6 km.

    https://blokk.run/#plan=n=Conquer+a+Marathon~s=2026-09-14~r=grow6,every4,cut20~b=Base:rest;easy10:Easy+run;cross;easy10:Easy+run;rest;easy8:Easy+run;long24:Long+run~b=Build:rest;easy10:Easy+run;quality12:5+%C3%97+2km+%40+threshold;easy8:Easy+run;rest;easy8:Easy+run;long26:Long+run~b=Peak:rest;easy12:Easy+run;quality14:3+%C3%97+5km+%40+marathon+pace;easy10:Easy+run;rest;easy8:Easy+run;long32:Long+run~b=Taper:rest;easy8:Easy+run;quality8:4+%C3%97+1km+%40+marathon+pace;easy6:Easy+run;rest;easy5:Easy+run;long18:Long+run~b=Race+week:rest;easy6:Easy+run;easy5:Easy+run;rest;easy4:Shakeout;rest;race42.2:Marathon~w=Base,Base*105,Base*110,Base*85,Build,Build*105,Build*110,Build*85,Build*115,Peak,Peak*105,Peak*85,Peak*110,Taper,Taper*75,Race+week~u=km


## The enumerations, as JSON

    https://blokk.run/schema.json

The session types, the aliases, the percentage range and the limits, read
straight out of the app's own constants. Check your output against it if you
would rather check a list than a sentence.


## Reading a link somebody gives you

The plan is in the text of the URL. **Do not fetch it** — the fragment, the
part after `#`, is never sent to any server, so opening the page gets you the
app and none of the plan. Parse the string you were given.

A link starting `#p=` instead of `#plan=` is **this same format, compressed**:
strip the one-character codec marker after `#p=`, base64url-decode the rest,
inflate it raw-deflate, and what comes out is exactly the `n=…~b=…~w=…~u=km`
string this page describes. It used to inflate to JSON with short keys. It
does not any more, and any instruction telling you to expect JSON there is
out of date.

If you cannot inflate it, you cannot read it, and no amount of staring at the
base64 will help. Ask for the `#plan=` form instead: the person who sent it
can get one from **AI** → *Copy this plan for an AI* on blokk.run.

**Never write one of those.** It needs a real deflate implementation, and a
base64-shaped string produced by imitation rather than by compression decodes
to nothing at all — the app rejects it and the reader gets an empty page. If
what you are about to send is a run of random-looking letters, you are writing
the wrong format. Go back to `#plan=`, which is words you can read back.


## Updating a link somebody gives you

A plan is its link, so changing a plan means changing the string and handing
back the whole URL. A diff is not something the reader can open, and neither
is a description of the edit on its own, so the new link has to be there in
full.

Say what you changed and why, then the link. Somebody asking for week nine to
be easier wants to know what got easier: which sessions moved, whether the
long run came down or the quality session did, and what that costs later in
the plan. Then the link, on a line of its own with nothing after it.

**If you already hold a valid link, you hold a complete example of this
format.** You do not need to fetch this page again to extend one. Read the
segments out of the link you were given and write the new one.

Most changes are the `w=` list alone, and touch no block at all:

- longer or shorter — add or remove entries
- easier or harder — change the percentages
- protect a week from the reader's own rule — add or remove the `!`

Taking a 24-week plan to 52 weeks is one of those: the `b=` segments come
through byte for byte, and only the week list grows.

Change a `b=` segment only when the *shape* of a week changes — a different
session, or the same session on a different day. If you rename a block, rename
it in `w=` too, or every week pointing at it stops resolving.


## If you cannot manage the format

Return this JSON instead and tell the user to paste it into **AI** →
*Paste what it gave you* on blokk.run:

    {
      "name": "string",
      "start": "YYYY-MM-DD (a Monday)",
      "unit": "km" | "mi",
      "blocks": [ { "name": "Build",
                    "days": [ { "type": "easy", "title": "Easy run", "km": 10 } ] } ],
      "weeks":  [ { "block": "Build", "progression": 100, "locked": false } ]
    }

Same rules: seven days per block, block names matched exactly, distances as
plain numbers.

The same page is served as plain text at /llms.txt. Blokk keeps nothing: a plan is its link, and the part after the # is never sent to any server.