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.
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 calendarRead 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.
For anyone already running auto-sync. Uses the same ATHLETE_ID and INTERVALS_KEY secrets — zero new credential setup.
- Copy
push.pyto your data repo root (next tosync.py) - Copy
push-workout.ymlto.github/workflows/push-workout.yml - 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.
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 --confirmCredentials loaded from (first match wins):
- CLI args:
--athlete-id,--api-key .sync_config.jsonin working directory (same file sync.py uses)- 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 listTo 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"
})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 --confirmOutput (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"}]}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 onlyRead-only — no --confirm needed. Supports +N syntax for relative dates.
python push.py move --event-id 33375903 --date 2026-03-06 # preview
python push.py move --event-id 33375903 --date 2026-03-06 --confirm # executePreview shows old date → new date.
python push.py delete --event-id 33375903 # preview (shows what would be deleted)
python push.py delete --event-id 33375903 --confirm # execute# 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 --confirmAccepts 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.
Completed activities — prepends NOTE: line to activity description (default):
python push.py annotate --activity-id i12345:abc123 --message "Cut short - knee pain" --confirmTo 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 --confirmPlanned workouts — prepends a NOTE: line to the workout description:
python push.py annotate --event-id 33375903 --message "Focus on cadence >90rpm" --confirmThis 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.
| 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
The description field uses Intervals.icu's native workout builder syntax. When provided, Intervals.icu parses the description into structured workout steps server-side.
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)
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).
- Steps start with
- - Blank lines required around repeat blocks
- Text before duration becomes step cue
- Case-insensitive keywords
- Nested repeats NOT supported
- 15m ramp 50%-75%
3x
- 15m 88-92%
- 5m 55%
- 10m 50%
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% |
- Don't use absolute watts — use
%FTPranges so workouts stay correct if FTP changes - Don't use
mfor meters —mmeans minutes. Usekm,mi, ormtrfor 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
403 Access denied—ATHLETE_IDis missing the leadingi(e.g.113739instead ofi113739), the API key is wrong, or the key doesn't belong to that athlete- Error saying
requestsis not installed — push.py is running under a Python interpreter that doesn't haverequestsinstalled; runpip install requestsin the same env assync.py, or invoke that env's Python explicitly - Command appears to have done nothing — preview is the default for all write operations. Add
--confirmto actually write
| 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 |