Translated from the original published on Habr on 2026-09-14 — original

LLM is good at the form of a training plan: it distributes sessions across days, describes intensity, explains exercises. But coherent text does not yet mean a coherent methodology. The model by itself does not store the athlete's current context and does not ensure continuity between the season goal and tomorrow's workout. At 1trAIner we solve this task through a cascade of horizons, explicit context, and constraints on top of generation. Below is how this approach works, its safeguards, and its limits.

Why a single request doesn't work

The request "make me a training plan" is not specific enough. It doesn't make clear what the person did yesterday, how much time they have tomorrow, and how the coming week relates to a longer-term goal. The model can fill these gaps with plausible assumptions. The result will be a neat answer, but the choice of load will be random relative to the athlete's actual situation.

The problem is not solved by the instruction "be an experienced coach." Such an instruction sets a role but does not convey history. Even a detailed profile does not replace recent workouts and recovery metrics. The goal may stay the same, while the basis for tomorrow's session changes.

There is also a difference in scale. At the season level, an overall training concept is needed. At the day level, a specific session compatible with that concept, the current condition, and the schedule. If each horizon is generated independently, there is no mandatory link between them: the weekly plan may look reasonable on its own but not continue the monthly one.

So the question for the architecture is not "how to write the most detailed prompt" but "what decisions must exist before this request." Cascading workout planning with LLM is a way to explicitly set such dependencies. It does not make the model error-free, but it does not leave the order of building the plan to its discretion.

Cascade of horizons

The service uses a chain:

season → month → week → day

The lower horizon is built on top of the upper one. The daily plan should not appear as an independent essay about a suitable workout. Its foundation is the weekly plan, which in turn has monthly and seasonal context.

If the required higher-level horizon is missing, the service builds it first. It is important to distinguish this from the instruction to the model to "take the season into account": you can only take into account what is actually passed in. A missing plan is a task for orchestration, not a suggestion for the model to guess at the content.

There is an additional check: the plan text must contain at least 300 characters. The placeholder "the planner returned no text" does not count as a plan and does not become the basis for the next horizon.

Below is an illustration of choosing the missing support, not a copy of the service's source code. The context here is conventionally represented by an object with plans by horizon:

// Which horizon needs to be built before the current one (null — nothing needed).
// We look exactly one step up: the month is fixed by weekly generation.
const MIN_PLAN_TEXT_LENGTH = 300;

function planUsable(plan) {
  if (!plan || !plan.id) return false;
  return String(plan.markdown_text || '').trim().length >= MIN_PLAN_TEXT_LENGTH;
}

function missingParentScope(scope, ctx = {}) {
  if (scope === 'day') return planUsable(ctx.weekly_plan) ? null : 'week';
  if (scope === 'week') return planUsable(ctx.monthly_plan) ? null : 'month';
  return null;
}

The function is short, but it closes a whole class of errors: before it, the daily plan could

be built on top of an empty weekly plan and honestly write about it right in the text

of the task. The user received a plan in which, instead of a workout, there was

an explanation of why it was missing.

The daily plan is assembled in advance, at night. This is not an optimization for the sake of beauty: by

the morning delivery, the text must already exist, otherwise the person opens the chat and

waits for generation.

What we put into the context

The second half of the task is what exactly the model sees at the moment of generation. Here it is easy

go to extremes: either give away everything and pay for tokens that have no

effect, or save money and get a plan disconnected from reality.

Context is assembled anew for each request — it costs more than writing

the profile into the system prompt once, but that's the only way the model responds about today, not about

an abstract athlete.

  • Recent workouts with dates and a "today / yesterday" marker. Without explicit dates

the model confuses the order and reviews the day-before-yesterday's session as a fresh one.

  • Auto-review of recent activities from Garmin data: heart rate zones, splits,

weather. This is what turns "ran 10 km" into "ran 10 km in the second

zone with a positive split at +24 °C".

  • Recovery metrics: sleep, resting heart rate, heart rate variability,

readiness, training load. The decision about tomorrow's

intensity.

  • Training metrics: threshold heart rate and zones, predictions for distances.

Intensity is tied to them, not to generic tables from the internet.

  • Nutrition diary for the week. It serves exactly one purpose: to warn about

underfueling before a high-volume day. The service does not prescribe diets.

  • Constraints from the dashboard: blocked dates, day-by-day availability,

minute limit. These are hard constraints, not preferences.

  • Athlete profile, goals, and memory — things that rarely change.

How metrics shape tomorrow

The rule is simple and stated explicitly: with low readiness, reduced heart rate

variability, short sleep, or high acute training load, intensity is reduced; with

high readiness, quality work is allowed. There is no rigid template of "three weeks

up, one down” does not exist in the planner — the load increases in steps, and the moment

recovery is determined by training load, readiness dynamics and quality

of completed workouts.

Precision of wording matters here: these are rules on top of the model, not its

own decision. The model phrases the workout in words, but the framework “today

quality is possible / today only easy” is not set by it.

Validators: what we check before a person sees the plan

Every check here is a response to a specific incident, not theoretical

caution. The general rule is this: if a model error can reach a person

silently, a check is needed to make it loud.

Several checks stand between the model’s response and the user.

  • Suitability of the reference. That same 300-character threshold: a short response is considered

a generation failure, not a plan.

  • Building out the horizon. If there is no weekly plan, it is built down to the daily one.
  • Hard constraints. Blocked dates and the minute limit are non-negotiable: on

a day marked as unavailable, no workout is scheduled, even if according to

the methodology one is needed there.

  • Medical boundary. Pain, injury, symptoms of illness are a reason to refer to

a doctor, not to pick a load. The service is informational and does not make diagnoses.

// Whether the plan is usable as a foundation for the lower horizon.
const usable = planUsable(ctx.weekly_plan);
if (!usable) {
  await generatePlanForScope('week', ctx);   // first build out the week
}
await generatePlanForScope('day', ctx);     // and only then release the day

Request cost

An architecture with rich context hits money faster than quality:

one coach response is several thousand input tokens. That is why the schedule

of background tasks is dictated by the provider's pricing, not by developer convenience.

Context for each request costs money, so the price should be calculated in advance. As of

10.09.2026 the model rates are as follows: input — $0.15 per million tokens, cached

input — $0.003 per million, output — $0.60 per million. During peak hours they are twice

higher: $0.30, $0.006 and $1.20 respectively.

Peak according to the provider's price list is 01:00–04:00 and 06:00–10:00 UTC on weekdays; weekends are not

counted as peak. Hence the nightly build of daily plans at 03:15–03:59 Moscow time:

in UTC that is off-peak, and the same work costs half as much. Cached input

is fifty times cheaper than regular, so it pays to keep the unchanging part of the context

stable from request to request.

What didn't work the first time

Three stories, each cost a separate fix.

The first — a plan on top of emptiness. The weekly generation was marked as started, no plan

appeared, and the nightly build of the day constructed a task on top of an empty weekly

plan and wrote about it right in the text. It's fixed by checking the foundation on which

the article begins.

The second — placeholders instead of text. The answer "the planner returned no text" formally

counts as a plan: there is an entry, there is a horizon, there is a date. Hence the length threshold.

The third — voice recognition. Voice messages are transcribed by our own

service built on faster-whisper. Without a domain hint, the model confidently turned

"Lycian Way" into "Lekistian", and Body Battery into whatever came out. A list of

terms and proper names in the recognizer's hint helped.

What's next

The immediate work is less about the model and more about the checks around it:

more automated tests for the plan structure and for compliance with the constraints from

the dashboard. We won't name dates: what already works is described on the page

"How the coach makes decisions", and you can try

you can access the service from the home page or from the page

AI running coach.

Source: Habr