API: Create or update workout without using description text syntax?

Hi,

I’m currently integrating the Intervals.icu API in my application and I’m able to create and update calendar workouts via:

PUT /api/v1/athlete/{id}/events/{eventId}

Until now I have been generating workouts by placing the steps inside the description field using the Intervals workout text syntax (e.g. - 10m 60%, 4x - 5m 110%, etc.), which works because Intervals parses the description and creates the workout structure automatically.

However, I would prefer to keep the description clean and define the workout structure directly in JSON (for example using workout_doc.steps).

Something like:

{
  "category": "WORKOUT",
  "start_date_local": "2026-03-10T10:00:00",
  "type": "Ride",
  "name": "Lactate Test",
  "workout_doc": {
    "steps": [
      {
        "duration": 300,
        "power": { "units": "%ftp", "value": 90 }
      }
    ]
  }
}

But it’s not clear to me if this is actually supported.

From what I read in the forum, it seems that the API expects either the workout description syntax or a workout file (zwo, mrc, erg, fit) and that the server then generates the workout structure itself.

So my questions are:

  1. Is it possible to create or update a workout via the API using structured JSON steps instead of the description syntax?
  2. If yes, what is the correct JSON schema for the workout steps?
  3. Or is the only supported way to:
  • send the description text syntax , or
  • upload a ZWO/MRC/ERG workout file ?

My goal is simply to keep the workout description clean for athletes while still defining the structured steps programmatically.

Thanks!

Guess I can answer my question by myself:
I created a FIT file and uploaded. Even here interval API create the workout in the description. So it seems not possible to get a clear description with workout steps.

Did you upload using API or intervals UI?

I tried this:

marcosreno@MBAReno backend % curl -X POST \

“https://intervals.icu/api/v1/athlete/i611360/events/bulk?upsert=true” \

-u “API_KEY:$TOKEN” \

-H “Content-Type: application/json” \

-d '[

{

"category": "WORKOUT",

"start_date_local": "2026-08-17T00:00:00",

"type": "Run",

"name": "TEST 2x1km Pace Range V2",

"description": "- 10m 5:30-6:16/km Pace\\n\\n2x\\n- 1km 3:30-3:50/km Pace\\n- 2m 5:10-6:00/km Pace\\n\\n- 10m 5:30-6:16/km Pace",

"external_id": "limiar-2x1km-pace-002"

}

]’

[{“id”:128751902,“start_date_local”:“2026-08-17T00:00:00”,“icu_training_load”:null,“icu_atl”:null,“icu_ctl”:null,“type”:“Run”,“carbs_used”:null,“ss_p_max”:null,“ss_w_prime”:null,“ss_cp”:null,“calendar_id”:1,“uid”:“01263f45-93f2-42bf-bee2-872fdcede371”,“athlete_id”:“i611360”,“category”:“WORKOUT”,“end_date_local”:“2026-08-18T00:00:00”,“name”:“TEST 2x1km Pace Range V2”,“description”:“- 10m 5:30-6:16/km Pace\n\n2x\n- 1km 3:30-3:50/km Pace\n- 2m 5:10-6:00/km Pace\n\n- 10m 5:30-6:16/km Pace”,“indoor”:null,“color”:null,“moving_time”:1880,“icu_ftp”:null,“w_prime”:null,“p_max”:null,“atl_days”:null,“ctl_days”:null,“updated”:“2026-08-11T22:28:09.608+00:00”,“not_on_fitness_chart”:false,“show_as_note”:false,“show_on_ctl_line”:false,“for_week”:false,“target”:null,“joules”:null,“joules_above_ftp”:null,“workout_doc”:{“steps”:[{“pace”:{“end”:376,“start”:330,“units”:“secs/km”},“duration”:600},{“reps”:2,“text”:“2x”,“steps”:[{“pace”:{“end”:230,“start”:210,“units”:“secs/km”},“distance”:1000,“duration”:220},{“pace”:{“end”:360,“start”:310,“units”:“secs/km”},“duration”:120}],“distance”:2720.430107526882,“duration”:680},{“pace”:{“end”:376,“start”:330,“units”:“secs/km”},“duration”:600}],“locales”:[],“options”:{},“distance”:6134.356606559764,“duration”:1880,“strain_score”:null,“average_watts”:0,“normalized_power”:0,“variability_index”:null,“polarization_index”:null},“push_errors”:null,“athlete_cannot_edit”:false,“hide_from_athlete”:false,“structure_read_only”:false,“created_by_id”:“i611360”,“shared_event_id”:null,“entered”:false,“carbs_per_hour”:null,“sub_type”:null,“distance”:6134.3564,“tags”:null,“attachments”:null,“oauth_client_id”:null,“external_id”:“limiar-2x1km-pace-002”,“load_target”:null,“time_target”:null,“distance_target”:null,“training_availability”:“NORMAL”,“max_training_time”:null,“can_train_sports”:null,“plan_athlete_id”:null,“plan_folder_id”:null,“plan_workout_id”:null,“plan_applied”:null,“icu_intensity”:null,“strain_score”:null,“plan_name”:null}]% marcosreno@MBAReno backend % curl \

“https://intervals.icu/api/v1/athlete/i611360/events/128751902” \

-u “API_KEY:$TOKEN”

{“id”:128751902,“start_date_local”:“2026-08-17T00:00:00”,“icu_training_load”:null,“icu_atl”:null,“icu_ctl”:null,“type”:“Run”,“carbs_used”:null,“ss_p_max”:null,“ss_w_prime”:null,“ss_cp”:null,“calendar_id”:1,“uid”:“01263f45-93f2-42bf-bee2-872fdcede371”,“athlete_id”:“i611360”,“category”:“WORKOUT”,“end_date_local”:“2026-08-18T00:00:00”,“name”:“TEST 2x1km Pace Range V2”,“description”:“- 10m 5:30-6:16/km Pace\n\n2x\n- 1km 3:30-3:50/km Pace\n- 2m 5:10-6:00/km Pace\n\n- 10m 5:30-6:16/km Pace”,“indoor”:null,“color”:null,“moving_time”:1880,“icu_ftp”:null,“w_prime”:null,“p_max”:null,“atl_days”:null,“ctl_days”:null,“updated”:“2026-08-11T22:28:09.608+00:00”,“not_on_fitness_chart”:false,“show_as_note”:false,“show_on_ctl_line”:false,“for_week”:false,“target”:null,“joules”:null,“joules_above_ftp”:null,“workout_doc”:{“steps”:[{“pace”:{“end”:376,“start”:330,“units”:“secs/km”},“duration”:600},{“reps”:2,“text”:“2x”,“steps”:[{“pace”:{“end”:230,“start”:210,“units”:“secs/km”},“distance”:1000,“duration”:220},{“pace”:{“end”:360,“start”:310,“units”:“secs/km”},“duration”:120}],“distance”:2720.430107526882,“duration”:680},{“pace”:{“end”:376,“start”:330,“units”:“secs/km”},“duration”:600}],“locales”:[],“options”:{},“distance”:6134.356606559764,“duration”:1880,“strain_score”:null,“average_watts”:0,“normalized_power”:0,“variability_index”:null,“polarization_index”:null},“push_errors”:null,“athlete_cannot_edit”:false,“hide_from_athlete”:false,“structure_read_only”:false,“created_by_id”:“i611360”,“shared_event_id”:null,“entered”:false,“carbs_per_hour”:null,“sub_type”:null,“distance”:6134.3564,“tags”:null,“attachments”:null,“oauth_client_id”:null,“external_id”:“limiar-2x1km-pace-002”,“load_target”:null,“time_target”:null,“distance_target”:null,“training_availability”:“NORMAL”,“max_training_time”:null,“can_train_sports”:null,“plan_athlete_id”:null,“plan_folder_id”:null,“plan_workout_id”:null,“plan_applied”:null,“icu_intensity”:null,“strain_score”:null,“plan_name”:null}% marcosreno@MBAReno backend %

The workout_doc looks correct. However, when I open the workout on my Garmin, I only see the duration. The pace range is missing. Is the pace field in workout_doc supposed to be converted to a pace target when the workout is pushed to Garmin? The same problem when upload a FIT file.