@0x3639/intervals-icu
    Preparing search index...

    Workouts

    client.workouts manages the athlete's workout library: creating, updating, deleting and duplicating workout templates stored in folders and plans. It also converts a workout definition to a file format a device or platform can consume (.zwo, .mrc, .erg, .fit).

    Method What it does
    WorkoutService.listWorkouts List workouts in the athlete's library
    WorkoutService.getWorkout Get a specific workout by ID
    WorkoutService.createWorkout Create a new workout
    WorkoutService.createWorkoutsBulk Create multiple workouts at once
    WorkoutService.updateWorkout Update an existing workout
    WorkoutService.deleteWorkout Delete a workout
    WorkoutService.duplicateWorkouts Duplicate workouts
    WorkoutService.convertWorkout Convert a workout definition to .zwo (Zwift), .mrc, .erg or .fit; resolves power targets from the authenticated athlete's settings, per the spec
    WorkoutService.convertWorkoutForAthlete The same conversion for a specific athlete id, e.g. a coached athlete
    WorkoutService.listWorkoutTags Every tag that has been applied to workouts in the athlete's library

    List the athlete's workout library and every tag applied to library workouts, then convert the first convertible workout to Zwift's .zwo format, falling back to a calendar workout event if the library has none.

    const client = new IntervalsClient({ apiKey });

    const localDate = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
    const now = new Date(); // one timestamp for both bounds

    const workouts = await client.workouts.listWorkouts();
    console.log(`${workouts.length} workouts in the library`);
    for (const workout of workouts.slice(0, 5)) {
    console.log(`- ${workout.name ?? workout.id}`);
    }

    const tags = await client.workouts.listWorkoutTags();
    console.log(tags.length > 0 ? `Workout tags: ${tags.join(', ')}` : 'No workout tags');

    // convertWorkout() requires name, description, type and workout_doc; check the runtime
    // shape of each rather than trusting the Workout/Event type, since API data is external.
    type ConvertibleWorkout = Workout & WorkoutConversionInput;
    const isConvertibleWorkout = (w: Workout): w is ConvertibleWorkout =>
    typeof w.name === 'string' &&
    typeof w.description === 'string' &&
    typeof w.type === 'string' &&
    typeof w.workout_doc === 'object' && w.workout_doc !== null && !Array.isArray(w.workout_doc);

    type ConvertibleEvent = Event & WorkoutConversionInput;
    const isConvertibleEvent = (e: Event): e is ConvertibleEvent =>
    typeof e.name === 'string' &&
    typeof e.description === 'string' &&
    typeof e.type === 'string' &&
    typeof e.workout_doc === 'object' && e.workout_doc !== null && !Array.isArray(e.workout_doc);

    let source: WorkoutConversionInput | undefined = workouts.find(isConvertibleWorkout);

    if (!source) {
    const yearAgo = new Date(now);
    yearAgo.setFullYear(yearAgo.getFullYear() - 1);
    const events = await client.events.listEvents({ oldest: localDate(yearAgo), newest: localDate(now), category: ['WORKOUT'] });
    source = events.find(isConvertibleEvent);
    }

    if (!source) {
    console.log('No convertible workout found in the library or on the calendar');
    } else {
    const zwo = await client.workouts.convertWorkout(
    { name: source.name, description: source.description, type: source.type, workout_doc: source.workout_doc },
    '.zwo',
    );
    console.log(zwo.toString('utf8').slice(0, 80));
    }

    Create a workout in the athlete's first library folder, read it back, then delete it.

    CI never runs this example; running it by hand changes the authenticated account. If the create call itself fails after the server committed it, nothing is cleaned up: search the account for Created by the SDK example (or Uploaded by the SDK example) and delete it by hand.

    // maxRetries: 0 — a create that committed before a 5xx would be repeated by a retry, and only the last response's id would be cleaned up.
    const client = new IntervalsClient({ apiKey, maxRetries: 0 });

    const folders = await client.folders.list();
    const folder = folders[0];

    if (!folder || folder.id === undefined) {
    console.log('No folders found (or the first one has no id); create a folder in the library before running this example');
    } else {
    console.log(`Using folder: ${folder.name ?? folder.id}`);

    // A unique marker in the name: if the create fails after the server committed it, this
    // is what to search the account for.
    const marker = `Created by the SDK example ${Date.now()}`;

    const workout = await client.workouts.createWorkout({
    folder_id: folder.id,
    name: marker,
    type: 'Ride',
    description: '- 20m 85%',
    });

    if (typeof workout.id !== 'number') {
    console.error('The API did not return an id for the created workout; it may need deleting by hand.');
    process.exit(1);
    }
    const workoutId = workout.id;
    console.log(`Created workout id=${workoutId}`);

    try {
    const fetched = await client.workouts.getWorkout(workoutId);
    console.log(`Fetched workout description: ${fetched.description}`);
    } finally {
    await client.workouts.deleteWorkout(workoutId);
    console.log(`Deleted workout ${workoutId}`);
    }
    }
    • WorkoutService.convertWorkout's WorkoutConversionInput requires name, description, type and workout_doc; a body missing workout_doc returns HTTP 500. See API behaviour.
    • Whose settings resolve the power targets is not live-verified: the live suite checks that both routes convert, not whose settings apply. The claim that the global route uses the authenticated athlete's settings comes from spec/openapi.json's description of POST /api/v1/download-workout{ext} ("The athlete to use is extracted from the bearer token and used to resolve power targets").
    • See API behaviour for the cross-service list.