# Notifications CLI commands

> Canonical HTML: https://screenrig.ai/docs/cli/notifications

Tell people and systems when a screen changes state with notifications and webhooks, follow the event log, work alerts and wait on operations. Every command here runs as described in the [CLI reference](https://screenrig.ai/docs/cli.md): global flags, JSON output, exit codes, revisions and retries.

## Operations

Uploads and publications run as operations. `media upload`, `media replace` and `app upload` wait for theirs by default; these commands are how you finish one you left running with `--no-wait`.

### `operations show`

Read one operation's current state.

```console
operations show <id>
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The operation id from the envelope that started it. |

```console
screenrig operations show op_01EXAMPLEOPERATION00000
```

`state` is `receiving`, `queued`, `running`, `succeeded`, `failed` or `cancelled`; the last three end the operation. A succeeded operation carries its `result`, and a failed one carries `error` with the problem `code` and `detail`.

### `operations wait`

Poll one operation until it finishes.

```console
operations wait <id> [--timeout MS] [--poll-ms MS]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The operation id. |
| `--timeout MS` | integer | no | 120000 | Deadline for the wait. It also bounds each underlying request. |
| `--poll-ms MS` | integer | no | 1000 | Interval between operation polls while waiting. |

```console
screenrig operations wait op_01EXAMPLEOPERATION00000
```

The wait ends when the operation is `succeeded`, `failed` or `cancelled`. A failed or cancelled operation exits `13` with its own error. A wait that runs out of time exits `11` and the operation keeps running; the error's `next` is `operations wait ID`, which resumes the wait without starting it again.

### `operations cancel`

Ask the control plane to cancel one operation.

```console
operations cancel <id>
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The operation id. |

```console
screenrig operations cancel op_01EXAMPLEOPERATION00000
```

Cancelling needs Manage access. The answer is the cancelled operation.

## Events

The project event log records what the project did and what the fleet did. A published release appends `application.published`; an app that calls `screenrig.emit(code)` appends its own event, which is the most direct evidence that it ran on a screen.

Presence is in the log too. `screen.offline` is written once a screen has been offline for 60 seconds, and `screen.online` when it comes back; a reconnect inside those 60 seconds writes neither. `screen.offline` carries `details.offline_since` and, when the screen has been online before, `details.last_online_at`. `screen.online` carries `details.last_online_at`, plus `details.offline_at` when the screen was offline before it returned. Like every `screen.*` event, they are free on the listen stream, so an agent can keep `events follow` open to watch a fleet.

A Player's content updates arrive as `screen.manifest_changed`, `screen.manifest_activated` and `screen.manifest_upgrade`, whose `details` carry `manifest_revision`, `state` and, when present, `code` and `attempt`. A `failed` upgrade has error severity, and `retrying`, `blocked` and `partial` are warnings.

### `events list`

Read one finite page of project events.

```console
events list [--after CURSOR] [--limit N] [--type TYPE] [--source project|log] [--screen-id ID] [--playlist-id ID] [--app-id ID]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `--after CURSOR` | string | no | the first page | Resume after the `next_cursor` of the previous page. |
| `--limit N` | integer | no | 50 | Page size, 1 to 200. |
| `--type TYPE` | string | no | every type | One event type, or a family by a prefix ending in a dot, such as `note.`. |
| `--source SOURCE` | string | no | `project` | `project` lists project events. `log` lists Player log records, which belong to a screen: use it with `--screen-id`. |
| `--screen-id ID` | string | no | every screen | Events whose resource is this screen, including SDK emits. With `--source log`, that screen's Player log records. |
| `--playlist-id ID` | string | no | every playlist | Events whose resource is this playlist, and events that name it in `details.playlist_id`, such as a screen taking it. Not with `--source log`. |
| `--app-id ID` | string | no | every application | Application resource, release and K/V events; excludes SDK emits. Not with `--source log`. |

```console
screenrig events list --limit 25
screenrig events list --type application.event --screen-id scr_SCREEN
screenrig events list --playlist-id pl_PLAYLIST
screenrig events list --source log --screen-id scr_SCREEN
```

SDK `emit()` and `emitConfirmed()` append `application.event` records whose
resource is the screen. Read them with `--type application.event`, optionally
adding `--screen-id`. The returned `details.primitive_id` and `details.code`
identify the emitting primitive and named action. `--app-id` selects application
resource, release and K/V events, so it excludes these screen events.

`--source log` reads the Player log: the conditions a Player reports, kept for the project's event retention. It has no sequence, so it is not on `events follow` and is not delivered to webhooks, and `--playlist-id` and `--app-id` are refused with it (`usage_error`, exit 2). An id of the wrong kind is refused before any request. `data.next_cursor` is `null` at the end of the history.

The default output is one JSON envelope for the page. With `--human`, output is one logfmt line per event: `at=`, `type=`, `severity=`, and scalar details.

### `events follow`

Stay on the event stream and print events as they arrive.

```console
events follow [--after CURSOR] [--timeout MS] [--type TYPE]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `--after CURSOR` | string | no | the live tail | Resume from this cursor. |
| `--timeout MS` | integer | no | no deadline | Ends the whole follow, including reconnect backoff. |
| `--type TYPE` | string | no | every type | One event type, or a family by a prefix ending in a dot, such as `note.`. |

```console
screenrig events follow
```

A disconnect or transient failure reconnects with backoff and resumes from the last event id. Replay after a reconnect is free. Authorization and not-found problems stop the command rather than retrying it.

With `--type`, the stream still carries every event and the CLI prints only the matching ones, and the `stream.*` frames that describe the stream itself always print: `screenrig events follow --type note.` watches project notes.

The default output is NDJSON: one envelope per event on its own line, rather than a single envelope. Use `--human` for logfmt lines.

## Notifications

A notification tells a person, or another system, when a screen changes state. An email notification sends each recipient an email, a webhook notification sends a signed HTTPS POST, and a Slack, Teams, PagerDuty or Telegram notification posts a message to a chat or pager. An organization has at most 50 of every channel together across its projects, and every project starts with one email to its owner when a screen has been offline for 10 minutes.

A notification watches the conditions you name:

| Condition | Starts when | Ends when |
| --- | --- | --- |
| `screen_offline` | The Player has been offline for 60 seconds. | The screen is back online. |
| `display_disconnected` | The Player reports its display disconnected. | The display reconnects. |
| `running_hot` | The device reports a temperature at or above the hot threshold. | The temperature returns to normal. |
| `on_battery` | A device with a battery reports it runs on the battery, unplugged. | The device reports another power source. |
| `storage_full` | The Player's storage cannot hold its content. | The shortfall clears. |
| `content_stuck` | The screen's content update is retrying, failed, blocked or partial. | The update reaches any other state. |
| `not_playing` | The Player shows an error instead of its content. Full storage is `storage_full`. | The Player's status is no longer an error. |
| `loop_stalled` | A screen that is online, on schedule and has current content has started no image or video for longer than its loop allows. | An image or video starts again, or screenRIG stops judging the screen. |
| `glass_blank` | The screenshots screenRIG took over a run that covers the whole loop were all one flat tone. | Two screenshots in a row show a picture, or screenRIG stops judging the screen. |
| `glass_frozen` | The screenshots screenRIG took over a run that covers the whole loop were all the same picture. | Two screenshots in a row show a moving picture, or screenRIG stops judging the screen. |
| `crashing` | The Player crashed or restarted its renderer repeatedly. | Never: each spike is its own notice. |
| `source_failed` | A data source's refresh failed. | A refresh works. |
| `source_stale` | A data source kept failing until its data expired. | A refresh works. |
| `card_data_stale` | A card's data, written with an expiry, reached it while a screen shows the card and nothing renewed it. | A write to the card's data lands. |
| `reach_dropped` | A campaign reaches fewer screens than when it was last published, because screens left its tags or folders, or were archived or deleted. | The campaign reaches as many screens again, or it is published again. |
| `group_empty` | A tag or folder in a campaign's audience matches no screen. | The tag or folder matches a screen again, or a publish removes it from the audience. |

A condition notifies once it has lasted `--hold` minutes (default 10), so a quick reconnect stays quiet. If it ends first, nothing is sent; if it ends after a notice went out, a resolved notice follows. `crashing` is sent at once. A notice is also skipped when someone acknowledges or snoozes the screen's [alert](https://screenrig.ai/docs/cli/notifications.md#alerts) during the hold, or when a mute covers the screen. `--tag` limits a notification to screens carrying that tag; without it, every screen counts.

Each notice has a severity. An opened notice is `error` for `screen_offline` and `warning` for every other condition, and a resolved notice or a test is `info`. `--min-severity LEVEL` (`info`, `warning` or `error`) creates no opened notice below that level, and a resolved notice follows its opened one. `--severity CONDITION=LEVEL`, repeatable, sets one condition's level for this notification, such as `--severity not_playing=error`; it counts for `--min-severity`. Teams and PagerDuty always show the level, email, Slack and Telegram text name it when the notification sets `--min-severity` or `--severity`, and a webhook body carries `severity`.

`--active-hours SPEC` sends opened notices only inside weekly windows, read in `--timezone ZONE`, which it requires. `SPEC` is `DAYS HH:MM-HH:MM`, such as `"Mon-Fri 08:00-22:00"`; repeat it up to 16 times. With `--outside-hours hold`, the default, a notice that falls due outside the hours waits for the next window and goes out then if its condition still holds, so a condition that clears first sends nothing. With `drop` the notice is skipped as `outside_hours`, which `notifications deliveries` lists. A resolved notice for an opened notice that was sent always goes out, `crashing` follows the hours like any other condition, a snooze is checked first, and `notifications test` ignores hours, severity and audience.

`--audience FILE` (inline JSON, a file, or `-` for stdin) chooses the screens with the rules of a campaign audience, such as `{"all":[{"tag":"level2"},{"tag":"overhead"}]}` for screens carrying both tags; `--all-tags TAG` and `--any-tag TAG` are the shorthands for an `all` and an `any` rule of tags, and `--tag` means any of the tags. `--tag`, `--any-tag`, `--all-tags` and `--audience` exclude one another.

`loop_stalled`, `glass_blank` and `glass_frozen` are verdicts screenRIG draws from the play reports and screenshots it receives, and they leave out what they cannot judge: a screen that is offline, whose display is off, that is under a takeover or suspended, that shows a single still or a video wall, or whose Player sends no play numbers. When screenRIG stops judging a screen it had found in one of them, the alert closes, but email, Slack, Teams and Telegram send no resolved notice, because nothing showed the screen recovering; PagerDuty still resolves its incident.

`source_failed` and `source_stale` watch the project's data sources, not its screens: a notice names the source and links its page, and `--tag` never matches a source.

`card_data_stale` watches a card, not a screen. Write a card's data with an expiry sized to its writer's schedule (`card data put --ttl 30m`), and when the job or agent that renews it stops, the data expires, the card is blank on every screen that shows it, and this condition tells you. The notice names the card and when its data expired, and links the page editor on a page that places it. A data source's documents are never announced here (`source_stale` covers them), and neither is a card no screen shows, data written with no expiry, or data that was deleted. `--tag` never matches a card.

`reach_dropped` and `group_empty` watch a campaign, not a screen. The notice names the campaign and the screens it reached before and now, or the empty tag or folder, and links to the campaign's calendar. A campaign belongs to the project, so `--tag`, `--any-tag`, `--all-tags` and `--audience` never limit these two; `--min-severity`, `--severity`, `--active-hours` and `--hold` apply as for any condition, and both are `warning` by default. `--reach-drop-screens N` and `--reach-drop-percent P` send a `reach_dropped` notice only for a drop of at least N screens and at least P percent of the screens the campaign reached when it was published; a drop must meet both, and without them any drop notifies. They need `reach_dropped` in `--conditions`. Each empty tag or folder of a campaign is its own notice, and `notifications deliveries` shows it as `audience_group` (`tag:Lobby` or `folder:sfd_…`). A publish that removes an empty group from the audience closes a PagerDuty incident and a webhook's alert but sends no recovery email or chat message.

Email goes to each recipient's verified address, one email per recipient for everything that falls due within about two minutes, with a link to each screen and to the notification in the dashboard. Recipients are `owner`, whoever owns the project when the notice sends, and the user ids of project members. A recipient with no verified address, or who has left the project, is skipped. Notification emails and screen notices are free.

Every email carries an unsubscribe link for its own recipient: the Unsubscribe button of mail apps, and a link in the footer. Following it stops that notification's emails to that one person, without signing in, and the page it opens has a Resubscribe button. The other recipients are unaffected, and the person stays in the recipients. `notifications show` lists who unsubscribed with `unsubscribed`, and `notifications resubscribe` mails them again. A notice with nobody left to email is skipped as `no_recipients`.

A webhook notification POSTs each notice as JSON, which you verify as described under [Verify a delivery](https://screenrig.ai/docs/cli/notifications.md#verify-a-delivery). With `--events` it also POSTs raw project events as they happen; those deliveries are metered as described under [Webhooks](https://screenrig.ai/docs/cli/notifications.md#webhooks).

A Slack, Teams, PagerDuty or Telegram notification posts one message per outage and screen, or per group of screens: the screen, what changed, since when, the project, a link to the screen, and, when an offline screen is back, its likely cause if one is known by then. A recovery message follows once the first one went out, and in PagerDuty it resolves the incident the first one opened. You give the credential once (`--slack` takes an incoming webhook URL, `--teams` the URL of a Workflows webhook, `--pagerduty` an Events API v2 routing key, `--telegram` a bot token with `--telegram-chat`); it is sealed, never shown again, and `destination_hint` shows only its masked end. A `-` reads it from one line of stdin so it stays out of your shell history. A failed post is retried for 24 hours, and a notification that fails for 72 hours is disabled.

When three or more screens fall into the same condition within five minutes of one another and share a network, location, model, Player version or tag, an email, Slack, Teams, PagerDuty or Telegram notification sends one notice for them instead of one per screen, and one notice says the group when its last screen recovers; each other screen's notice is skipped as `grouped`. A webhook still sends one notice per screen.

Reading notifications and their deliveries needs Read only access; creating, changing, testing and deleting one, rotating its secret and resubscribing a person need Manage access.

| Problem code | Exit | Meaning |
| --- | --- | --- |
| `webhook_url_rejected` | `8` | The URL is not `https` on port 443 or 8443, or its host does not resolve only to public Internet addresses. `error.detail` carries the server's reason. Nothing was created or changed. |
| `notification_destination_rejected` | `8` | The destination does not fit its channel, or is a retired Office 365 connector URL. `error.detail` names the shape that fits. Nothing was created or changed. |
| `notification_limit_reached` | `5` | The organization already has 50 notifications. Delete one first. |
| `revision_conflict` | `6` | The `--expect-rev` revision was stale. Run `notifications show`, then retry. |
| `rate_limited` | `7` | More than 20 `notifications test` calls in a minute for this organization. |
| `invalid_request` | `8` | An `--email` recipient who is not a member of the project, or an option that belongs to another channel, such as `--url` on an email notification. Nothing was created or changed. |

### `notifications create`

Create an email, webhook, Slack, Teams, PagerDuty or Telegram notification.

```console
notifications create (--email RECIPIENTS | --webhook URL | --slack URL | --teams URL | --pagerduty KEY
                      | --telegram TOKEN --telegram-chat CHAT_ID) [--conditions LIST] [--events TYPES]
                     [--tag TAG... | --any-tag TAG... | --all-tags TAG... | --audience FILE] [--hold MINUTES]
                     [--min-severity LEVEL] [--severity CONDITION=LEVEL]... [--active-hours SPEC]...
                     [--timezone ZONE] [--outside-hours hold|drop] [--reach-drop-screens N] [--reach-drop-percent P]
                     [--description TEXT] [--disabled]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `--email RECIPIENTS` | string | one channel flag | none | Send email to these comma-separated recipients: `owner`, or the user id of a project member, at most 20. |
| `--webhook URL` | string | one channel flag | none | An `https` URL on port 443 (the default) or 8443 whose host resolves only to public Internet addresses. The server checks it on every write and again before every delivery. A URL carrying credentials (`user:pass@` or `token@`) or a `#` fragment is refused locally with exit 2. |
| `--conditions LIST` | string | yes, except a webhook | none | Comma-separated conditions from the table above. A webhook needs this, `--events`, or both. |
| `--slack URL` | string | one channel flag | none | Post to Slack through an incoming webhook URL on `hooks.slack.com`, or `-` to read it from one line of stdin. |
| `--teams URL` | string | one channel flag | none | Post to Teams through the URL of a Workflows webhook (the trigger “When a Teams webhook request is received”), or `-` for stdin. Office 365 connector URLs are retired and refused. |
| `--pagerduty KEY` | string | one channel flag | none | Trigger and resolve PagerDuty incidents with an Events API v2 routing key of 32 characters, or `-` for stdin. |
| `--telegram TOKEN` | string | one channel flag | none | Post to Telegram with a bot token, or `-` for stdin. It needs `--telegram-chat`. |
| `--telegram-chat CHAT_ID` | string | with `--telegram` | none | The chat to post to: its numeric id, such as `-1001234567890`, or `@channelname`. |
| `--events TYPES` | string | no | none | Webhook only. Also POST these raw project events: 1 to 32 comma-separated exact types, such as `screen.online`, or prefixes ending in `.*`, such as `screen.*`. |
| `--tag TAG` | string | no | every screen | Only screens carrying any of these tags. Repeat the flag or comma-separate, up to 16 tags. |
| `--hold MINUTES` | integer | no | 10 | How long a condition must last before it notifies, 0 to 1440. `crashing` is never held. |
| `--all-tags TAG` | string | no | none | Only screens carrying every listed tag. Repeat the flag or comma-separate. |
| `--any-tag TAG` | string | no | none | Only screens carrying at least one listed tag, as `--tag`. Repeat the flag or comma-separate. |
| `--audience FILE` | string | no | every screen | The screens as a campaign audience rule in JSON: inline, a file, or `-` for stdin. Not with the tag options. |
| `--min-severity LEVEL` | string | no | none | `info`, `warning` or `error`: opened notices below it are not sent. |
| `--severity CONDITION=LEVEL` | string | no | the defaults | Sets one condition's severity for this notification. Repeat the flag. |
| `--active-hours SPEC` | string | no | always | Send opened notices only inside this weekly window, `DAYS HH:MM-HH:MM` such as `"Mon-Fri 08:00-22:00"`. Repeat up to 16 times. Needs `--timezone`. |
| `--timezone ZONE` | string | with `--active-hours` | none | The IANA zone the hours are read in, such as `America/Los_Angeles`. |
| `--outside-hours MODE` | string | no | `hold` | `hold` waits for the next window; `drop` skips the notice. |
| `--reach-drop-screens N` | integer | no | any drop | Only with `reach_dropped`: send a notice only when the campaign lost at least N screens (1 or more). |
| `--reach-drop-percent P` | integer | no | any drop | Only with `reach_dropped`: also require a loss of at least P percent (1 to 100) of the screens the campaign reached when it was published. |
| `--description TEXT` | string | no | none | At most 200 characters. |
| `--disabled` | boolean | no | off | Create it disabled. Nothing is sent until `notifications update --enable`. |

```console
screenrig notifications create --email owner --conditions screen_offline,display_disconnected --tag Lobby
```

```console
screenrig notifications create --webhook https://hooks.example.com/screenrig --conditions screen_offline,crashing
```

```console
screenrig notifications create --slack - --conditions screen_offline,crashing < slack-url.txt
```

The envelope is the notification. A webhook's carries its signing secret in `data.secret` (`whsec_…`) with the warning `webhook_secret_shown_once`. Store the secret in the receiver's secret store right away. Only `notifications rotate-secret` returns it again, and the CLI never writes it to its configuration, operation log, or write-recovery state. After an interrupted or ambiguous create, rerun the identical command within 24 hours: it reuses the saved idempotency key, and the server returns the same answer, secret included. Notices start with the first event after the notification is created.

### `notifications list`

List this project's notifications, never their secrets.

```console
notifications list
```

```console
screenrig notifications list
```

Each row is a notification of any channel, oldest first, with its `status`. A Slack, Teams, PagerDuty or Telegram notification shows `destination_hint`, its credential masked to the host and last characters, in the `TO` column of `--human`. Below Manage access a webhook's `url` is empty and `url_redacted` is true, and `TO` shows `(withheld)`.

### `notifications show`

Inspect a notification and its delivery health.

```console
notifications show <id>
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `ntf_` (any channel but webhook) or `whk_` (webhook) notification id. |

```console
screenrig notifications show ntf_01EXAMPLENOTICE00000
```

`status` is `active`, `failing` while no delivery has succeeded since `failing_since`, or `disabled`. After 72 hours of continuous failure the notification is disabled with `disabled_reason: delivery_failures`, and the event log records `notification.disabled`. `snoozed_until` is present while a snooze is set. `last_success_at` and `last_failure_at` report the latest outcomes. `revision` is the value `--expect-rev` guards. `min_severity`, `severity`, `active_hours` with `outside`, `audience` and `reach_drop` appear while set, and an email notification lists `unsubscribed`.

### `notifications update`

Change a notification's recipients, URL, destination, conditions, screens, hold, snooze, or enabled state.

```console
notifications update <id> [--email RECIPIENTS | --url URL | --destination VALUE] [--telegram-chat CHAT_ID]
                          [--conditions LIST | --clear-conditions]
                          [--events TYPES | --clear-events] [--tag TAG... | --any-tag TAG... | --all-tags TAG... | --audience FILE | --all-screens]
                          [--hold MINUTES] [--min-severity LEVEL | --clear-min-severity]
                          [--severity CONDITION=LEVEL]... [--clear-severity]
                          [--active-hours SPEC]... [--timezone ZONE] [--outside-hours hold|drop] [--clear-active-hours]
                          [--reach-drop-screens N] [--reach-drop-percent P] [--clear-reach-drop]
                          [--description TEXT | --clear-description] [--enable | --disable]
                          [--snooze WHEN] [--expect-rev REVISION]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `ntf_` (any channel but webhook) or `whk_` (webhook) notification id. |
| `--email RECIPIENTS` | string | at least one change | unchanged | Email notifications: replaces the recipients. |
| `--url URL` | string | at least one change | unchanged | Webhook notifications: replaces the URL, checked like `create`. |
| `--destination VALUE` | string | at least one change | unchanged | Slack, Teams, PagerDuty and Telegram notifications: replaces the credential, checked like `create`. `-` reads it from one line of stdin. |
| `--telegram-chat CHAT_ID` | string | at least one change | unchanged | Telegram notifications: replaces the chat. |
| `--conditions LIST` | string | at least one change | unchanged | Replaces the conditions. |
| `--clear-conditions` | boolean | at least one change | off | Remove every condition. A webhook then needs `--events`. |
| `--events TYPES` | string | at least one change | unchanged | Webhook notifications: replaces the raw project events. |
| `--clear-events` | boolean | at least one change | off | Webhook notifications: stop POSTing raw project events. It then needs conditions. |
| `--tag TAG` | string | at least one change | unchanged | Replaces the screen tags, and an audience if the notification has one. |
| `--all-screens` | boolean | at least one change | off | Remove the screen tags and the audience; the notification covers every screen. |
| `--hold MINUTES` | integer | at least one change | unchanged | 0 to 1440. |
| `--all-tags TAG` | string | at least one change | unchanged | Replaces the screens with those carrying every listed tag. |
| `--any-tag TAG` | string | at least one change | unchanged | Replaces the screens with those carrying at least one listed tag, as `--tag`. |
| `--audience FILE` | string | at least one change | unchanged | Replaces the screens with a campaign audience rule in JSON: inline, a file, or `-` for stdin. |
| `--min-severity LEVEL` | string | at least one change | unchanged | `info`, `warning` or `error`. |
| `--clear-min-severity` | boolean | at least one change | off | Remove the severity filter. |
| `--severity CONDITION=LEVEL` | string | at least one change | unchanged | Replaces the severity overrides with these. Repeat the flag. |
| `--clear-severity` | boolean | at least one change | off | Remove every severity override. |
| `--active-hours SPEC` | string | at least one change | unchanged | Replaces the windows with these. Repeat up to 16 times. `--timezone` defaults to the zone the notification already has. |
| `--timezone ZONE` | string | at least one change | unchanged | The IANA zone the hours are read in. Alone, it keeps the current windows. |
| `--outside-hours MODE` | string | at least one change | unchanged | `hold` or `drop`. Alone, it keeps the current windows. |
| `--clear-active-hours` | boolean | at least one change | off | Remove the active hours. Notices that were held for them are judged again at once. |
| `--reach-drop-screens N` | integer | at least one change | unchanged | Replaces the least number of screens a `reach_dropped` notice needs the campaign to have lost. |
| `--reach-drop-percent P` | integer | at least one change | unchanged | Replaces the least percentage of its published reach the campaign must have lost. |
| `--clear-reach-drop` | boolean | at least one change | off | Remove the threshold, so any drop notifies. Take `reach_dropped` out of `--conditions` only together with it. |
| `--description TEXT` | string | at least one change | unchanged | At most 200 characters. |
| `--clear-description` | boolean | at least one change | off | Remove the description. |
| `--enable` | boolean | at least one change | off | Clears the failure state and starts at the event that records the change; nothing from while it was off is sent. |
| `--disable` | boolean | at least one change | off | Stops notices; pending notices fail with `webhook_disabled`. |
| `--snooze WHEN` | string | at least one change | unchanged | Send nothing until then: a duration such as `30m`, `1h` or `2d` (under 30 days), an RFC 3339 instant with seconds and an offset, or `off` to end the snooze. Notices that fall due meanwhile are skipped, not sent later. |
| `--expect-rev REVISION` | string | no | none | Optional revision precondition. |

```console
screenrig notifications update ntf_01EXAMPLENOTICE00000 --snooze 2h
```

`update` changes only the fields you pass. The channel never changes: `--email` applies to an email notification, `--url` and `--events` to a webhook, and `--destination` and `--telegram-chat` to a chat or pager one, and an option of another channel is `invalid_request` (exit 8). Give at most one of `--email`, `--url` and `--destination`. The screen tags and an audience exclude each other: setting one clears the other. An email notification keeps at least one condition, and a webhook keeps conditions or events. The signing secret is unchanged; use `notifications rotate-secret`.

### `notifications delete`

Delete a notification. Its pending notices fail with `webhook_deleted`.

```console
notifications delete <id> [--expect-rev REVISION]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `ntf_` (any channel but webhook) or `whk_` (webhook) notification id. |
| `--expect-rev REVISION` | string | no | none | Optional revision precondition. |

```console
screenrig notifications delete ntf_01EXAMPLENOTICE00000
```

Its delivery log cannot be read afterwards; its rows expire with event retention.

### `notifications rotate-secret`

Replace a webhook notification's signing secret and receive the new one once. Any other channel has no secret and answers `invalid_request` (exit 8).

```console
notifications rotate-secret <id> [--expect-rev REVISION]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `ntf_` (any channel but webhook) or `whk_` (webhook) notification id. |
| `--expect-rev REVISION` | string | no | none | Optional revision precondition. |

```console
screenrig notifications rotate-secret whk_01EXAMPLEWEBHOOK00000
```

The envelope carries the new secret in `data.secret` with the warning `webhook_secret_shown_once`; store it right away. The previous secret stops signing, but at most one attempt already in flight can still arrive signed with it, so accept both for a short while. After an interrupted or ambiguous rotation, rerun the identical command within 24 hours to get the same secret back.

### `notifications test`

Send one test email, signed test POST, or chat message to this notification only.

```console
notifications test <id>
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `ntf_` (any channel but webhook) or `whk_` (webhook) notification id. |

```console
screenrig notifications test ntf_01EXAMPLENOTICE00000
```

The envelope is the queued delivery. It is attempted once without retries, works on a disabled or snoozed notification, and never changes the notification's failure state. At most 20 test deliveries a minute per organization; more is `rate_limited` (exit 7). Read the outcome with `notifications deliveries`.

A PagerDuty test triggers an information event keyed `screenrig:test:DELIVERY_ID` and resolves it at once, so no incident stays open.

### `notifications resubscribe`

Email a person again who unsubscribed from a notification's emails.

```console
notifications resubscribe <id> --user USER_ID
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `ntf_` id of an email notification. |
| `--user USER_ID` | string | yes | none | The person's user id, as `notifications show` lists it under `unsubscribed`. |

```console
screenrig notifications resubscribe ntf_01EXAMPLENOTICE00000 --user usr_01EXAMPLEPERSON0000
```

The envelope is the notification. A person who is not unsubscribed changes nothing. Resubscribing a person raises the revision and appends `notification.updated`. A notification of another channel or project is `not_found` (exit 4).

### `notifications deliveries`

Read a notification's delivery log, newest first.

```console
notifications deliveries <id> [--before CURSOR] [--limit N]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `ntf_` (any channel but webhook) or `whk_` (webhook) notification id. |
| `--before CURSOR` | string | no | the newest event | The `next_cursor` (`whc1_…` or `whc2_…`) of the previous page. |
| `--limit N` | integer | no | 50 | Rows per page, 1 to 200. |

```console
screenrig notifications deliveries ntf_01EXAMPLENOTICE00000 --limit 20
```

Each row is one event, or one notice of one condition on one screen: `event_type`, `state` (`pending`, `succeeded`, `failed`, or `skipped`), `attempts`, `last_status`, and a fixed `last_error` class such as `http_status`, `timeout`, `connection_failed`, `tls_failed`, `dns_failed`, `url_rejected`, `email_failed`, `email_rejected`, or `secret_unavailable`. A screen notice adds `condition`, `phase` (`opened` or `resolved`), `screen_id`, `screen_name` (for a data source, a campaign or a card, its id and name) and `since`, and an email notice adds `emailed`, the number of people reached. A notice that never needed sending is `skipped`, with its reason in `last_error`: `condition_cleared` (the screen left the state before the hold ended), `not_opened` (no notice went out to resolve), `snoozed` (the notification is snoozed), `acknowledged` (someone acknowledged or snoozed the screen's alert during the hold), `muted` (a mute covers the screen), `outside_hours` (the notification's active hours were closed and it drops notices outside them), `below_severity` (the notice is below `min_severity`, which was raised after it was queued), `planned_reboot` (the screen went offline, disconnected its display or crashed while restarting for a reboot screenRIG sent, until it is done or ten minutes after it was sent), `no_recipients` (no recipient is a project member with a verified address who has not unsubscribed), `grouped` (the screen fell into the condition with others, and one notice on another row says them all), or `no_longer_judged` (a `loop_stalled`, `glass_blank` or `glass_frozen` condition ended because screenRIG stopped judging the screen, a `reach_dropped` one because the campaign was published again or deleted, a `group_empty` one because a publish removed the tag or folder from its audience, or a `card_data_stale` one because the card's data was deleted, so no recovery was said). The notice that said a group of screens carries `group` (`id`, `attribute`, `value` and `count`), and each other screen of the group carries `group_id`; `--human` shows both in a `GROUP` column. Rows never include the receiver's response. `next_cursor` is `null` at the end of the log. A failed attempt is retried with backoff, 30 seconds doubling to one hour, for 24 hours, then the row is `failed`.

### Verify a delivery

A screen notice's body is JSON: `type` (`alert.opened` or `alert.resolved`), `id` (the delivery id), `notification_id`, `project_id`, `condition`, `screen` (`id` and `name`), `since` (when the condition began), `at` (when it held long enough, or cleared), `event_id` and `severity` (`info`, `warning` or `error`: the condition's level on `alert.opened`, `info` on `alert.resolved`). `alert.resolved` for `screen_offline` also carries `likely_cause` (`device_restart`, `app_crash`, `app_restart`, `network` or `platform`), `null` while the cause is unknown. For a data source, a campaign or a card, `screen` names it, and a campaign notice adds `reach` (`published` and `current` screens) for `reach_dropped` or `audience_group` (`kind` `tag` or `folder`, and `value`) for `group_empty`. A raw event from `--events` is the same JSON object `events list` returns. The headers are `ScreenRig-Webhook-Id` (the notification id), `ScreenRig-Event-Id` (stable across retries: the delivery id for a notice, the event cursor for an event), and `ScreenRig-Signature: t=<unix seconds>,v1=<hex>`, where `v1` is HMAC-SHA256 over `"<t>.<raw body>"` keyed with the secret string. Compute it over the raw bytes before parsing JSON, compare in constant time, reject an old `t`, and deduplicate on `ScreenRig-Event-Id`, because delivery is at least once and unordered. Answer 2xx within 10 seconds; redirects are not followed.

```javascript
import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: the request body bytes (Buffer) exactly as received.
function verify(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
  if (typeof signatureHeader !== "string" || !signatureHeader) return false;
  const parts = Object.fromEntries(signatureHeader.split(",").map((part) => part.split("=", 2)));
  const t = Number(parts.t);
  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest();
  const given = Buffer.from(parts.v1 ?? "", "hex");
  return given.length === expected.length && timingSafeEqual(given, expected);
}
```

## Alerts

An alert is one problem on one screen while it lasts. It opens when a screen enters one of the screen [notification conditions](https://screenrig.ai/docs/cli/notifications.md#notifications) (`screen_offline`, `display_disconnected`, `running_hot`, `on_battery`, `storage_full`, `content_stuck`, `not_playing`, `loop_stalled`, `glass_blank` or `glass_frozen`), whether or not a notification watches it, and resolves when the condition ends. A screen has at most one open alert per condition. A crash spike, a data source, a card and a campaign are notices, never alerts. A screen that restarts for a reboot screenRIG sent (`screen reboot` or a reboot schedule) opens no `screen_offline` or `display_disconnected` alert until the reboot is done or ten minutes after it was sent; a condition that still holds then opens its alert. `alerts list` shows what is open now.

An open alert needs attention until someone acknowledges or snoozes it, or a mute covers its screen. Acknowledging says someone is on it: the alert stays open, records who acknowledged it and when, and needs no attention until it resolves. Snoozing acknowledges until a time at most 30 days ahead, and the alert needs attention again then if it is still open. A later acknowledge or snooze replaces the earlier one. While an alert is acknowledged or snoozed, a notification's notice about it that is still in its hold is skipped.

A mute is for maintenance: it names screens, tags or both and ends at a time at most 30 days ahead. It covers a screen while the screen's id is listed or it carries a listed tag, so a screen tagged later is covered and one that loses the tag is not. A muted screen's alerts still open and resolve, marked `muted`, but never need attention, and every notification skips their notices. An organization has at most 50 current mutes across its projects.

`counts.attention` in `alerts list` is the number the dashboard shows beside Alerts. Opening, resolving and acknowledging an alert append `screen.alert_opened`, `screen.alert_resolved` and `screen.alert_acknowledged` to the project event log, and creating and ending a mute append `alert_mute.created` and `alert_mute.deleted`. They are free on the listen stream and on webhooks, as every `screen.*` event is. Resolved alerts stay listed for the plan's event retention. Reading alerts and mutes needs Read only access; acknowledging, snoozing, muting and unmuting need Manage access.

| Problem code | Exit | Meaning |
| --- | --- | --- |
| `alert_not_open` | `5` | The alert already resolved, so there is nothing to acknowledge or snooze. Run `alerts list` and use an open one. |
| `alert_mute_limit_reached` | `5` | The organization already has 50 current mutes. End one with `alerts unmute`, or cover more screens with one mute by tag. |
| `invalid_request` | `8` | The `--until` instant is more than 30 days ahead. Nothing was changed. |
| `not_found` | `4` | No alert or mute has this id in the project. A resolved alert is gone after the plan's event retention, and a mute once it has ended. |

### `alerts list`

List this project's alerts, open ones first.

```console
alerts list [--state open|resolved|all] [--screen-id ID] [--before CURSOR] [--limit N]
            [--format json|csv|xlsx|pdf] [--output FILE]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `--state STATE` | string | no | `open` | `open`, `resolved` or `all`. |
| `--screen-id ID` | string | no | every screen | Only this screen's alerts. |
| `--before CURSOR` | string | no | the first page | The `next_cursor` of the previous page. |
| `--limit N` | integer | no | 100 | Rows per page, 1 to 200. |
| `--format FORMAT` | string | no | `json` | `json`, or `csv`, `xlsx` or `pdf` to write the alerts as a file instead of printing them. |
| `--output FILE` | path | no | `./alerts.FORMAT` | The file to write with `--format csv`, `xlsx` or `pdf`. Not with `json`. |

```console
screenrig alerts list
```

```console
screenrig alerts list --state all --screen-id scr_01EXAMPLESCREEN00000000
```

```console
screenrig alerts list --state all --format csv --output alerts.csv
```

Each row is one alert: `id` (`alr_…`), `screen_id`, `screen_name`, `condition`, `state` (`open` or `resolved`), `since` (when the condition began), `resolved_at` once it resolved, `acknowledged_at` and `acknowledged_by` (`kind`, `id` and `name` of the person or agent) once acknowledged, `snoozed_until` while snoozed, `muted` and `attention`. Open alerts come first, newest first. `counts` carries `open` and `attention` for the whole project, whatever the page and filters, and `next_cursor` is `null` at the end.

With `--format csv`, `xlsx` or `pdf` the file holds every alert the filters match, up to 50,000 rows, and the envelope carries its `path`, `bytes` and `sha256`. [Reports and exports](https://screenrig.ai/docs/reports.md) describes the files.

### `alerts show`

Show one alert.

```console
alerts show <id>
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `alr_` alert id. |

```console
screenrig alerts show alr_01EXAMPLEALERT000000
```

The envelope is the alert, in the shape of an `alerts list` row.

### `alerts ack`

Acknowledge an open alert: someone is on it.

```console
alerts ack <id>
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `alr_` alert id. |

```console
screenrig alerts ack alr_01EXAMPLEALERT000000
```

The alert stays open and needs no attention until it resolves; `acknowledged_by` names the person or agent and `acknowledged_at` the time. A notification's notice still in its hold is skipped. Acknowledging again replaces the earlier acknowledge or snooze. A resolved alert is `alert_not_open` (exit 5). The envelope is the alert, and the event log records `screen.alert_acknowledged`.

### `alerts snooze`

Acknowledge an open alert until a time, so it needs attention again then.

```console
alerts snooze <id> (--for DURATION | --until TIME)
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `alr_` alert id. |
| `--for DURATION` | duration | one of `--for`, `--until` | none | How long to snooze from now: a duration such as `30m`, `4h` or `2d`, at most `29d23h59m`. |
| `--until TIME` | string | one of `--for`, `--until` | none | When the snooze ends: an RFC 3339 instant with seconds and an offset, such as `2026-11-01T09:00:00-07:00`, at most 30 days ahead. |

```console
screenrig alerts snooze alr_01EXAMPLEALERT000000 --for 4h
```

`snoozed_until` is the time the snooze ends. Until then the alert needs no attention and a notification's notice still in its hold is skipped, as for `alerts ack`; if the alert is still open at that time, it needs attention again. A resolved alert is `alert_not_open` (exit 5).

### `alerts mute`

Mute screens for maintenance.

```console
alerts mute [--screen-id ID]... [--tag TAG]... (--for DURATION | --until TIME) [--note TEXT]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `--screen-id ID` | string | at least one of `--screen-id`, `--tag` | none | A `scr_` screen id. Repeat it, or list ids comma-separated; at most 100. |
| `--tag TAG` | string | at least one of `--screen-id`, `--tag` | none | Every screen carrying this tag. Repeat it, or list tags comma-separated; at most 16. |
| `--for DURATION` | duration | one of `--for`, `--until` | none | How long the mute lasts from now: a duration such as `30m`, `4h` or `2d`, at most `29d23h59m`. |
| `--until TIME` | string | one of `--for`, `--until` | none | When the mute ends: an RFC 3339 instant with seconds and an offset, such as `2026-11-01T09:00:00-07:00`, at most 30 days ahead. |
| `--note TEXT` | string | no | none | Why the screens are muted, for the people who see the mute; at most 200 characters. |

```console
screenrig alerts mute --tag Lobby --for 2h --note "Rewiring the lobby displays"
```

The envelope is the mute: `id` (`amt_…`), `screen_ids`, `tags`, `until`, `note`, `created_by` and `created_at`. The alerts of every covered screen read `muted` at once, those already open included. An organization has at most 50 current mutes across its projects; one more is `alert_mute_limit_reached` (exit 5), and a mute that has ended no longer counts. After an interrupted or ambiguous mute, read `alerts mutes` first: rerunning the identical command with `--until` within 24 hours returns the same mute, while a rerun with `--for` counts from a new time and makes another.

### `alerts mutes`

List the mutes that have not ended, soonest end first.

```console
alerts mutes
```

```console
screenrig alerts mutes
```

Each row is a mute as `alerts mute` prints it.

### `alerts unmute`

End a mute now.

```console
alerts unmute <mute-id>
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<mute-id>` | string | yes | none | The `amt_` mute id. |

```console
screenrig alerts unmute amt_01EXAMPLEMUTE0000000
```

The screens' open alerts need attention again, and later notices are no longer skipped as muted. A mute that has ended is `not_found` (exit 4). The event log records `alert_mute.deleted`.

## Webhooks

A webhook sends this project's own events to an HTTPS endpoint you run as signed POSTs, so a matching event can wake an agent, a bot, or your own service. Each matching event arrives as the same JSON object `events list` returns. A webhook is a [notification](https://screenrig.ai/docs/cli/notifications.md#notifications) in its original form: it counts toward the organization's 50 notifications, and `notifications list` shows it. New work uses `notifications`, which also sends email and screen notices.

Each delivery attempt is free and does not count toward the organization's hourly agent request cap.

```console
webhooks create --url URL --event-types TYPES [--description TEXT] [--disabled]
webhooks list
webhooks show <id>
webhooks update <id> [--url URL] [--event-types TYPES] [--description TEXT | --clear-description]
                     [--enable | --disable] [--expect-rev REVISION]
webhooks delete <id> [--expect-rev REVISION]
webhooks rotate-secret <id> [--expect-rev REVISION]
webhooks test <id>
webhooks deliveries <id> [--before CURSOR] [--limit N]
```

`webhooks create` answers `webhook_limit_reached` (exit 5) when the organization already has 50 notifications, where `notifications create` answers `notification_limit_reached`. A refused URL is `webhook_url_rejected` (exit 8) for both.

`--url` takes an `https` URL on port 443 or 8443 whose host resolves only to public Internet addresses, checked like `notifications create`; a URL that carries credentials or a `#` fragment is refused locally with exit 2. `--event-types` takes 1 to 32 comma-separated exact types, such as `screen.online`, or prefixes ending in `.*`, such as `screen.*`. `--description` is at most 200 characters. `--disabled` creates the webhook disabled. `webhooks update --enable` clears the failure state and starts at the current event, so nothing from while it was disabled is sent, and `--disable` fails pending deliveries with `webhook_disabled`. `--expect-rev` guards the revision (`revision_conflict`, exit 6).

`webhooks create` and `webhooks rotate-secret` print the signing secret once in `data.secret` with the warning `webhook_secret_shown_once`; after an interrupted or ambiguous call, rerun the identical command within 24 hours to get the same answer back. Delivery starts with the first event after the webhook is created. `webhooks test` queues one `webhook.test` delivery for this webhook only; it works on a disabled webhook, is attempted once without retries, is metered like any delivery, and is limited to 20 a minute per organization (`rate_limited`, exit 7). `webhooks list` shows this project's webhook notifications, oldest first, and a Read only credential receives each `url` empty with `url_redacted` true. `webhooks deliveries` reads the log newest event first, 1 to 200 rows a page (default 50), with `next_cursor` (`whc1_…`) as `--before`; a row's `state` is `pending`, `succeeded` or `failed`, with `last_status` and a fixed `last_error` class. Verify a delivery as described under [Verify a delivery](https://screenrig.ai/docs/cli/notifications.md#verify-a-delivery).

## Other command groups

- [Projects](https://screenrig.ai/docs/cli/projects.md)
- [Service clients](https://screenrig.ai/docs/cli/service-clients.md)
- [Help and support](https://screenrig.ai/docs/cli/help.md)
- [Media](https://screenrig.ai/docs/cli/media.md)
- [Media sync](https://screenrig.ai/docs/cli/media-sync.md)
- [Playlists](https://screenrig.ai/docs/cli/playlists.md)
- [Cards](https://screenrig.ai/docs/cli/cards.md)
- [Applications](https://screenrig.ai/docs/cli/applications.md)
- [Screens](https://screenrig.ai/docs/cli/screens.md)
- [Schedules](https://screenrig.ai/docs/cli/schedules.md)
- [Campaigns](https://screenrig.ai/docs/cli/campaigns.md)
- [Device health](https://screenrig.ai/docs/cli/device.md)
- [Reviews and notes](https://screenrig.ai/docs/cli/reviews.md)
- [Reports](https://screenrig.ai/docs/cli/reports.md)
- [Data sources](https://screenrig.ai/docs/cli/data-sources.md)
- [BI dashboards](https://screenrig.ai/docs/cli/bi.md)
- [Triggers](https://screenrig.ai/docs/cli/triggers.md)
- [Fleet links](https://screenrig.ai/docs/cli/fleet-links.md)
- [Advertising](https://screenrig.ai/docs/cli/ads.md)
