twenty-four: AI training plans

The fitness service generates multi-week training plans using Claude AI.

Plans are stored in S3 with a full feedback loop - generate, review, iterate, then sync to Intervals.icu when satisfied.

ICU Calendar view Training Plan Manager

The Problem

Training plans are rigid. They assume:

  • You have perfect availability
  • You never travel
  • You don’t have other commitments
  • You can do exactly what the plan says when it says to do it

Real life doesn’t work that way.

I needed a plan generator that could:

  1. Understand my calendar - Don’t schedule hard workouts when I’m traveling
  2. Respect weekly volume limits - Don’t jump from 20 miles/week to 40 miles/week
  3. Place long workouts intelligently - Move 90+ minute efforts from Wednesday to Saturday
  4. Generate structured workouts - Warmup, main set, cooldown with specific zones
  5. Allow iteration - Let me improve the plan if something doesn’t look right
  6. Work around existing workouts - Import what’s already on my calendar

How It Works

POST /plans - Create a new training plan:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
curl -X POST https://fitness.twenty-four.home/plans \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Spring Half Marathon",
    "race_name": "Seattle Half Marathon",
    "race_date": "2026-03-07",
    "race_distance": 13.1,
    "sport": "Run",
    "start_date": "2026-01-01",
    "import_icu_workouts": true
  }'

Response (202 Accepted):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "success": true,
  "plan_id": "PLAN_Seattle_Half_Marathon_2026-01-01",
  "name": "Spring Half Marathon",
  "method": "week-by-week",
  "generation_status": "generating",
  "weeks_generated": 0,
  "total_weeks": 10,
  "poll_url": "/plans/PLAN_Seattle_Half_Marathon_2026-01-01",
  "message": "Plan generation started. View at /plans/PLAN_Seattle_Half_Marathon_2026-01-01 to see weeks as they're generated."
}

The API returns immediately - generation continues in the background. Open the plan URL to watch weeks appear in real-time.

GET /plans/:id - View the plan in a calendar UI, or add ?format=json for data.

POST /plans/:id/feedback - Regenerate with adjustments:

1
2
3
4
5
curl -X POST https://fitness.twenty-four.home/plans/PLAN_Seattle_Half_Marathon_2026-01-01/feedback \
  -H "Content-Type: application/json" \
  -d '{
    "feedback": "Make the plan less aggressive. Add more easy days in weeks 2-4."
  }'

POST /plans/:id/week/3/feedback - Fix just one week:

1
2
3
4
5
curl -X POST https://fitness.twenty-four.home/plans/PLAN_Seattle_Half_Marathon_2026-01-01/week/3/feedback \
  -H "Content-Type: application/json" \
  -d '{
    "feedback": "Move the long run to Sunday instead of Saturday."
  }'

POST /plans/:id/sync - Push to Intervals.icu when satisfied:

1
2
3
4
5
curl -X POST https://fitness.twenty-four.home/plans/PLAN_Seattle_Half_Marathon_2026-01-01/sync \
  -H "Content-Type: application/json" \
  -d '{
    "folder_name": "Half Marathon Training (v2)"
  }'

Two Generation Methods

The system supports two approaches:

Method Parameter Speed Quality
Week-by-week (default) use_full_plan: false ~1-2 min/week Higher - each week generated with context from previous weeks
Full-plan use_full_plan: true ~30 seconds Good - entire plan generated in one pass

Progressive Rendering

A 10-week plan takes 10-20 minutes to generate with week-by-week mode. Rather than waiting with no feedback, the system now shows weeks as they’re produced.

How it works:

  1. Immediate response - POST returns 202 with generation_status: "generating"
  2. Background processing - Each week generates and saves to S3 independently
  3. Live UI updates - The plan page polls every 2 seconds and displays new weeks
  4. Progress indicator - Animated progress bar shows “Generating week N of M”
1
2
3
┌─────────────────────────────────────────────────────────────┐
│  ⏳ Generating week 3 of 10...  ████████░░░░░░░░░░░░  30%   │
└─────────────────────────────────────────────────────────────┘

Poll for status programmatically:

1
2
3
4
5
6
curl https://fitness.twenty-four.home/plans/PLAN_ID?format=json | jq '{
  generation_status,
  weeks_generated,
  total_weeks,
  workout_count: (.workouts | length)
}'

Response while generating:

1
2
3
4
5
6
{
  "generation_status": "generating",
  "weeks_generated": 3,
  "total_weeks": 10,
  "workout_count": 15
}

Response when complete:

1
2
3
4
5
6
{
  "generation_status": "complete",
  "weeks_generated": 10,
  "total_weeks": 10,
  "workout_count": 45
}

The UI automatically reloads when generation completes or fails. Actions (feedback, sync) are disabled during generation.

Week-by-Week Generation

The default approach generates each week sequentially with full context:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
================================================================================
WEEK 3 of 9
================================================================================

📝 Generating Week 3...
   Phase: Build
   Target load: 450
   Days to race: 49

🔍 Validating Week 3 (attempt 1)...
 Week 3 validation passed

💾 Saving checkpoint after Week 3...
 Checkpoint saved to S3

🚀 Creating 5 workouts in Intervals.icu...
 Created 5 workouts in Intervals.icu

📊 Progress: 33.3% (3/9 weeks)

Each week’s prompt includes:

  • Prior weeks - What was generated before (load, duration, workout types)
  • Current fitness - ATL, CTL, TSB, ramp rate from Intervals.icu
  • Plan progress - Completed load, miles, hours, rest days used
  • Holidays - Fetched from ICU calendar, enforced as rest days
  • Imported workouts - Existing ICU workouts to work around

This context awareness means Week 5 knows exactly what happened in Weeks 1-4 and can make intelligent adjustments.

Two-Pass Validation

Before accepting each week, the system runs validation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Check for violations BEFORE auto-fixing
requiredFixes := DetectRequiredFixes(week, plan)
if len(requiredFixes) > 0 {
    log.Printf("⚠️  Week %d has %d constraint violations, attempting feedback retry...", weekNum, len(requiredFixes))

    // Build feedback and retry with it
    feedback := BuildFeedbackSection(requiredFixes)
    retryWeek, retryErr := GenerateWeeklyPlanWithFeedback(plan, weekNum, feedback)

    if retryErr == nil && len(DetectRequiredFixes(retryWeek, plan)) == 0 {
        log.Printf("✅ Feedback retry produced clean output!")
        week = retryWeek
    }
}

Example violations detected and fed back:

1
2
3
4
5
6
7
8
🔸 bike_weekend: You scheduled a 120min Ride Long on Jan 15 (Wednesday).
   Long bikes (>90min) MUST be on Saturday or Sunday ONLY.

🔸 pool_closure: You scheduled a Technique swim on Jan 17 (Friday).
   The pool is CLOSED on Friday. Put swims on Mon-Thu or Sat-Sun.

🔸 pool_swim_duration: You scheduled a 90-minute pool swim.
   Pool swims are limited to 60 minutes (1 hour lane reservation).

The model gets one chance to fix violations. If that fails, auto-fixes are applied (moving workouts, capping durations).

Sport-Specific Templates

The system uses different prompt templates based on race type:

Running:

  • weekly_prompt_v1_half_marathon.txt - Half marathon plans
  • weekly_prompt_v1_50k.txt - Ultramarathon plans

Triathlon:

  • weekly_prompt_v1_him.txt - Half Ironman (70.3) plans
  • weekly_prompt_v1_im.txt - Full Ironman plans

Each template includes sport-specific rules:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
HALF IRONMAN TRAINING RULES:
1. Race distance: 1.2 mile swim + 56 mile bike + 13.1 mile run
2. Max long bike: 56 miles / 3.5 hours (race distance)
3. Max long run: 13.1 miles / 2 hours (race distance)
4. Max swim: 2200 yards (race distance)
5. Long rides (>2 hours) MUST be on Saturday or Sunday
6. Include brick workouts: bike followed immediately by run
7. Rest days: 1-2 per week (Monday or Friday recommended)
8. Multi-sport balance: ~3 swims, ~3 bikes, ~3-4 runs per week

🚨 CRITICAL POOL CLOSURE RULE 🚨
The pool is CLOSED on Tuesday, Thursday, and Friday.
Swim workouts are ONLY ALLOWED on: Mon, Wed, Sat, Sun

Open water swims are PREFERRED on weekends during May-October.

The validation system enforces these rules and rejects weeks that violate them.

Importing Existing Workouts

Setting import_icu_workouts: true fetches existing calendar entries and includes them in the prompt:

1
2
3
4
5
6
7
8
EXISTING ICU WORKOUTS THIS WEEK (ALREADY SCHEDULED):
- Tuesday (Jan 7): HIIT - Gym HIIT Class (45min)
- Thursday (Jan 9): Strength - Gym Strength (60min)
- Friday (Jan 10): PT - Personal Training (60min)

⚠️  IMPORTANT: These workouts are ALREADY on the calendar. DO NOT replace them.
Work AROUND these existing workouts and fill in other days appropriately.
Count their duration toward your weekly volume planning.

This lets you keep gym classes, personal training, and other commitments while generating endurance training around them.

Holiday Awareness

The system fetches holidays from Intervals.icu and enforces rest:

1
2
3
HOLIDAYS THIS WEEK:
- Spring Break: 2026-03-07 to 2026-03-14
⚠️  NO WORKOUTS during holidays!

If Claude accidentally schedules a workout during a holiday, validation catches it and either:

  1. Asks Claude to regenerate with feedback
  2. Auto-removes the workout

Structured Workouts

Every workout uses Intervals.icu structured format:

1
2
3
4
5
6
7
8
9
Warmup
- 10m Ramp 60-75% pace, Z1-Z2 easy build

Tempo Intervals 3x
- 8m 88-92% pace, Z4 threshold effort, focus on steady breathing
- 3m 70% pace, Z2 active recovery

Cooldown
- 10m 60-70% pace, Z1 easy jog

Key format rules:

  • Steps start with -
  • Capitalize Ramp for progressive efforts
  • Include zone notation (Z1-Z5) alongside percentages
  • Add descriptive section headers (not just “Main Set”)

Gym Plan Generator

Separate from race training, the system also generates repeating gym schedules:

1
2
3
4
5
6
7
curl -X POST https://fitness.twenty-four.home/gym/plan \
  -H "Content-Type: application/json" \
  -d '{
    "plan_name": "Winter Gym 2025-26",
    "start_date": "2025-12-29T00:00:00-08:00",
    "end_date": "2026-03-31T00:00:00-08:00"
  }'

Weekly pattern:

  • Monday: HIIT @ 7:00 AM
  • Tuesday: Strength @ 6:00 AM
  • Thursday: Strength @ 6:00 AM
  • Friday: Personal Training @ 6:00 AM
  • Saturday: HIIT @ 8:00 AM

Automatically skips holidays and creates plan templates in ICU library.

The Feedback Loop

The full workflow:

  1. Create plan - POST to /plans with race details
  2. Review - Open the web UI calendar at /plans/:id
  3. Iterate - Use feedback endpoints:
    • /plans/:id/feedback - regenerate entire plan
    • /plans/:id/week/:num/feedback - regenerate one week
  4. Version history - Each regeneration increments version number
  5. Sync - When satisfied, POST to /plans/:id/sync

Example feedback prompts:

Full plan:

  • “Make the plan less aggressive, reduce weekly mileage by 15%”
  • “Add more recovery weeks, I want a recovery week every 2 weeks instead of 3”
  • “Move all long runs to Sunday instead of Saturday”

Week-specific:

  • “This week has too much intensity, replace the intervals with an easy run”
  • “Add a tempo run on Wednesday”
  • “Make this a recovery week with only easy runs”

Storage & Checkpoints

Plans are stored in S3 with progressive saves after each week. This enables both the live UI updates and failure recovery.

1
2
3
4
5
6
7
// After each week completes, save immediately
plan.WeeksGenerated = weekNum + 1
plan.UpdatedAt = time.Now()
if err := SaveStoredPlan(plan); err != nil {
    log.Printf("⚠️ Failed to save progress after week %d: %v", weekNum+1, err)
}
log.Printf("📊 Progress: %.0f%% (%d/%d weeks) - saved to S3", progress, weekNum+1, totalWeeks)

Generation states:

Status Meaning
generating Plan creation in progress, weeks appearing progressively
complete All weeks generated successfully
failed Generation failed (error message in generation_error field)

If generation fails mid-plan, the partial plan remains in S3. You can view completed weeks and decide whether to delete and retry, or provide feedback to regenerate from that point.

Prompt Templates

Download the Claude prompt templates used for each race type:

These are Go text templates with placeholders (e.g., {{.RaceName}}, {{.WeekNumber}}) that get filled in with context about your race, fitness level, and prior weeks before being sent to Claude.


See also: Part 0: The Platform | Part 1: Building with Claude | Part 2: Calendar Service | Part 3: Gym Service | Part 4: Strava Service | Part 5: Workout Generator | Part 6: AI Recommendations | Part 7: Service Consolidation | Part 8: System Architecture

Co-Authored-By: Claude noreply@anthropic.com