---
title: "Update a schedule"
description: "Change an on-call schedule's name, timezone, validity range, weekly windows, rotation cadence and rotation membership — in one atomic call."
---

`PATCH /api/im/schedules/:id`

This is where a schedule's rotation actually gets configured. [Creating a schedule](./schedules) gives you an empty shell (`rotationMode: "none"`, no members); this endpoint sets the cadence, the weekly windows and who is in the rotation.

Every field is **optional**. Only the fields present in the body are touched — a request body of `{ "name": "Primary" }` renames the schedule and changes nothing else.

## Authentication

Requires the base IM access every endpoint in this API needs, plus the write bar: your role must be `admin`, or you must be a team admin of the schedule's team. An organization-wide API token is treated as an organization admin.

<Callout type="warn">
Incident Management is currently restricted to platform administrators. While that restriction is in place, organization roles and organization-wide API tokens receive `403 imAccessDenied` on every `/api/im/**` endpoint, including this one.
</Callout>

## Request body

| Field | Type | Description |
|-------|------|-------------|
| `name` | string | Schedule name, up to 120 characters. |
| `timezone` | string | IANA timezone (e.g. `Europe/Berlin`). Every handover boundary and weekly window is evaluated in this zone, across DST. |
| `effectiveFrom` | string \| null | ISO timestamp. The schedule pages nobody before it. `null` = no start bound. |
| `effectiveUntil` | string \| null | ISO timestamp. The schedule pages nobody after it. `null` = no end bound. |
| `weeklySchedules` | array | Recurring on-call windows, up to 50. Each: `{ from: "HH:mm", until: "HH:mm", days: number[] }`, `days` as ISO weekdays (1 = Monday .. 7 = Sunday). `until` may be `"24:00"` for end-of-day, and an `until` at or before `from` wraps past midnight. An empty array means 24/7. |
| `rotations` | array | **Full replacement** of the rotation groups, up to 50. Each: `{ members: string[] }`, up to 200 members. Array order is the rotation order; member order within a group is preserved. Every member must already be a member of the schedule's team. Omit the field to leave rotations untouched — sending `[]` removes them all. |

Plus the cadence fields — `rotationMode`, `rotationRepeats`, `customRepeatUnit`, `customRepeatValue`, `autoRotationSize`, `roundRobinSize`, `startsOnTime`, `startsOnDayOfWeek`, `startsOnDateOfMonth`. They are documented once, with their allowed values, under [Rotation cadence](./schedules#rotation-cadence).

`teamId` and `tierId` are **not** patchable. Moving a schedule between teams or tiers would silently re-point who it pages, so it is not a partial update.

### How the cadence is validated

Cadence fields are merged onto the schedule's current row and then validated **as a whole**, not field by field. That is what lets you patch a single field — say `roundRobinSize` — without re-sending the rest of the cadence, while still rejecting a combination that cannot work (for example `rotationRepeats: "weekly"` with no `startsOnDayOfWeek` anywhere in the effective config).

Validation happens before anything is written. A cadence the materializer would choke on never reaches the database.

### When shifts are recomputed

Changing the timezone, the validity range, the weekly windows, any cadence field, or the rotations re-materializes the schedule's shifts. A bare rename does not — it would take the schedule's lock and rewrite rows for nothing.

## Example (cURL)

```bash
curl -X PATCH "$BASE_URL/api/im/schedules/7" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "rotationMode": "explicit",
    "rotationRepeats": "custom",
    "customRepeatUnit": "minutes",
    "customRepeatValue": 30,
    "startsOnTime": "09:00",
    "rotations": [
      { "members": ["u_abc123"] },
      { "members": ["u_def456"] }
    ]
  }'
```

## Response

`200 OK`: the schedule's configuration after the update.

```json
{
  "id": 7,
  "displayName": "Primary On-Call",
  "timezone": "Europe/Berlin",
  "effectiveFrom": "2026-08-01T00:00:00.000Z",
  "effectiveUntil": null,
  "weeklySchedules": [],
  "rotationMode": "explicit",
  "rotationRepeats": "custom",
  "customRepeatUnit": "minutes",
  "customRepeatValue": 30,
  "autoRotationSize": null,
  "roundRobinSize": 1,
  "startsOnDayOfWeek": null,
  "startsOnDateOfMonth": null,
  "startsOnTime": "09:00",
  "rotations": [
    {
      "id": 12,
      "rotationOrder": 1,
      "members": [{ "userId": "u_abc123", "userName": "Ada Lovelace", "memberOrder": 1 }]
    },
    {
      "id": 13,
      "rotationOrder": 2,
      "members": [{ "userId": "u_def456", "userName": "Bob Fixit", "memberOrder": 1 }]
    }
  ]
}
```

The `rotations` key is present **only when you sent `rotations`**. A request that did not touch them omits the key entirely rather than re-reading state you did not change; use `GET /api/im/teams/:id/escalations` if you need the current rotation set.

## Common errors

| Status | `data.code` | Cause |
|--------|-------------|-------|
| 400 | `invalidRequestBody` | Non-numeric `:id`, a missing or over-long `name`, an invalid `timezone`, or a malformed `rotations` entry (not an object, `members` not an array, an empty user id). |
| 403 | `imAccessDenied` | No Incident Management access — see the note under Authentication. |
| 403 | `imTeamWriteDenied` | IM access, but neither organization admin nor team admin of this schedule's team. |
| 404 | `imScheduleNotFound` | No such schedule in your organization. |
| 404 | `imTeamMemberNotFound` | A user id in `rotations` is not a member of the schedule's team. |
| 422 | `imInvalidRotationMode` | `rotationMode` is not `none`, `auto` or `explicit`. |
| 422 | `imInvalidRotation` | The cadence does not hold together — an unknown `rotationRepeats` or `customRepeatUnit`, a `startsOnDateOfMonth` outside 1–31, a required field missing for the effective cadence, too many rotations or members, or a duplicate user id inside one group. |
| 422 | `imInvalidNumber` | `customRepeatValue`, `autoRotationSize` or `roundRobinSize` is not an integer ≥ 1. |
| 422 | `imInvalidTimeOfDay` | `startsOnTime` or a weekly window's `from`/`until` is not `"HH:mm"`. |
| 422 | `imInvalidDayOfWeek` | `startsOnDayOfWeek`, or a day inside `weeklySchedules`, is not an ISO weekday 1–7. |
| 422 | `imInvalidTimeFilter` | `weeklySchedules` is not an array, has more than 50 entries, or an entry is not an object. |
