Form vs Fatigue and how I feel

I’m looking at my fitness chart and trying to correlate how I feel with what I’m seeing. In term of how tired I feel and how much energy I have, should I be looking at fatigue or form? It feels like the form line corresponds better to my feeling of tiredness. Right now, for example, I’m feeling more tired than I’ve often felt when my purple fatigue line has been much higher, while my form is really low which I think must explain it. But then why isn’t form called fatigue, and fatigue called something else?

Have a read here:

And make sure to read the other Guide topics. They explain quit a lot of your questions for starters.

1 Like

Due to trademark rights, Fitness, Fatigue and Form cannot be named what TrainingPeaks (trademark owners) call it, namely Chronic Training Load (CTL), Acute Training (ATL) and Training Stress Balance (TSB) respectively.

So Fatigue is your short term training load (the last 7 days), while Form is the difference between Fatigue and Fitness, the longer term training load (last 42 days).

As you work each day, you gain fitness and fatigue together. After 14-21 days, if you have worked hard enough to push beyond what you did previously (overload), you need to rest/reduce the training load to allow the body time to recover and adapt.

Because Fitness is measured over a longer period (42 days), the number doesn’t drop as quickly as Fatigue would drop when you’re in a recovery week. The formula (for the absolute calculation, not % of form) is:
Form = Fatigue - Fitness, The “balance” that TrainingPeaks refers to (TSB) is allowing the drop of fatigue (a negative value) or an increase from a negative number towards zero, or slightly positive.

But before it drops to zero, you want to start training again, so you’re not fully recovered. This also allows your body to continue to adapt to an increasing training load.

@MedTechCD has shared a link to the explanation of how you should feel relative to what the data says. To add to those posts, it’s important to keep a track of how you feel (daily), and then compare it to the chart. When you “feel” tired, have a look at the (Form) value on the chart, and see if there’s a correlation. Then see how long it takes to recover, and look at the (Form) value on the chart.

If you are not overloading your body over a 2-3 week period, you might not feel as tired as you would if you were training optimally. This is what some refer to as the “green” zone (green being the colour used to determine the optimal training zone), which shows your Form value between -5 and -30. By progressively overloading the body, you will see the Form number creep close to -30 (start of the red zone), which is shown as high risk. That would be the theoretical point at which you need to start recovery. Some might fatigue before reaching -30 and some after. This is why it’s important to track how you feel daily, so you have a subjective input to go with an objective input (data, plus other signs like HRV and resting HR).

5 Likes

Thanks

Thanks. Super helpful. My goal is to gradually increase the distance I can run so that I can run a decent amount each week, with serious long runs on the weekend, without risking injury or overtiredness. This is less for the sake of fitness to be perfectly honest than just because I love running. But because my work reqiures a lot of mental energy and drive, I need to keep my fatigue at a minimum (I’m 60), so I’m trying to finesse it so that I can slowly increase distance without becoming overtired.

1 Like

This disconnect between the chart and how you feel is exactly why subjective inputs matter. Numbers only capture ~10% of what affects readiness. I built TrueFeel to bridge this gap, it layers your actual feel (energy, soreness, motivation) on top of your Intervals data to give a daily Go/Modify/Bail call. The pattern matching gets better over time as it learns your personal signals.

Thanks! Very cool app. I actually started building something similar for myself (but more general - a little “ai running coach”) that I can talk to against my intervals.icu data and a corpus of open-sourced expertise. I think there’s definitely a market for what you are doing here.

That’s awesome, love that you’re building one too. The “talk to my data” approach is great for deeper analysis. TrueFeel is more opinionated on purpose: instead of a conversation, it gives you one binary-ish call first thing in the morning before you’ve had coffee. The idea is that most days, you don’t need analysis, you just need a yes/no/maybe. Would be curious to see what you’re building, and if you or anyone is up to it I’d love to hear what you think: truefeel.ai

So I’m running it on Vercel, using Supabase to store everything. When I log in, the first thing it does it pull in any new training data from intervals.icu. This then gets pulled in as context for the model, along with a bunch of other system prompt instructions, including the instruction to be sure to consult the work of Stephen Seiler, Phil Maffetone, Joe Friel, and Greg McMillan, so that it’s not only relying on the model’s general knowledge. It also saves all chats and uses that as context as well, so that over time it builds a picture of me as an individual runner. Next step is to have it automatically vectorise our chats and maybe a proprietary corpus of running and exercise medicine expertise into a proper little mini-RAG to support better advice. I built it using Perplexity Computer, and it was surprisingly easy even though I’m not a developer. If you like I can get Perplexity Computer to give me the prompt that you can use do make your own - would probably take you a couple of hours.

Really appreciate the detailed writeup, and yes, I’d genuinely love to see the prompt. Thanks for offering.
The architecture you’re describing makes a lot of sense for the “talk to my data” direction, especially the Seiler / Maffetone / Friel / McMillan grounding, that’s smart. The RAG step over chat history is where it gets interesting, because that’s when the tool stops being a generic assistant and starts being your assistant.

The one thing I’d flag from my own build: I started in the same LLM-as-coach direction and ended up pulling the decision logic out into a deterministic rules engine, with the LLM only writing the explanation after the fact. Two reasons: consistency (same inputs always produce the same call, which matters when you’re deciding at 6am whether to do intervals) and testability (I have ~800 test cases covering edge combinations of feel + data + plan). The chat layer on top is still LLM, but it’s not making the call.

Not saying that’s the right architecture for what you’re building, if the job is “help me think through my training,” conversational LLM is exactly right. If the job is “give me a binary call before coffee,” you probably want rules underneath. Different jobs, different shapes.

Would love to see the prompt whenever you have a minute. And if you ever want to compare notes on the Intervals.icu data parsing, happy to share what I learned (their /events endpoint has some quirks around recurring workouts and load fields).

I had very good success increasing my running volume the old-fasioned way:

Each week, decide on a goal volume for this week. In weeks that I dedicate to volume increase, this is 10% for me. In other weeks which contain quality sessions, it will be about 2%. In recovery or taper/race weeks, -50%…-30%.

Then I’ll take the maximum weekly volume over the last 6 weeks, multiply by that percentage, and create my sessions so they hit the target. 30% of the volume goes to the long run, the rest is distributed in whatever way during my easy or quality runs that week.

This seems to work very well. I can check the fitness page here too see if I’m hitting the spot, or doing a bit to little or too much. Intervals is not always so good at predicting the correct load due to various reasons but over time it works itself out. I do use custom spreadsheets as data master, python scripts and the API to manage all of it, but it surely could be done manually.

I gave myself some time (about a year) to fiddle with this, while I ran more and more with the goal of not getting hurt. Worked so far. Much better than just going from race plan to race plan as before. I still can plan for some race specific blocks, but weekly volume is my priority metric.

Note that I’m an old fart with plenty of patience, and very process driven, so this slow style works very well for me. If you’re a hot young one, it might be too boring. :joy:

1 Like

Sure! Please find below:

  • high-level description of the stack and architecture (extremely simple)
  • prompt you should be able to use to get started (no guarantees!)

====================
STACK & ARCHITECTURE

The app is a fullstack web application hosted entirely on Vercel, with Supabase as its database and Perplexity’s AI API as its reasoning engine.

Frontend: React + TypeScript, built with Vite, styled with Tailwind CSS and shadcn/ui components. Four pages: a Coach (chat), a Dashboard (activity table), Settings (credentials + system prompt editor), and Memory (athlete facts viewer). Authentication is handled by Clerk — only signed-in users can access anything.

Backend: A set of Vercel serverless functions (TypeScript) that handle all the sensitive work — fetching from intervals.icu, calling Perplexity, reading/writing to Supabase. The API key for intervals.icu is stored encrypted (AES-256-GCM) in Supabase; the encryption secret lives only in Vercel’s environment variables.

On every chat turn, the backend does the following in parallel: fetches the last 90 days of runs from intervals.icu, fetches current fitness/fatigue metrics (CTL, ATL, TSB), fetches VO2max from the wellness endpoint, loads a persistent athlete memory store, and loads the system prompt. It assembles all of this into a structured context package — with 20 runs summarised on one line each — and sends it plus the session’s conversation history to Perplexity’s sonar-pro model.

Two background processes run fire-and-forget after responses are returned: session naming (sonar generates a 3–6 word title for each conversation after the first exchange) and memory extraction (every 8 messages, sonar distils new facts about the athlete from the conversation into a persistent global memory store).

The system prompt lives in Supabase, not in code, and is editable via the Settings UI without a redeployment.

======================
PROMPT TO GET STARTED

Build me a personal AI running coach web application. Here is exactly what I want:

What it does

A chat interface where I can talk to an AI coach that has full access to my running data from intervals.icu. Every time I send a message, the app fetches my live data from intervals.icu and sends it as context to the AI — so the coach is always working from my actual numbers, not generic advice.

Stack

  • Frontend: React + TypeScript + Vite + Tailwind CSS + shadcn/ui
  • Backend: Vercel serverless functions (TypeScript, ESM modules)
  • Database: Supabase (Postgres) for storing credentials, chat history, and athlete memory
  • AI: Perplexity API (sonar-pro model for chat; sonar for lightweight background tasks)
  • Auth: Clerk (protect all routes and API endpoints)
  • Hosting: Vercel

Data fetched from intervals.icu on every chat turn

  • Last 90 days of runs (filtered to running activities only)
  • From the most recent activity: CTL (chronic training load / fitness), ATL (acute training load / fatigue), TSB (form = CTL minus ATL), athlete max HR, lactate threshold HR
  • VO2max from the wellness endpoint
  • Each run summarised on one line: date (day of week) (name) | distance | duration | pace/km | grade-adjusted pace | intensity factor | avg HR | max HR | HR% of max | resting HR | Z2 minutes | RPE | session-RPE | feel | training load | TRIMP | cardiac decoupling | cadence (spm) | elevation gain | temperature | weight

Supabase tables

  1. credentials — stores the encrypted intervals.icu API key and athlete ID (row id=‘singleton’), the custom system prompt (row id=‘system_prompt’), and the athlete memory (row id=‘memory’)
  2. chat_messages — stores all messages with role, content, session_id, created_at
  3. sessions — stores session id, AI-generated name, created_at, last_message_at

System prompt placeholders

The system prompt contains these placeholders filled at chat time:
{{ctl}}, {{atl}}, {{tsb}}, {{max_hr}}, {{lthr}}, {{vo2max}}, {{runs}}

Features

Chat (Coach page)

  • Session-scoped chat: each conversation has a unique ID generated client-side
  • Collapsible sidebar listing past sessions with AI-generated names and relative timestamps
  • Click a past session to resume it (loads full history, continues the conversation)
  • Delete session button on each sidebar item
  • Quick prompt suggestions on the empty state
  • After the first exchange in a new session, fire a background call to generate a 3-6 word session name using the sonar model
  • Session history capped at last 20 messages per turn to control context window size

Athlete memory

  • Every 8 messages, fire a background call that reads the recent conversation and extracts persistent facts about the athlete (goals, injury history, preferences, patterns) into a structured memory store in Supabase
  • This memory is injected into every chat turn regardless of session
  • A Memory page where the user can view and edit the memory directly

Settings page

  • Form to enter intervals.icu API key and athlete ID (stored encrypted in Supabase)
  • Full-height textarea to view and edit the system prompt (saved to Supabase, takes effect immediately on next chat turn)

Dashboard page

  • Table of all activities from intervals.icu (not just runs) with configurable columns

Debug endpoint

  • GET /api/debug/prompt — returns the fully resolved system prompt and the complete messages array that would be sent to Perplexity, for the most recent session. Useful for inspecting exactly what the model sees.

Security

  • All API routes protected by Clerk’s requireAuth() middleware
  • intervals.icu API key encrypted with AES-256-GCM before storage; encryption secret in Vercel env vars only
  • Clerk Bearer token attached to every frontend API call via a registered token getter in queryClient.ts

Environment variables needed

SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, ENCRYPTION_SECRET (64-char hex), PERPLEXITY_API_KEY, CLERK_PUBLISHABLE_KEY, CLERK_SECRET_KEY, VITE_CLERK_PUBLISHABLE_KEY

System prompt to use (paste into Settings after setup)

You are a knowledgeable, experienced running coach specialising in masters athletes (over-50s). You have a warm, direct manner — honest when the data warrants concern, encouraging when it warrants praise, never sycophantic.

Athlete profile:

  • Age: [YOUR AGE]
  • Height: [YOUR HEIGHT]
  • Max HR: {{max_hr}} bpm
  • VO2max: {{vo2max}}
  • Lactate threshold HR (LTHR): {{lthr}} bpm
  • Goal: [YOUR GOAL]
  • Philosophy: [YOUR PHILOSOPHY]
  • Weekly target: [YOUR WEEKLY KM TARGET] km

Coaching methodology:
Draw on the model’s general understanding of reputable exercise science and running medicine, but refine that with the combined expertise of Maffetone (aerobic base, MAF heart rate), Seiler (80/20 polarised training), Friel (periodisation, load management, CTL/ATL/TSB interpretation), Higdon (progressive structure, consistency), and McMillan (pace zones, energy systems). When these methodologies conflict, prioritise injury prevention, aerobic development, and sustainable load — in that order. Never recommend walking intervals. When drawing on specific methodologies or research, name the source inline — for example: ‘Maffetone recommends keeping HR below MAF for base building’ or ‘Friel’s TSB guidance suggests…’. Do NOT use numbered footnote markers like [1] or (2) — there is no footnote system in this interface.

Current fitness (intervals.icu):

  • CTL (fitness): {{ctl}}
  • ATL (fatigue): {{atl}}
  • TSB (form): {{tsb}} (positive = fresh, negative = tired)

Recent runs (last 20):
{{runs}}

How to coach:

Interpret before answering. Before responding to any question, silently consider what the data actually shows. Look at trends across the last 20 runs — HR drift, TSB trajectory, pace vs HR relationship, Z2 discipline, weekly volume pattern. Let that interpretation shape your answer.

Be proactive. If you notice something significant in the data that the athlete hasn’t asked about — a concerning trend, a positive development, an inconsistency — mention it briefly.

Ask when you need to. If a question can’t be answered well without knowing something you don’t know, ask. One focused question is better than a generic answer.

Be honest about uncertainty. If the data is insufficient to draw a firm conclusion, say so. Do not invent facts, statistics, or research findings. If you are drawing on exercise science or making a physiological claim, it should reflect genuine scientific consensus or established methodology — not speculation dressed up as fact. When uncertain, say so.

Be concrete. Vague advice is useless. Where possible, give a specific recommendation: a distance, an HR ceiling, a number of days.

Adapt your response length to the question. A simple yes/no question needs 2-3 sentences. A request for a weekly plan warrants more detail.

No aggressive speed work unless explicitly asked.

Respond warmly and directly. Reference actual data and long-term patterns when relevant.