> ## Documentation Index
> Fetch the complete documentation index at: https://docs.devhelm.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Planning Maintenance Windows

> Schedule planned downtime in DevHelm without paging on-call — from the CLI, SDKs, REST API, or an AI agent

By the end of this guide, you'll have maintenance windows configured for the three flows that actually happen in production:

1. **A planned deploy or migration that takes 30 minutes** — schedule a one-shot window from your deploy script.
2. **Recurring weekly infrastructure maintenance** — a single window with an iCal RRULE.
3. **An AI agent scheduling the window for you** — via the MCP server's `create_maintenance_window` tool.

For the conceptual model and the field reference, see [Maintenance windows](/incidents/maintenance-windows).

<Accordion title="Prerequisites">
  * DevHelm CLI installed (or any SDK / MCP-enabled agent)
  * An API token (`DEVHELM_API_TOKEN`) and the org / workspace headers set
  * At least one monitor running — see the [quickstart](/quickstart)
</Accordion>

<Note>
  **Maintenance windows are state, not infrastructure.** They live in the imperative surfaces (CLI, SDKs, MCP, REST API, dashboard) — *not* in `devhelm.yml` or the Terraform provider. See [Why not Terraform / not in devhelm.yml?](/incidents/maintenance-windows#why-not-terraform-why-not-devhelm-yml) for the rationale.
</Note>

## Schedule a 30-minute window now

Pick the surface you'll wire into your deploy pipeline.

<CodeGroup>
  ```bash CLI theme={null}
  devhelm maintenance-windows create \
    --start "2026-05-15T14:00:00Z" \
    --end   "2026-05-15T14:30:00Z" \
    --reason "Quarterly DB upgrade" \
    --monitor <monitor-id>
  ```

  ```python Python SDK theme={null}
  from devhelm import Devhelm

  client = Devhelm()  # picks up DEVHELM_API_TOKEN + tenant headers from env

  window = client.maintenance_windows.create({
      "monitorId": "<monitor-id>",
      "startsAt":  "2026-05-15T14:00:00Z",
      "endsAt":    "2026-05-15T14:30:00Z",
      "reason":    "Quarterly DB upgrade",
  })
  print(f"window opened: {window.id}")
  ```

  ```typescript JS / TS SDK theme={null}
  import {Devhelm} from '@devhelm/sdk'

  const client = new Devhelm({token: process.env.DEVHELM_API_TOKEN!})

  const window = await client.maintenanceWindows.create({
    monitorId: '<monitor-id>',
    startsAt:  '2026-05-15T14:00:00Z',
    endsAt:    '2026-05-15T14:30:00Z',
    reason:    'Quarterly DB upgrade',
  })
  console.log(`window opened: ${window.id}`)
  ```

  ```bash REST API theme={null}
  curl -X POST https://api.devhelm.io/api/v1/maintenance-windows \
    -H "Authorization: Bearer $DEVHELM_API_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "monitorId": "<monitor-id>",
      "startsAt": "2026-05-15T14:00:00Z",
      "endsAt": "2026-05-15T14:30:00Z",
      "reason": "Quarterly DB upgrade"
    }'
  ```
</CodeGroup>

Omit `monitorId` (or `--monitor`) for an **org-wide** window covering every monitor — the right call when a deploy or upgrade touches the whole platform.

To cover a specific *list* of monitors (the API stores one monitor per window), pass several IDs to the CLI's `--monitor` flag and the CLI fans out into one window per monitor:

```bash theme={null}
devhelm maintenance-windows create \
  --start "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
  --end   "$(date -u -d '+30 minutes' +%Y-%m-%dT%H:%M:%SZ)" \
  --reason "Production deploy $(git rev-parse --short HEAD)" \
  --monitor 9f4a-...,8b21-...,7c33-...
```

The SDKs are deliberately one-window-at-a-time so the call site stays explicit; loop over your list there.

## Wire it into your deploy script

The full pattern is **create → run → cancel**. If the deploy succeeds in 5 minutes, cancel the window early; if it runs long, *extend* (don't replace) the window.

```bash theme={null}
set -euo pipefail

WINDOW=$(devhelm maintenance-windows create \
  --start "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
  --end   "$(date -u -d '+30 minutes' +%Y-%m-%dT%H:%M:%SZ)" \
  --reason "Deploy $(git rev-parse --short HEAD)" \
  --monitor "$MONITOR_ID" \
  -o json | jq -r '.id')

trap 'devhelm maintenance-windows delete "$WINDOW" || true' EXIT

# ... run your deploy ...
```

The `trap … EXIT` line guarantees the window is cancelled even if the deploy script aborts — a half-suppressed monitor outliving the deploy is the failure mode you want to avoid.

### Extend instead of letting it expire

If a migration runs long, **update** the existing window with a new `--end` rather than waiting for it to expire and then scheduling a new one. The expiry / re-create path leaks alerts during the gap.

```bash theme={null}
devhelm maintenance-windows update "$WINDOW" \
  --start "$ORIGINAL_START" \
  --end   "$(date -u -d '+30 minutes' +%Y-%m-%dT%H:%M:%SZ)" \
  --reason "Migration still running"
```

The update endpoint is a full replacement (`PUT`) — pass `--start` and `--end` together.

## Schedule a recurring window

For weekly maintenance, use an [iCal RRULE](https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.10). The first `start`/`end` define the first occurrence; the RRULE generates the rest.

<CodeGroup>
  ```bash CLI theme={null}
  devhelm maintenance-windows create \
    --start "2026-05-19T02:00:00Z" \
    --end   "2026-05-19T04:00:00Z" \
    --repeat-rule "FREQ=WEEKLY;BYDAY=TU" \
    --reason "Weekly infra maintenance"
  ```

  ```python Python SDK theme={null}
  client.maintenance_windows.create({
      "startsAt":   "2026-05-19T02:00:00Z",
      "endsAt":     "2026-05-19T04:00:00Z",
      "repeatRule": "FREQ=WEEKLY;BYDAY=TU",
      "reason":     "Weekly infra maintenance",
  })
  ```

  ```typescript JS / TS SDK theme={null}
  await client.maintenanceWindows.create({
    startsAt:   '2026-05-19T02:00:00Z',
    endsAt:     '2026-05-19T04:00:00Z',
    repeatRule: 'FREQ=WEEKLY;BYDAY=TU',
    reason:     'Weekly infra maintenance',
  })
  ```
</CodeGroup>

Common RRULE patterns:

| Schedule                | RRULE                             |
| ----------------------- | --------------------------------- |
| Every Tuesday           | `FREQ=WEEKLY;BYDAY=TU`            |
| First day of each month | `FREQ=MONTHLY;BYMONTHDAY=1`       |
| Every other Saturday    | `FREQ=WEEKLY;BYDAY=SA;INTERVAL=2` |

## Let an AI agent schedule it for you

If you're driving a deploy from Cursor, Claude Desktop, or any other MCP-enabled assistant, the [DevHelm MCP server](/sdk/mcp/overview) exposes maintenance-window tools that an agent can call **before** the deploy and again **after** it finishes:

* `create_maintenance_window` — open the window scoped to the affected monitors.
* `update_maintenance_window` — extend if the deploy runs long.
* `cancel_maintenance_window` — close it once the deploy verifies green.
* `list_maintenance_windows` — check whether one is already open before scheduling.
* `get_maintenance_window` — fetch a single window by ID.

A typical agent flow looks like:

> **Agent**: I'm about to run the production migration. I'll open a 30-minute maintenance window first so on-call doesn't get paged.
>
> *(calls `create_maintenance_window` with `monitorId`, `startsAt`, `endsAt`, `reason: "Postgres major upgrade — DB-1234"`)*
>
> **Agent**: Window scheduled. Running the migration.
>
> *(deploy runs)*
>
> **Agent**: Migration succeeded, all health checks green. Closing the window so alerting resumes.
>
> *(calls `cancel_maintenance_window` with the window ID)*

Wire this into your agent's pre-deploy / post-deploy hooks so it's the default path, not something a human has to remember. See [DevHelm Skill](https://github.com/devhelmhq/skill) for the agent-side instructions.

## What happens during a window

| Behavior                                           | During an active window                                                           |
| -------------------------------------------------- | --------------------------------------------------------------------------------- |
| Monitor checks                                     | **Still run.** Maintenance windows do not pause monitoring.                       |
| Check results                                      | **Still recorded.** Your historical uptime / response-time charts stay accurate.  |
| Incidents                                          | **Still created** — but at severity `MAINTENANCE` instead of `DOWN` / `DEGRADED`. |
| Notification policies                              | **Skipped** when `suppressAlerts` is `true` (the default).                        |
| Channel deliveries (Slack / PagerDuty / email / …) | **Suppressed.**                                                                   |
| Resume after the window ends                       | **Immediate** — the next failed check evaluates policies normally.                |

For the full precedence story (maintenance windows vs. resource-group suppression vs. notification policies), see [Alert suppression](/alerting/suppression).

## Listing and inspecting windows

Use the listing flags to confirm what's currently open before scheduling:

```bash theme={null}
devhelm maintenance-windows list --status active
devhelm maintenance-windows list --status upcoming
devhelm maintenance-windows list --monitor <monitor-id>
```

`active` returns currently open windows; `upcoming` returns scheduled-but-not-yet-open windows. Past / cancelled windows are not surfaced in the list today — check the audit log if you need that history.

## Next steps

<CardGroup cols={2}>
  <Card title="CLI command reference" icon="terminal" href="/cli/commands/maintenance-windows">
    Full flag list and deploy-script pattern.
  </Card>

  <Card title="MCP server overview" icon="robot" href="/sdk/mcp/overview">
    Wire AI agents into your deploy flow.
  </Card>

  <Card title="Alert suppression" icon="bell-slash" href="/alerting/suppression">
    Maintenance windows vs. resource-group suppression.
  </Card>

  <Card title="CI/CD pipeline" icon="rotate" href="/guides/ci-cd-pipeline">
    Add maintenance windows to your release workflow.
  </Card>
</CardGroup>
