update-routine
Update an existing workout routine by ID. Modify title, notes, and exercise configurations for your Hevy routines.
Instructions
Update an existing routine by ID. You can modify the title, notes, and exercise configurations. Returns the updated routine with all changes applied.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| routineId | Yes | ||
| title | Yes | ||
| notes | No | ||
| exercises | No |
Implementation Reference
- src/index.ts:76-76 (registration)The top-level MCP server registration call for all routine tools, including 'update-routine'. Calls registerRoutineTools(server, hevyClient).
registerRoutineTools(server, hevyClient); - src/tools/routines.ts:347-425 (handler)The core handler implementation for the 'update-routine' tool. It registers with server.tool(), receives routineId, title, notes, and exercises params, transforms data via hevyClient.updateRoutine(), and returns the updated routine.
server.tool( "update-routine", "Update an existing routine by ID. You can modify the title, notes, and exercise configurations. Returns the updated routine with all changes applied.", updateRoutineSchema, withErrorHandling(async (args: UpdateRoutineParams) => { if (!hevyClient) { throw new Error( "API client not initialized. Please provide HEVY_API_KEY.", ); } const { routineId, title, notes, exercises } = args; let usesRepRanges = false; const data: PutV1RoutinesRoutineid200 = await hevyClient.updateRoutine( routineId, { routine: { title, notes: notes ?? null, exercises: exercises.map((exercise): PutRoutinesRequestExercise => { const sets = exercise.sets.map((set): PutRoutinesRequestSet => { const repRange = buildRepRange(set.repRange); const fixedReps = getFixedRepsFromRepRange(repRange); const reps = typeof set.reps === "number" ? set.reps : fixedReps; return { type: set.type as PutRoutinesRequestSetTypeEnumKey, weight_kg: set.weight ?? set.weightKg ?? null, reps: reps ?? null, distance_meters: set.distance ?? set.distanceMeters ?? null, duration_seconds: set.duration ?? set.durationSeconds ?? null, custom_metric: set.customMetric ?? null, rep_range: repRange, }; }); if ( sets.some( (set) => set.rep_range != null && getFixedRepsFromRepRange(set.rep_range) === null, ) ) { usesRepRanges = true; } return { exercise_template_id: exercise.exerciseTemplateId, superset_id: exercise.supersetId ?? null, rest_seconds: exercise.restSeconds ?? null, notes: exercise.notes ?? null, sets, }; }), }, }, ); if (!data) { return createEmptyResponse( `Failed to update routine with ID ${routineId}`, ); } const routine = formatRoutine(data); const response = createJsonResponse(routine, { pretty: true, indent: 2, }); if (usesRepRanges) { response.content.push({ type: "text", text: repRangeDisplayWarningText, }); } return response; }, "update-routine"), ); - src/tools/routines.ts:312-344 (schema)The input schema (updateRoutineSchema) for 'update-routine', defining routineId (string), title (string), notes (optional string), and exercises (array of objects with exerciseTemplateId, supersetId, restSeconds, notes, sets). Uses Zod for validation.
// Update existing routine const updateRoutineSchema = { routineId: z.string().min(1), title: z.string().min(1), notes: z.string().optional(), exercises: z.preprocess( parseJsonArray, z.array( z.object({ exerciseTemplateId: z.string().min(1), supersetId: z.coerce.number().nullable().optional(), restSeconds: z.coerce.number().int().min(0).optional(), notes: z.string().optional(), sets: z.array( z.object({ type: z .enum(["warmup", "normal", "failure", "dropset"]) .default("normal"), weight: z.coerce.number().optional(), weightKg: z.coerce.number().optional(), reps: zNullableInt, distance: z.coerce.number().int().optional(), distanceMeters: z.coerce.number().int().optional(), duration: z.coerce.number().int().optional(), durationSeconds: z.coerce.number().int().optional(), customMetric: z.coerce.number().optional(), repRange: zOptionalRepRange, }), ), }), ), ), } as const; - src/tools/routines.ts:31-59 (helper)CoerceNullishNumberInput helper used to preprocess nullable number fields in the schema.
function coerceNullishNumberInput(value: unknown): unknown { if (value === null || value === undefined) { return value; } if (typeof value !== "string") { return value; } const trimmed = value.trim(); if (trimmed === "") { return undefined; } const lowered = trimmed.toLowerCase(); if (lowered === "null") { return null; } if (lowered === "undefined") { return undefined; } const asNumber = Number(trimmed); if (Number.isNaN(asNumber)) { return value; } return asNumber; } - src/tools/routines.ts:76-120 (helper)Helper functions buildRepRange and getFixedRepsFromRepRange used in the update-routine handler to process rep range data on sets.
function buildRepRange(repRange?: { start?: number | null; end?: number | null; }): { start?: number; end?: number } | null { if (!repRange) { return null; } const start = repRange.start ?? undefined; const end = repRange.end ?? undefined; if (start === undefined && end === undefined) { return null; } return { start, end }; } /** * Returns a fixed rep count when `repRange` is a fixed range (start and end are * both non-null and equal). Otherwise returns null. */ function getFixedRepsFromRepRange( repRange: | { start?: number | null; end?: number | null; } | null | undefined, ): number | null { if (!repRange) { return null; } const start = repRange.start ?? null; const end = repRange.end ?? null; if (start === null || end === null) { return null; } if (start !== end) { return null; } return start; }