Skip to content

Latest commit

 

History

History
328 lines (240 loc) · 12 KB

File metadata and controls

328 lines (240 loc) · 12 KB

Agentic Workout Push

Write, read, move, delete, and annotate planned workouts on an athlete's Intervals.icu calendar. Update sport-specific thresholds. For AI platforms that can execute code or trigger GitHub Actions (OpenClaw, Claude Code, Cowork, ChatGPT Codex, etc.). Chat-only users cannot use this.

Safety: Preview by Default

All write operations (push, move, delete, set-threshold, annotate) default to preview mode. Nothing is written unless you add --confirm.

Agent rule: Always run without --confirm first. Show the preview to the athlete. Add --confirm only after the athlete approves.

# Step 1: Agent runs preview
python push.py push --json week.json
# → Returns preview JSON showing what WOULD be pushed

# Step 2: Agent shows athlete the summary

# Step 3: Athlete says "looks good" / "go" / "yes" / etc.

# Step 4: Agent runs with --confirm
python push.py push --json week.json --confirm
# → Actually writes to calendar

Read operations (list) have no safety gate — they never modify anything.

Scope: All write operations (push, move, delete) operate on planned events only — future calendar items. Completed activities and training history cannot be modified or deleted through push.py.

Setup

Path 1: GitHub Actions dispatch (recommended)

For anyone already running auto-sync. Uses the same ATHLETE_ID and INTERVALS_KEY secrets — zero new credential setup.

  1. Copy push.py to your data repo root (next to sync.py)
  2. Copy push-workout.yml to .github/workflows/push-workout.yml
  3. Done. Your agent triggers the workflow via GitHub API dispatch.

How the agent triggers it (Python):

import requests, json

workouts = [
    {
        "name": "Sweet Spot 3x15",
        "date": "2026-03-10",
        "type": "Ride",
        "description": "- 15m 55%\n\n3x\n- 15m 88-92%\n- 5m 55%\n\n- 10m 50%",
        "duration_minutes": 85,
        "tss": 75,
        "target": "POWER"
    }
]

requests.post(
    f"https://api.github.com/repos/{owner}/{repo}/actions/workflows/push-workout.yml/dispatches",
    headers={
        "Authorization": f"Bearer {github_token}",
        "Accept": "application/vnd.github+json",
    },
    json={
        "ref": "main",
        "inputs": {
            "command": "push",
            "workouts": json.dumps(workouts),
            "confirm": "true"
        }
    }
)

The agent needs a GITHUB_TOKEN with actions:write scope on the data repo.

Path 2: Local execution (Claude Code, Cowork, ChatGPT Codex App, json-manual users)

For agents running locally with direct filesystem access.

push.py depends on requests. Install it into the same Python environment you use for sync.py: pip install requests.

# Single workout (preview)
python push.py push --name "Sweet Spot 3x15" --date 2026-03-10 --type Ride \
  --description "- 15m 55%\n\n3x\n- 15m 88-92%\n- 5m 55%\n\n- 10m 50%" \
  --duration 85 --tss 75

# Batch from JSON file (execute)
python push.py push --json week.json --confirm

Credentials loaded from (first match wins):

  1. CLI args: --athlete-id, --api-key
  2. .sync_config.json in working directory (same file sync.py uses)
  3. Environment: ATHLETE_ID, INTERVALS_KEY

ATHLETE_ID must include the leading i — e.g. i113739, not 113739. INTERVALS_KEY is the raw API key string from Intervals.icu → Settings → Developer.

# CLI flags
python push.py list --athlete-id i123456 --api-key abc123...

# Env vars
export ATHLETE_ID=i123456
export INTERVALS_KEY=abc123...
python push.py list

# .sync_config.json (next to push.py)
# {"athlete_id": "i123456", "intervals_key": "abc123..."}
python push.py list

To keep your local JSON data fresh automatically (so push.py and your agent always have current data), see json-local-sync.

Python import also works:

from push import IntervalsPush
pusher = IntervalsPush(athlete_id, api_key)
result = pusher.push_workout({
    "name": "Sweet Spot 3x15",
    "date": "2026-03-10",
    "type": "Ride",
    "description": "- 15m 55%\n\n3x\n- 15m 88-92%\n- 5m 55%\n\n- 10m 50%",
    "duration_minutes": 85,
    "tss": 75,
    "target": "POWER"
})

Commands

push — Add workouts to calendar

python push.py push --json week.json                    # preview
python push.py push --json week.json --confirm           # execute
python push.py push --name "Endurance" --date 2026-03-10 --type Ride --confirm

Output (preview):

{"success": true, "mode": "preview", "count": 2, "summary": [...], "message": "Preview only - add --confirm to write to calendar"}

Output (execute):

{"success": true, "count": 2, "events": [{"id": 33375903, "name": "Sweet Spot 3x15", "date": "2026-03-10", "type": "Ride", "category": "WORKOUT"}]}

list — Show planned workouts

python push.py list                          # this week (today → +6 days)
python push.py list --newest +13             # next two weeks
python push.py list --oldest 2026-03-01 --newest 2026-03-31  # March
python push.py list --category RACE_A        # races only

Read-only — no --confirm needed. Supports +N syntax for relative dates.

move — Move a workout to a different date

python push.py move --event-id 33375903 --date 2026-03-06          # preview
python push.py move --event-id 33375903 --date 2026-03-06 --confirm # execute

Preview shows old date → new date.

delete — Remove a workout

python push.py delete --event-id 33375903              # preview (shows what would be deleted)
python push.py delete --event-id 33375903 --confirm    # execute

set-threshold — Update sport-specific thresholds

# Preview (shows current → new values)
python push.py set-threshold --sport cycling --ftp 295

# Execute after athlete confirms
python push.py set-threshold --sport cycling --ftp 295 --indoor-ftp 283 --confirm

# Other sports
python push.py set-threshold --sport run --lthr 172 --max-hr 192 --confirm
python push.py set-threshold --sport swim --threshold-pace 0.82 --confirm

Accepts sport families (cycling, run, swim, walk, ski, rowing) or Intervals.icu activity types (Ride, Run, Swim, etc.). Only sends fields you provide — omitted fields stay unchanged.

Agent safety: Only update thresholds after a validated test result (FTP test, LTHR test, max HR test). Never update from a single ride estimate or eFTP. Always preview first to show the athlete the old → new diff.

annotate — Add notes to activities or planned workouts

Completed activities — prepends NOTE: line to activity description (default):

python push.py annotate --activity-id i12345:abc123 --message "Cut short - knee pain" --confirm

To post to the activity's chat/messages panel instead, add --chat:

python push.py annotate --activity-id i12345:abc123 --message "Cut short - knee pain" --chat --confirm

Planned workouts — prepends a NOTE: line to the workout description:

python push.py annotate --event-id 33375903 --message "Focus on cadence >90rpm" --confirm

This shows up in Intervals.icu as:

NOTE: Focus on cadence >90rpm

- 15m 55%

3x
- 15m 88-92%
- 5m 55%

- 10m 50%

Provide --activity-id OR --event-id, not both.

Workout Fields

Field Required Type Description
name Yes string Workout name
date Yes string YYYY-MM-DD, must be today or future
type No string Activity type (default: Ride)
description No string Intervals.icu workout syntax (see below)
duration_minutes No number Planned duration (max 720)
tss No number Planned TSS (max 500)
target No string POWER, HR, or PACE
category No string WORKOUT (default), RACE_A, RACE_B, RACE_C, NOTE
indoor No boolean Mark as indoor
external_id No string For upsert deduplication

Valid activity types: Ride, VirtualRide, MountainBikeRide, GravelRide, EBikeRide, Run, VirtualRun, TrailRun, Swim, NordicSki, VirtualSki, Rowing, WeightTraining, Walk, Hike, Workout, Other

Intervals.icu Workout Syntax

The description field uses Intervals.icu's native workout builder syntax. When provided, Intervals.icu parses the description into structured workout steps server-side.

Targets

Power: 75%, 220w, Z2, 95-105%, MMP30s, MMP5m HR: 70% HR, 95% LTHR, Z2 HR Pace: 60% Pace, Z2 Pace, 5:00/km Pace Cadence: append 90rpm to any step Ramps: ramp 50%-75% Freeride: step with no target (ERG off)

Duration

5m = 5 minutes, 30s = 30 seconds, 1h2m30s = 1 hour 2 min 30 sec

CRITICAL: m means minutes, not meters. For distance use km, mi, or mtr (e.g., 500mtr, 2km, 1mi).

Structure

  • Steps start with -
  • Blank lines required around repeat blocks
  • Text before duration becomes step cue
  • Case-insensitive keywords
  • Nested repeats NOT supported

Example

- 15m ramp 50%-75%

3x
- 15m 88-92%
- 5m 55%

- 10m 50%

Template Mappings

Maps Section 11 Workout Reference template IDs to Intervals.icu description syntax. Use these as inspiration and adapt to the athlete — don't copy-paste templates without considering current fitness, goals, and constraints. Always use %FTP ranges, not absolute watts — Intervals.icu resolves % to the athlete's current FTP.

Template ID Name Description Syntax
AE-1 Recovery Spin - 5m ramp 40%-50%\n- 30m 50-55%\n- 5m ramp 50%-40%
AE-2 Medium Endurance - 10m ramp 50%-65%\n- 90m 65-75%\n- 10m ramp 65%-50%
AE-3 Long Endurance - 15m ramp 50%-65%\n- 150m 65-75%\n- 15m ramp 65%-50%
SS-1 Sweet Spot 3x15 - 15m ramp 50%-75%\n\n3x\n- 15m 88-92%\n- 5m 55%\n\n- 10m 50%
SS-2 Sweet Spot 2x20 - 15m ramp 50%-75%\n\n2x\n- 20m 88-92%\n- 5m 55%\n\n- 10m 50%
SS-3 Sweet Spot 2x30 - 15m ramp 50%-75%\n\n2x\n- 30m 88-92%\n- 5m 55%\n\n- 10m 50%
TH-1 Threshold 3x10 - 15m ramp 50%-75%\n\n3x\n- 10m 95-105%\n- 5m 55%\n\n- 10m 50%
TH-2 Threshold 2x15 - 15m ramp 50%-75%\n\n2x\n- 15m 95-105%\n- 5m 55%\n\n- 10m 50%
TH-3 Threshold 2x20 - 15m ramp 50%-75%\n\n2x\n- 20m 95-105%\n- 5m 55%\n\n- 10m 50%
VO2-1 VO2max 5x4 - 15m ramp 50%-75%\n\n5x\n- 4m 106-120%\n- 3m Z1\n\n- 10m 50%
VO2-2 VO2max 4x5 - 15m ramp 50%-75%\n\n4x\n- 5m 106-120%\n- 4m Z1\n\n- 10m 50%
VO2-3 VO2max 6x3 - 15m ramp 50%-75%\n\n6x\n- 3m 106-120%\n- 3m Z1\n\n- 10m 50%
AN-1 Anaerobic 8x30s - 15m ramp 50%-75%\n\n8x\n- 30s 150%\n- 4m30s Z1\n\n- 10m 50%
AN-2 Anaerobic 10x1m - 15m ramp 50%-75%\n\n10x\n- 1m 130-150%\n- 3m Z1\n\n- 10m 50%

What NOT To Do

  • Don't use absolute watts — use %FTP ranges so workouts stay correct if FTP changes
  • Don't use m for metersm means minutes. Use km, mi, or mtr for distance
  • Don't nest repeats — Intervals.icu doesn't support it
  • Don't push past dates — validation rejects them. Planned workouts are future events
  • Don't skip blank lines around repeat blocks — parsing breaks without them
  • Don't update thresholds from estimates — only from validated test results

Troubleshooting

  • 403 Access deniedATHLETE_ID is missing the leading i (e.g. 113739 instead of i113739), the API key is wrong, or the key doesn't belong to that athlete
  • Error saying requests is not installed — push.py is running under a Python interpreter that doesn't have requests installed; run pip install requests in the same env as sync.py, or invoke that env's Python explicitly
  • Command appears to have done nothing — preview is the default for all write operations. Add --confirm to actually write

Files

File Goes to Description
push.py Data repo root (next to sync.py) Validates and manages workouts + thresholds
push-workout.yml .github/workflows/push-workout.yml GitHub Actions workflow for dispatch
README.md Reference only (stays in section-11) This file