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}`);
}
}
WorkoutConversionInput requires name, description, type and workout_doc; a body missing workout_doc returns HTTP 500. See API behaviour.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").