# Screens CLI commands

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

Pair, list, publish to, assign, tag, move, reload, capture and delete screens, and manage folders, links, video walls and live view. 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.

## Screens

A screen is one Player. These commands read it, point it at a playlist, and manage its life. Revision preconditions are optional on writes.

Tags group screens into fleets. Each screen carries 0 to 16 unique tags of 1 to 32 letters or digits, set with `screen tag`. `screen assign`, `screen reload`, `screen toast`, `screen tag`, and `screen screenshot` take one screen id, several ids, or `--tag TAG`, and a fleet answer reports every screen: see [Fleet results](https://screenrig.ai/docs/cli/screens.md#fleet-results).

Screen folders group screens by place, one folder per screen. A fleet command also takes `--folder ID`: see [Screen folders](https://screenrig.ai/docs/cli/screens.md#screen-folders).

A screen in a [protected folder](https://screenrig.ai/docs/permissions.md#protected-folders) answers a playlist change with `pending_review` for a second approver, and refuses tag, field, location, timezone, wall, replace, delete, archive, suspend, display, volume, toast and play-page changes with `protected_target` (exit 5).

A `storage_full` warning on `screen list`, `screen show`, `screen update`, `screen assign` or `screen publish` means the device running the Player has no room for its content: free up storage there, then retry.

### `screen pair`

Pair a Player with the code shown on its screen.

```console
screen pair <code...> [--name NAME] [--folder ID]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<code...>` | string | yes | none | The six-character pairing code the Player shows. Spaces, dashes and letter case are ignored, so a code shown in two groups works with or without quotes. |
| `--name NAME` | string | no | an unused `Screen N` | Display name for the screen, 1 to 120 characters. |
| `--folder ID` | string | no | project root | The `sfd_` screen folder to put the screen in. An agent limited to screen folders must name a folder of its scope; the project root and every other folder answer `outside_screen_scope` (exit 3). |

```console
screenrig screen pair "$PAIRING_CODE" --name "Lobby"
screenrig screen pair "$PAIRING_CODE" --folder sfd_01EXAMPLEFOLDER000000000
```

The answer carries `public_url` and the paired `screen`. The Player finishes pairing the next time it contacts the service, with nothing to do on the display: `screen show` reports `state` `active` and `online` `true` once it has. After an interrupted request, rerun the identical command to reuse the saved idempotency key.

### `browser setup`

Complete a browser Player's setup from the code it shows.

```console
browser setup --code CODE [--open]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `--code CODE` | string | yes | none | The setup code the browser Player shows. |
| `--open` | boolean | no | off | Open the browser Player. |

```console
screenrig browser setup --code SETUP_CODE --open
```

The answer carries `code`, `status` (`claimed`), `screen_id` and the screen's `player_public_url`. The CLI withholds a URL that fails its safety checks, with the warning `player_url_withheld`. An invalid or expired code creates no screen. After an interrupted request, rerun the identical command.

### `screen list`

List screens. Archived screens are omitted unless you ask for them.

```console
screen list [--state archived] [--tag TAG] [--folder ID|root] [--field NAME=VALUE...] [--near LAT,LON,KM]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `--state` | string | no | active screens | The only accepted value is `archived`, which lists archived screens only. |
| `--tag TAG` | string | no | every screen | Lists only screens carrying this exact tag. |
| `--folder ID\|root` | string | no | every screen | Lists only the screens directly inside this `sfd_` screen folder, not those of its subfolders, or with `root` only screens outside every folder. |
| `--field NAME=VALUE` | string | no | none | Lists only screens whose field `NAME` holds exactly `VALUE`. Repeatable, each name once; a screen must match every one. |
| `--near LAT,LON,KM` | string | no | none | Lists only screens located within `KM` kilometres of the point, in decimal degrees; `KM` is above 0. A screen with no location is left out. |

```console
screenrig screen list --tag Lobby
screenrig screen list --folder sfd_01EXAMPLEFOLDER000000000
screenrig screen list --field region=north
screenrig screen list --near 47.6062,-122.3321,25
```

The filters combine. Each row carries the screen's `tags`, `folder_id`, `fields` and `location` when it has them. With `--human`, the table adds a column only when a listed screen has the value: `PLATFORM`, `REASON` (an archived screen's `archive_reason`), `TAGS`, `FOLDER`, `FIELDS`, `LOCATION`, `ASSIGNED` (a screen that plays something other than its default playlist), `HEALTH` (a screen that needs attention) and `UPGRADE` (a screen whose Player is still switching to the issued playlist version, or failed to). `--state archived` also lists screens a Player archived itself (`device_reset`, `device_unpair`) and screens `screen replace` archived (`replaced`).

### `screen show`

Read one screen, including revision, assignment, and liveness.

```console
screen show <id>
```

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

```console
screenrig screen show scr_01EXAMPLESCREEN00000000
```

`tags` lists the screen's fleet tags. `effective_playlist`, `takeover`, and `playlist_schedule` report what the screen plays and why; see [Schedules and takeover](https://screenrig.ai/docs/cli/schedules.md#schedules). `health` and the Player's host capabilities are in [Device health and power](https://screenrig.ai/docs/cli/device.md#device). `online` is true while a Player is connected, including a short reconnect window. Optional `last_online_at` and `last_ip` appear after the first connect. Optional `observation` reports the surfaces a Player has painted: `id`, `width`, `height`, `pixel_ratio`, and `presentation`. All of these are read-only; `screen update` cannot send them, and an archived screen still returns its row.

`commands` lists the screen's last 10 reboots, reloads, cache clears, app restarts and updates, oldest first, each with its `type`, its `state` (`queued`, `sent`, `done`, `failed`, `expired`, `cancelled`, or `superseded`), its `source` (`schedule` for a reboot a [reboot schedule](https://screenrig.ai/docs/cli/device.md#cmd-screen-reboot-schedule) issued; absent for one a person asked for), its `expires_at`, and when it was sent or done. `reboot_schedule` and `next_reboot_at` report the screen's reboot schedule and the start of its next reboot window. A cache clear, app restart or update the Player ended also carries its `cause` and a short `detail`, which `--human` prints as a `CAUSE` column and a line under the table. `suspension` carries the `message` and `since` of a suspended screen. A screen archived by `screen replace` reports `replaced_by`, the screen its device moved to. With `--human`, `screen show` prints each of them.

`last_outage` reports the screen's latest outage: `offline_since`, `back_at` and `duration_s`, and, once a health report decides it, a `likely_cause` of `device_restart`, `app_crash`, `app_restart`, `network` or `platform`; it is absent when screenRIG cannot tell. A restart is decided as the screen comes back, while a network or screenRIG cause waits for the Player's next health report, which can take up to fifteen minutes. `glass` carries the verdicts screenRIG drew from the screen's play reports and screenshots. With `--human`, `screen show` prints `Last outage` and `Likely cause`, and `Playlist stalled`, `Blank screen` or `Frozen picture` for a verdict that found a problem; [Notifications](https://screenrig.ai/docs/cli/notifications.md#notifications) says what the verdicts leave alone.

`folder_id` names the screen's folder and is absent at the project root. `fields`, `location` and `wall` appear once set: see `screen update` and `screen wall set`. Each `observation.surfaces` entry also carries `rotation` when the Player can read it, and a video wall reports one entry per output, named as `screen wall set` takes them.

`manifest_upgrade` reports whether the Player has switched to the playlist version issued to it. Its `state` is `none`, `pending`, `downloading`, `preparing`, `activating`, `retrying`, `failed`, `blocked`, `partial` or `current`, beside `desired_playlist` and `active_playlist` (`id`, `name` and `revision`), the last version the Player acknowledged, which does not prove that every session shows it. A failing or partial upgrade also carries `code`, `attempt`, `retry_at` and `missing_page_count`. `--human` prints `Target v42 · Playing v41`. A failed upgrade schedules no retry: fix the cause, then run `screen reload` or assign the playlist again. A saved assignment does not confirm playback.

`applications_unsupported` appears while the paired Player cannot show `application` or `iframe` pages: the manifest is unchanged and the Player skips them. `recovery_pending` appears while a display that lost its stored identity offers to reconnect as this screen, with the `expires_at` of the offer and the `platform`, `model`, `firmware` and `manufacturer` the display reports: see [`screen recover`](https://screenrig.ai/docs/cli/screens.md#cmd-screen-recover).

### `screen update`

Patch a screen's name, playlist assignment, timezone override, fields, or location in one call.

```console
screen update <id> [--name NAME] [--playlist-id ID | --clear-playlist] [--timezone ZONE | --clear-timezone] [--field NAME=VALUE...] [--unset-field NAME...] [--clear-fields] [--location LAT,LON [--location-name NAME] [--address TEXT] | --clear-location] [--expect-rev REVISION]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `scr_` screen id. |
| `--name NAME` | string | no | unchanged | Display name for the screen. |
| `--playlist-id ID` | string | no | unchanged | The `pl_` playlist this screen should run. Not with `--clear-playlist`. |
| `--clear-playlist` | boolean | no | off | Clears the screen's own default assignment. Pairing and every other content level stay, and the screen shows idle when none applies. An archived screen answers `screen_archived`. Not with `--playlist-id`. |
| `--timezone ZONE` | string | no | unchanged | Overrides the screen's time zone with an IANA zone identifier such as `America/Los_Angeles`. Validated against the server's zone database. Not with `--clear-timezone`. |
| `--clear-timezone` | boolean | no | off | Removes the override, so the screen follows the zone its device reports again. Not with `--timezone`. |
| `--field NAME=VALUE` | string | no | unchanged | Sets one field and leaves the others. Repeatable, each name once. `NAME` is 1 to 32 lower case letters, digits or underscores, `VALUE` is 1 to 200 characters after trimming, and a screen holds at most 32 fields. Not with `--clear-fields`. |
| `--unset-field NAME` | string | no | unchanged | Removes one field. Repeatable. Not with `--clear-fields`. |
| `--clear-fields` | boolean | no | off | Removes every field. Not with `--field` or `--unset-field`. |
| `--location LAT,LON` | string | no | unchanged | Where the screen is, in decimal degrees: latitude from -90 to 90, longitude from -180 to 180. Replaces the whole location, so repeat `--location-name` and `--address` to keep them. Not with `--clear-location`. |
| `--location-name NAME` | string | no | none | The short place name a card shows, 1 to 40 characters. Requires `--location`. |
| `--address TEXT` | string | no | none | An address for people and agents, 1 to 200 characters. Requires `--location`. |
| `--clear-location` | boolean | no | off | Removes the location. Not with `--location`. |
| `--expect-rev REVISION` | string | no | none | Optional revision precondition. Omit to write the current resource. A supplied stale value is `revision_conflict`: refetch, reapply, retry. |

Provide at least one of `--name`, `--playlist-id`, `--clear-playlist`, `--timezone`, `--clear-timezone`, `--field`, `--unset-field`, `--clear-fields`, `--location` or `--clear-location`.

```console
screenrig screen update scr_01EXAMPLESCREEN00000000 --name "Lobby"
screenrig screen update scr_01EXAMPLESCREEN00000000 --field store_number=0142 --field region=north
screenrig screen update scr_01EXAMPLESCREEN00000000 --location 47.6062,-122.3321 --location-name "Seattle HQ"
screenrig screen update scr_01EXAMPLESCREEN00000000 --clear-playlist --expect-rev 3
```

Under review, a change of `--playlist-id` alone is proposed like
`screen assign`; a Propose session cannot change the other fields.

Changing fields or location changes the screen's manifest revision, so a screen that shows a playlist picks up the new values. Cards and iframe pages read them per screen, so one playlist shows each screen's own values: `{screen.field.NAME}`, `{screen.name}` and `{screen.timezone}` fill any text param of a card, `"location": "{screen.location}"` fills a card's location in the shape that card takes, and a card data key may carry `{screen.field.NAME}`, such as `menu-{screen.field.store_number}`. A screen that lacks a field shows that text empty, and the card goes without that data key or location. `screen show` lists a screen's fields and location, and `screen list` finds screens by them. See [Screen tokens in iframe addresses and application params](https://screenrig.ai/docs/cli/media.md#screen-tokens-in-iframe-addresses-and-application-params).

### `screen publish`

Create a new playlist from a prepared file and assign it to a screen.

```console
screen publish <id> <file> [--expect-rev REVISION] [--note TEXT] [--no-wait] [--poll-ms MS] [--timeout MS] [--no-progress]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `scr_` screen id. |
| `<file>` | path | yes | none | The prepared playlist document, or `-` for stdin. |
| `--expect-rev REVISION` | string | no | none | Optional **screen** revision precondition. Omit to write the current resource. A supplied stale value is `revision_conflict`: refetch, reapply, retry. |
| `--note TEXT` | string | no | none | A note for the person who reviews the change: plain text, at most 280 characters. Ignored when the change applies at once. |
| `--no-wait` | boolean | no | off | Return after assignment without waiting for the Player to show the playlist. |
| `--poll-ms MS` | integer | no | 2000 | Interval between screen polls while waiting. |
| `--timeout MS` | integer | no | 120000 | Deadline for the wait. It also bounds each underlying request. |
| `--no-progress` | boolean | no | off | Suppress the stage lines on stderr. |

```console
screenrig screen publish scr_LOBBY lobby.json
```

This creates a new playlist and assigns it. It does not update an existing playlist by name. Inspect the prepared document first. Repeat identical input to resume an ambiguous failure. Assignment readback does not prove playback; request a screenshot separately. Under review it returns `stage: pending_review` with both reviews. See [Prepare, publish, and edit playlists](https://screenrig.ai/docs/cli.md#playlist-workflows).

After assignment the command waits until the Player shows the playlist: `data.stage` is `playing` and `data.playback_verified` is `true`. Stage changes print to stderr as they happen, as JSON lines or, with `--human`, as text. Repeating the identical command after a publish finished reports that publish (`data.already_published`) while the screen still shows its playlist, and publishes afresh once the screen has moved on.

### `screen assign`

Point a screen, or a fleet of screens, at a playlist.

```console
screen assign (<id>... | --tag TAG | --folder ID) --playlist-id ID [--expect-rev REVISION] [--note TEXT]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>...` | string | one of `<id>`, `--tag` or `--folder` | none | One or more `scr_` screen ids. Several ids run as one fleet request. |
| `--tag TAG` | string | one of `<id>`, `--tag` or `--folder` | none | Every active screen carrying this tag. |
| `--folder ID` | string | one of `<id>`, `--tag` or `--folder` | none | Every active screen in this `sfd_` screen folder and the folders inside it. |
| `--playlist-id ID` | string | yes | none | The `pl_` playlist to run. |
| `--expect-rev REVISION` | string | no | none | One screen id only. Optional revision precondition. Omit to write the current resource. A supplied stale value is `revision_conflict`: refetch, reapply, retry. |
| `--note TEXT` | string | no | none | A note for the person who reviews the change: plain text, at most 280 characters. Ignored when the change applies at once. |

```console
screenrig screen assign scr_01EXAMPLESCREEN00000000 --playlist-id pl_01EXAMPLEPLAYLIST000000
screenrig screen assign --tag Lobby --playlist-id pl_01EXAMPLEPLAYLIST000000
screenrig screen assign --folder sfd_01EXAMPLEFOLDER000000000 --playlist-id pl_01EXAMPLEPLAYLIST000000
```

A playlist that schedules pages assigns to any screen: the schedules are read in the screen's own timezone (its override, else the zone its device reports, else UTC).

To publish to a fleet, create the playlist once with `playlist create`, then run `screen assign --tag TAG --playlist-id ID`. `screen publish` targets one screen.

Under Propose access, or in a project that requires review for agent changes, the change to one screen or to the whole fleet answers `pending_review` instead: `data.review` carries the review id and its dashboard `url`, a `pending_review` warning names the url, and `data.next` is `reviews show ID`. Nothing changes on glass until a person approves it, so do not repeat the write. On a screen in a protected folder this answers `pending_review` at every access level, and an approver other than you and other than the person you work for decides it (`second_approver_required`, exit 3, otherwise). A caller limited to screen folders assigns only on screens of its scope. A saved assignment does not confirm playback; `screen screenshot` shows the glass.

### `screen set-timezone`

Override the time zone a screen's civil rules are read in, or remove the override.

```console
screen set-timezone <id> (--timezone ZONE | --clear) [--expect-rev REVISION]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `scr_` screen id. |
| `--timezone ZONE` | string | one of `--timezone` or `--clear` | none | Overrides the screen's time zone with an IANA zone identifier such as `Europe/Berlin`. |
| `--clear` | boolean | one of `--timezone` or `--clear` | off | Removes the override, so the screen follows the zone its device reports again. Not with `--timezone`. |
| `--expect-rev REVISION` | string | no | none | Optional revision precondition. Omit to write the current resource. A supplied stale value is `revision_conflict`: refetch, reapply, retry. |

```console
screenrig screen set-timezone scr_01EXAMPLESCREEN00000000 --timezone America/Los_Angeles
screenrig screen set-timezone scr_01EXAMPLESCREEN00000000 --clear
```

Every screen has an effective timezone, and every civil rule reads it: page visibility, ad slots, playlist schedules, takeovers, campaigns, display and reboot schedules, and `{screen.timezone}`. It is this override, else the zone the screen's Player reports from its device, else `UTC`. A screen needs no override while its device reports the right zone. `screen show` prints the effective zone as `Timezone` with where it comes from, `effective_timezone` and `timezone_source` (`screen`, `device` or `default`) in `--json`. Changing the effective zone remints the manifest revision.

`ZONE` is `Area/City`, such as `America/New_York` or `Asia/Tokyo`, or `UTC`.

### `screen wall set`

Set the video wall of a screen: one Player driving two to twelve displays as one canvas.

```console
screen wall set <id> --outputs NAMES --arrangement row|column|grid [--columns N] [--rotation 0|90|180|270] [--expect-rev REVISION]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `scr_` screen id. |
| `--outputs NAMES` | string | yes | none | Two to twelve comma-separated output names in physical order, left to right then top to bottom, as `screen show` lists them under `observation.surfaces`, such as `HDMI-A-1,HDMI-A-2`. Each is 1 to 64 letters, digits, dots, underscores or dashes, listed once. |
| `--arrangement ARRANGEMENT` | string | yes | none | `row` (one row, left to right), `column` (one column, top to bottom) or `grid` (rows of `--columns` outputs, top to bottom). |
| `--columns N` | integer | yes, for `grid` | none | Outputs per row of a grid: 2 to 6, dividing the outputs into at least 2 rows. Refused for `row` and `column`. |
| `--rotation DEGREES` | string | no | `0` | Clockwise rotation of every output: `0`, `90`, `180` or `270`. `90` and `270` are portrait panels. |
| `--expect-rev REVISION` | string | no | none | Optional revision precondition. Omit to write the current resource. A supplied stale value is `revision_conflict`: refetch, reapply, retry. |

```console
screenrig screen wall set scr_01EXAMPLESCREEN00000000 --outputs HDMI-A-1,HDMI-A-2 --arrangement row
screenrig screen wall set scr_01EXAMPLESCREEN00000000 --outputs DP-1,DP-2,DP-3 --arrangement row --rotation 90 --expect-rev 4
screenrig screen wall set scr_01EXAMPLESCREEN00000000 --outputs DP-1,DP-2,DP-3,DP-4,DP-5,DP-6 --arrangement grid --columns 3
```

One Player drives the displays as one wall, so a playlist page spans all of them. The set replaces the whole wall, so repeat `--rotation` and `--columns` to keep them. Only the Linux Player's kiosk install and the Windows Player declare the `display.wall` capability; any other Player answers `wall_unsupported` (exit 5) and nothing changes. Propose access cannot change a wall (`insufficient_access`). A wall of 2 or 3 displays needs Plus or above and a wall of 4 to 12 displays Enterprise: a plan without it answers `feature_not_in_plan` (exit 8), while `screen wall clear` and a set that keeps the stored wall stay open. See [Video walls](https://screenrig.ai/docs/players.md#video-walls).

Author the playlist for the wall's shape with `playlist init` and the screen's id, which writes the matching canvas and `format` (`displays`, `arrangement`, `orientation`, and `columns` for a grid). A primitive's `displays`, such as `[2]` or `[1, 2, 5, 6]`, name the display numbers it covers, counted from 1 left to right then top to bottom, and must fill a rectangle of the wall. Assigning a playlist whose format is not the wall's shape is `playlist_format_mismatch` (exit 5); a playlist without a format plays the same on every display. Once the screen plays a playlist with a matching format, every display shows its number and name for 20 seconds after a change, so you can check the order on glass.

### `screen wall clear`

Remove a screen's video wall.

```console
screen wall clear <id> [--expect-rev REVISION]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `scr_` screen id. |
| `--expect-rev REVISION` | string | no | none | Optional revision precondition. Omit to write the current resource. A supplied stale value is `revision_conflict`: refetch, reapply, retry. |

```console
screenrig screen wall clear scr_01EXAMPLESCREEN00000000
```

The assigned playlist is not re-checked: if it has a `format`, the screen's manifest degrades (`wall_format_mismatch`) and the Player keeps its last good page until you assign a playlist without a format. A screen without a wall answers unchanged. Propose access cannot change a wall (`insufficient_access`).

### `screen archive`

Hide a screen and darken its glass.

```console
screen archive <id> [--expect-rev REVISION]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `scr_` screen id. |
| `--expect-rev REVISION` | string | no | none | Optional revision precondition. Omit to write the current resource. A supplied stale value is `revision_conflict`: refetch, reapply, retry. |

```console
screenrig screen archive scr_01EXAMPLESCREEN00000000
```

Archiving hides the screen from the default listing and darkens the glass. It does not unbind the Player.

Whoever holds the display's key can still connect while the screen is archived. To retire hardware for good, leave the screen archived, or move the screen to new hardware with [`screen recover`](https://screenrig.ai/docs/cli/screens.md#cmd-screen-recover) or [`screen replace`](https://screenrig.ai/docs/cli/screens.md#cmd-screen-replace), which retire the old key.

### `screen unarchive`

Restore an archived screen to the default listing.

```console
screen unarchive <id> [--expect-rev REVISION]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `scr_` screen id. |
| `--expect-rev REVISION` | string | no | none | Optional revision precondition. Omit to write the current resource. A supplied stale value is `revision_conflict`: refetch, reapply, retry. |

```console
screenrig screen unarchive scr_01EXAMPLESCREEN00000000
```

A Player can also archive its own screen: a confirmed reset on a Player that holds a key, or unpairing a browser Player paired by cookie. `screen show` then reports `archive_reason` as `device_reset` or `device_unpair` (`project` for an archive you request), with `archived_at`. The screen keeps its device binding for every reason, so a Player that still holds that key or device cookie stays dark and resumes on unarchive with no re-pairing. The Player that was reset or unpaired drops its key or cookie and pairs again as a new screen. A key replaced by a confirmed screen recovery stays retired.

A screen archived as `replaced` by `screen replace` keeps no device binding, so unarchive refuses it and `screen delete` removes it.

### `screen suspend`

Show a notice on a screen instead of its content, until it is resumed.

```console
screen suspend <id> --message TEXT [--expect-rev REVISION]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `scr_` screen id. |
| `--message TEXT` | string | yes | none | The notice the screen shows: 1 to 280 characters after trimming. Line feed is the only control character, and the Player wraps the text and keeps its line feeds. |
| `--expect-rev REVISION` | string | no | none | Optional revision precondition. Omit to write the current resource. A supplied stale value is `revision_conflict`: refetch, reapply, retry. |

```console
screenrig screen suspend scr_01EXAMPLESCREEN00000000 --message "Closed for stocktake. Back at 9am."
```

A suspended screen shows the message in place of its content. Nothing else changes: its playlist, schedule, takeover, and display control stay and keep evolving, and `screen resume` shows what the screen would be showing now. `screen show` reports `suspension` with the `message` and `since`; suspending a suspended screen replaces the message and keeps `since`. Suspending bumps the screen revision, and the Player refetches at once. An archived screen answers `screen_archived`.

### `screen resume`

End a screen's suspension so it shows its own content again.

```console
screen resume <id> [--expect-rev REVISION]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `scr_` screen id. |
| `--expect-rev REVISION` | string | no | none | Optional revision precondition. Omit to write the current resource. A supplied stale value is `revision_conflict`: refetch, reapply, retry. |

```console
screenrig screen resume scr_01EXAMPLESCREEN00000000
```

Resume shows the screen's own content again, from what the screen would be showing now, and the Player refetches at once. A screen that is not suspended answers unchanged, with no revision change. An archived screen answers `screen_archived`.

### `screen delete`

Delete a screen.

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

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `scr_` screen id. |
| `--expect-rev REVISION` | string | no | none | Optional revision precondition. Omit to write the current resource. A supplied stale value is `revision_conflict`: refetch, reapply, retry. |

```console
screenrig screen delete scr_01EXAMPLESCREEN00000000
```

Delete removes a screen only while no Player key is bound to it, and `screen show` reports `deletable: true` for that screen. A Player that pairs by code, a native Player or the browser Player, holds a key, and the key stays bound to its screen, so deleting that screen answers 409 `screen_archive_required` (exit code 5): archive it instead. Only a browser Player paired by cookie holds no key, and its screen deletes whether it is paired or still pending.

To pair a display that holds a key as a new screen, confirm a reset on the Player itself, then claim the code it shows with `screen pair`; the old screen stays archived. To move a screen to replacement hardware, use [`screen recover`](https://screenrig.ai/docs/cli/screens.md#cmd-screen-recover), or `screen replace` when that hardware is already paired as its own screen.

### `screen recover`

Reconnect a display that lost its stored identity to the screen it was paired as.

```console
screen recover <id> [--expect-rev REVISION]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `scr_` screen id. |
| `--expect-rev REVISION` | string | no | none | Optional revision precondition. Omit to write the current resource. A supplied stale value is `revision_conflict`: refetch, reapply, retry. |

```console
screenrig screen show scr_01EXAMPLESCREEN00000000
screenrig screen recover scr_01EXAMPLESCREEN00000000
```

A display that lost its stored identity and now reports this screen's hardware identifiers while pairing makes an offer. `screen show` reports it as `recovery_pending` with its deadline and, when the display reports them, its `platform`, `model`, `firmware` and `manufacturer`. Nothing is rebound until this command confirms it: the screen keeps its label, playlist, timezone, schedules, and history, and the display's new key replaces the previous one, which retires after a fifteen-minute grace window. A screen archived by a device reset stays archived until `screen unarchive`.

Compare the reported model and firmware with the display you expect before confirming: any application running on a display can read its identifiers, so the offer alone does not prove which display is asking. The service refuses an offer while the screen's current Player is still online, and a screen accepts at most three offers an hour. Without an offer the command answers `recovery_not_offered`, an offer whose pairing session lapsed answers `recovery_expired`, and identifiers attached to more than one screen answer `recovery_ambiguous`; nothing is rebound. Recovery never crosses projects.

### `screen replace`

Move the device paired as another screen onto this screen, keeping this screen's settings and history.

```console
screen replace <id> --with ID [--expect-rev REVISION]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `scr_` screen to keep. |
| `--with ID` | string | yes | none | The `scr_` screen whose paired device takes over. It must be another screen of the project. |
| `--expect-rev REVISION` | string | no | none | Optional revision precondition on the screen to keep. Omit to write the current resource. A supplied stale value is `revision_conflict`: refetch, reapply, retry. |

```console
screenrig screen replace scr_01EXAMPLESCREEN00000000 --with scr_01EXAMPLENEWSCREEN000000
```

Pair the new hardware as its own screen first, then replace. The device's key, or a browser Player's credential, now opens `<id>`, which keeps its name, tags, timezone, playlist, schedules, takeover, display schedule and override, video wall, comments, fields, location, and history, and takes the replacement's host and Player. Its health, status, storage, and queued commands start over. The screen named by `--with` is archived with `archive_reason` `replaced` and `replaced_by` naming `<id>`. It keeps no device binding, so `screen unarchive` cannot restore it and `screen delete` removes it. The key `<id>` held retires after a fifteen-minute grace window, then its old device is refused. Both screens must be active with a paired Player: a screen still waiting to pair answers `resource_conflict`, an archived one answers `screen_archived`, and a video wall needs a replacement Player that declares `display.wall` (`wall_unsupported`). The request always carries an idempotency key, and an exact retry returns the original answer.

To move a display that lost its stored identity and now reports this screen's identifiers, use [`screen recover`](https://screenrig.ai/docs/cli/screens.md#cmd-screen-recover).

### `screen rotate-public-id`

Issue a new public id for a screen.

```console
screen rotate-public-id <id> [--expect-rev REVISION]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `scr_` screen id. |
| `--expect-rev REVISION` | string | no | none | Optional revision precondition. Omit to write the current resource. A supplied stale value is `revision_conflict`: refetch, reapply, retry. |

```console
screenrig screen rotate-public-id scr_01EXAMPLESCREEN00000000
```

The screen's `public_id`, the credential in its public URL, is replaced and every session opened with the old one ends. The Player's pairing is kept: a paired Player opens a new session and keeps showing its content.

### `screen link create`

Create a screen's link, or replace it, and print its Player URL.

```console
screen link create <id> [--expect-rev REVISION]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `scr_` screen id. The screen is active: its Player has paired once with its code. |
| `--expect-rev REVISION` | string | no | none | Optional revision precondition. Omit to write the current resource. A supplied stale value is `revision_conflict`: refetch, reapply, retry. |

```console
screenrig screen link create scr_01EXAMPLESCREEN00000000
```

Prints `data.url`, the Player URL (`https://play.screenrig.ai/player/#screen=…`) to paste as the URL of a browser source in vMix or OBS, which keeps no storage and would otherwise lose its pairing on every load. The Player runs from the URL on every load: the URL is the whole pairing. Creating the link again replaces it (`data.replaced` is `true`): the old URL stops working at once and the Player opened from it shows a notice with no content. The URL is a credential that opens the screen: it is printed under a sensitive label, kept out of the operation log, and shown again by `screen link show`. Open it in one place. A screen that has not paired answers `resource_conflict` (exit code 5), and an archived screen answers `screen_archived`. After an interrupted request, rerun the identical command: it reuses the saved idempotency key and returns the same link.

### `screen link show`

Print a screen's link and its Player URL.

```console
screen link show <id>
```

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

```console
screenrig screen link show scr_01EXAMPLESCREEN00000000
```

Prints `data.url` under a sensitive label with `data.hint` (the last four characters of the secret), `data.created_at` and `data.last_used_at` once a Player has opened the link. A Read only credential receives `url` empty with `url_redacted` true, and human output says the URL is withheld; a Manage sign-in receives it. A screen without a link answers `screen_link_not_found` (exit code 4) and names the command that creates one. `screen show` prints `Screen link: on (…HINT)` for a screen that has one, and never its URL.

### `screen link delete`

Turn a screen's link off.

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

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `scr_` screen id. |
| `--expect-rev REVISION` | string | no | none | Optional revision precondition. Omit to write the current resource. A supplied stale value is `revision_conflict`: refetch, reapply, retry. |

```console
screenrig screen link delete scr_01EXAMPLESCREEN00000000
```

The URL stops working at once and every session it opened ends: a browser source still showing it shows a notice and no content until it is given a new URL. The screen, its pairing and its content are untouched. It succeeds, changing nothing, when the screen has no link.

### `screen reload`

Ask a screen's Player, or every Player in a fleet, to reload once.

```console
screen reload (<id>... | --tag TAG | --folder ID) [--expires-in DURATION] [--expect-rev REVISION]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>...` | string | one of `<id>`, `--tag` or `--folder` | none | One or more `scr_` screen ids. Several ids run as one fleet request. |
| `--tag TAG` | string | one of `<id>`, `--tag` or `--folder` | none | Every active screen carrying this tag. |
| `--folder ID` | string | one of `<id>`, `--tag` or `--folder` | none | Every active screen in this `sfd_` screen folder and the folders inside it. |
| `--expires-in DURATION` | string | no | `24h`, applied by the server | How long the request waits for an offline screen to come online: `90s`, `30m`, `2h`, or `1d`, from `60s` to `24h`. |
| `--expect-rev REVISION` | string | no | none | One screen id only. Optional revision precondition. |

```console
screenrig screen reload scr_01EXAMPLESCREEN00000000
screenrig screen reload scr_01EXAMPLESCREEN00000000 --expires-in 2h
screenrig screen reload --tag Lobby
screenrig screen reload --folder sfd_01EXAMPLEFOLDER000000000
```

The answer carries a `reload_id`, a `state`, and an `expires_at`. A screen that is online is sent the reload at once (`state` `sent`); an offline screen holds it `queued` and is sent it when it comes online before `expires_at`, 24 hours after the request unless `--expires-in` says otherwise. A newer reload supersedes one still queued, `screen show` lists the screen's `commands`, and `screen command cancel` cancels one still queued. In a fleet, each result's `reload` carries the same `state`. A web Player reloads at its next page boundary; a native Player reconnects, refetches its manifest, and checks for an update. A Player ignores a reload within ten minutes of the last one it acted on. Reload works on active and archived screens and does not change the screen revision.

### `screen toast`

Post one transient message as stage chrome on a live screen or a fleet.

```console
screen toast (<id>... | --tag TAG | --folder ID) --text TEXT [--level info|alert|error] [--duration-ms MS]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>...` | string | one of `<id>`, `--tag` or `--folder` | none | One or more `scr_` screen ids. Several ids run as one fleet request. |
| `--tag TAG` | string | one of `<id>`, `--tag` or `--folder` | none | Every active screen carrying this tag. |
| `--folder ID` | string | one of `<id>`, `--tag` or `--folder` | none | Every active screen in this `sfd_` screen folder and the folders inside it. |
| `--text TEXT` | string | yes | none | One to 120 characters, line feed as the only break, at most three lines. |
| `--level LEVEL` | string | no | `info` | `info`, `alert`, or `error`. A screen can set the lowest toast level it shows, and `error` toasts always show. A Player shows a toast on one line, a line feed as a space. Command success does not establish visibility. |
| `--duration-ms MS` | integer | no | 60000, applied by the server | Between 2000 and 60000 when supplied. |

```console
screenrig screen toast scr_01EXAMPLESCREEN00000000 --text "Lobby closed"
screenrig screen toast --tag Lobby --text "Closing in ten minutes"
screenrig screen toast --folder sfd_01EXAMPLEFOLDER000000000 --text "Closing in ten minutes"
```

A toast is not a primitive: it occupies no canvas slot, has no layer, and takes no part in readiness or crossfade. Latest wins, there is no queue and no cancel, and the accepted envelope is `{ expires_at }` only. Never put credentials or other secret material in the text.

### `screen identify`

Show a screen's name over its content for a while, to find it on site.

```console
screen identify <id> [--duration DURATION]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `scr_` screen id. |
| `--duration DURATION` | string | no | `60s`, applied by the server | How long the overlay stays up, counted from when the Player receives the request: `30s`, `2m`, or `1m30s`, from `2s` to `10m`. |

```console
screenrig screen identify scr_01EXAMPLESCREEN00000000
screenrig screen identify scr_01EXAMPLESCREEN00000000 --duration 2m
```

The Player draws a full-screen overlay above its content, with the screen's name very large and its id small below it. Playback continues underneath, and a newer identify replaces the overlay. The accepted envelope carries `identify_id`, `duration_ms`, and an `expires_at` 60 seconds after the request, so a screen that is offline never shows it later. Only a Player that declares the `identify` capability shows the overlay: any other screen answers 409 `identify_unsupported` (exit code 5) and nothing is sent, so update the Player or show the name with `screen toast`. An archived screen answers `screen_archived` and a screen still waiting to pair answers `resource_conflict`. Identify does not change the screen revision.

### `screen play-page`

Start a page of the playlist a screen is playing now, to check that page with a screenshot.

```console
screen play-page <id> --page-id ID
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `scr_` screen id. |
| `--page-id ID` | string | yes | none | A page of the playlist the screen is playing now: 1 to 128 characters. |

```console
screenrig screen play-page scr_01EXAMPLESCREEN00000000 --page-id welcome
screenrig screen screenshot scr_01EXAMPLESCREEN00000000
```

The page starts from its beginning on the screen's Player and the rotation continues from there, so a screenshot taken next shows that page without waiting out the rotation. The page must belong to the playlist the screen plays now: its takeover, else a campaign that holds, else its schedule, else its assigned playlist. Any other page answers `page_not_on_screen` (exit 5), and the problem names that playlist. A Player that does not declare the `play_page` capability in its host report answers `play_page_unsupported` (exit 5) and nothing is sent: update the Player.

The answer carries `playlist_id`, `page_id`, `manifest_revision` and `expires_at`. The request expires after 60 seconds and does not change the screen revision. An archived screen answers `screen_archived` and a screen still waiting to pair answers `resource_conflict`. A retry with the same idempotency key replays the original answer.

### `screen screenshot`

Capture one still of an active screen, or of every screen in a fleet, and write it to disk.

```console
screen screenshot <id> [--output FILE] [--timeout MS] [--poll-ms MS]
screen screenshot (<id> <id>... | --tag TAG | --folder ID) [--output DIRECTORY] [--concurrency N] [--timeout MS] [--poll-ms MS]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>...` | string | one of `<id>`, `--tag` or `--folder` | none | One or more `scr_` screen ids. |
| `--tag TAG` | string | one of `<id>`, `--tag` or `--folder` | none | Every active screen carrying this tag, with no screen-count cap. |
| `--folder ID` | string | one of `<id>`, `--tag` or `--folder` | none | Every active screen in this `sfd_` screen folder and the folders inside it, with no screen-count cap. |
| `--output FILE` | path | no | `./<id>.webp` | For one screen, a file path; an existing file is overwritten without a prompt. For several ids, `--tag` or `--folder`, a directory, created if missing, that receives one `<screen_id>.webp` per screen. |
| `--concurrency N` | integer | no | 4 | Captures in flight at once for several ids, `--tag` or `--folder`, from 1 to 8. |
| `--timeout MS` | integer | no | 35000 | How long to wait for the capture. |
| `--poll-ms MS` | integer | no | 500 | Interval between capture-status polls. |

```console
screenrig screen screenshot scr_01EXAMPLESCREEN00000000 --output ./lobby.webp
```

```console
screenrig screen screenshot --tag Lobby --output ./lobby-shots
```

```console
screenrig screen screenshot --folder sfd_01EXAMPLEFOLDER000000000 --output ./north-shots
```

The command blocks until the WebP is on disk. The envelope reports `screen_id`, `capture_id`, `path`, `bytes`, `sha256`, `width`, and `height`, and never image bytes. Latest wins: a later request replaces an in-flight capture. A screenshot of the Player web app draws each `iframe` page as a dark grey tile labelled "Embedded web page" and each application as the application renders itself; the image carries no marker for the tile, so it does not prove that an iframe page rendered.

For several ids, `--tag` or `--folder`, the answer is a [fleet result](https://screenrig.ai/docs/cli/screens.md#fleet-results) with `action: "screenshot"`, the `selector`, and the `output` directory; each `ok` result carries `path`, `bytes`, `sha256`, `width`, and `height`. With `--tag`, the CLI first lists the tagged screens, following every list page; each list page is one agent request and the captures themselves are not counted, and several ids need no list. `--idempotency-key` is refused in this form, because each capture uses its own key. If a capture fails unexpectedly, that screen's result is `unexpected_error`, no new captures start, the screens still waiting report `not_attempted`, and the exit code is 1.

For one screen the envelope also carries `frame.flat`, whether this one image is a single flat tone, and `picture`, the screen's blank or frozen verdict when it has one. A flat image alone is no verdict: a transition between pages, or a page that is black on purpose, looks the same. With `--folder`, the CLI lists the folder and every folder inside it, one `screen list --folder` per folder, and a screen the list shows offline is reported failed without a capture.

The server admits 10 captures per screen, 30 per organization and 30 per address a minute; the CLI retries `429` and `5xx` answers with backoff and honors `Retry-After`. A capture that cannot finish is `screenshot_unavailable` (exit 5): the screen is offline, the request `timed_out`, the page shown holds a surface the Player cannot capture (`unsupported_surface`, such as a visible third-party iframe in the browser Player), the Player's capture failed (`capture_failed`), or the capture was still pending when `--timeout` ran out. `--timeout 0` leaves the wait to the server, which expires an unanswered request after 30 seconds. A failed capture carries `error.display` when the Player has reported its display: `power`, `connected`, `reported_at` and `stale` (after 35 minutes). A fresh `off` or `standby` report suggests retrying when the display is on.

### `screen live start`

Start live view of a screen, or renew it, and print the dashboard page that shows it.

```console
screen live start <id> [--duration DURATION] [--wait DURATION]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `scr_` screen id. |
| `--duration DURATION` | string | no | `5m` | How long the session lasts unless renewed: `30s` to `5m`, such as `90s` or `5m`. A session ends five minutes after it began at the latest. |
| `--wait DURATION` | string | no | `30s` | How long to wait for the Player to start publishing: `1s` to `10m`, or `0` to return at once. |

```console
screenrig screen live start scr_01EXAMPLESCREEN00000000
screenrig screen live start scr_01EXAMPLESCREEN00000000 --duration 2m --wait 60s
```

Live view shows what a screen plays as video in the dashboard; the CLI starts, reads and stops the session and never carries the video. `data` is the session with `viewer_url`, the page to open. Starting a screen that has a session renews it: the lease becomes the later of its current end and `--duration` from now, never past five minutes after the session began. Then the session ends by itself: nothing renews or restarts it, and the next session needs a new `screen live start`. It ends earlier when the lease passes, on `screen live stop`, or when the Player ends it.

The command asks the Player to publish and reads the session every second for `--wait` until the Player reports connected. If the Player has not connected when the wait is over, the answer is still ok with the warning `live_not_ready`: read `screen live status` again, or open `viewer_url`. A session that ended while waiting is `live_unavailable` (exit 5) and its detail names `ended_reason`, such as `consent_timeout` (on Android, someone at the screen allows screen capture once each time the Player starts). An offline screen is `live_unavailable` too, and an archived one answers `screen_archived`. A Player that does not declare the `live_view` capability answers `live_unsupported` (exit 5) and nothing is sent: update the Player. A server without a media relay answers `feature_unavailable`: use `screen screenshot`. Live view is free. See [Live view](https://screenrig.ai/docs/players.md#live-view).

### `screen live status`

Read a screen's live view session.

```console
screen live status <id>
```

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

```console
screenrig screen live status scr_01EXAMPLESCREEN00000000
```

`state` is `idle` (no session), `requested` (the Player was asked), `publishing` (the relay holds its video) or `ended`, with `ended_reason` for ten minutes. `publisher.connected` is the Player's own report, at most 45 seconds old, and is not proof of a picture. `data` includes `viewer_url`.

### `screen live stop`

End a screen's live view session.

```console
screen live stop <id>
```

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

```console
screenrig screen live stop scr_01EXAMPLESCREEN00000000
```

Ends the session and tells the Player to stop publishing, then prints the session as it stands: `ended` with `ended_reason` `stopped`. It succeeds when there is no session. A session also ends by itself when its lease passes.

### `screen live frame`

Save JPEG frames of what a screen plays, from its live stream, to check playback and motion.

```console
screen live frame <id> [--output PATH] [--count N] [--interval DURATION] [--duration DURATION] [--wait DURATION] [--stop]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `scr_` screen id. |
| `--output PATH` | path | no | the working directory | A file for one frame, or a directory that receives `frame-<captured_at>.jpg` per frame, with the colons of the time written as hyphens. `--count` above 1 needs a directory. |
| `--count N` | integer | no | 1 | How many new frames to save. |
| `--interval DURATION` | string | no | `1s` | Pause between frames: `1s` to `5m`, or `0` to take each frame as it arrives. |
| `--duration DURATION` | string | no | `2m` | Lease of the live session this command starts: `30s` to `5m`. It is lengthened to cover the frames plus 30 seconds, and a run that needs more than five minutes is refused. |
| `--wait DURATION` | string | no | `30s` | How long to wait for the Player to start publishing, and for each frame: `1s` to `10m`, or `0` to try once. |
| `--stop` | boolean | no | off | End the live session when done, also when a frame failed. |

```console
screenrig screen live frame scr_01EXAMPLESCREEN00000000
screenrig screen live frame scr_01EXAMPLESCREEN00000000 --output now.jpg --stop
screenrig screen live frame scr_01EXAMPLESCREEN00000000 --output frames/ --count 5 --interval 1s
```

The command starts the live session, waits for the Player to publish, asks the media relay for frames, and reads each new frame once. A frame is the video as the Player's encoder sent it, scaled to at most 1280x720 and lossy, about one a second: it shows motion and what is on glass now, so compare frames (`--count 5 --interval 1s`) to check that content moves. It is not a screenshot, which stays the layout proof. Frames are free.

`data` is `{screen_id, live_id, frames}`, with `path`, `captured_at`, `width`, `height`, `bytes` and `sha256` per frame, and `stopped` `true` when `--stop` ended the session. Without `--stop` the session is left to its lease. A Player that does not publish within `--wait`, or sends no new frame within it, is `live_not_ready` (exit 5); on Android, someone at the screen allows screen capture once each time the Player starts. A session that ended, or whose frames stopped arriving, is `live_unavailable` (exit 5). A Player without the `live_view` capability answers `live_unsupported`, and a server without a media relay or frames answers `feature_unavailable` (exit 4): use `screen screenshot`.

### `screen tag`

Set, add, remove, or clear the tags on a screen or a fleet.

```console
screen tag (<id>... | --tag TAG | --folder ID) (--set TAGS | --add TAGS | --remove TAGS | --clear) [--expect-rev REVISION]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>...` | string | one of `<id>`, `--tag` or `--folder` | none | One or more `scr_` screen ids. Several ids run as one fleet request. |
| `--tag TAG` | string | one of `<id>`, `--tag` or `--folder` | none | Every active screen carrying this tag. |
| `--folder ID` | string | one of `<id>`, `--tag` or `--folder` | none | Every active screen in this `sfd_` screen folder and the folders inside it. |
| `--set TAGS` | string | exactly one action | none | Replace the whole set with these comma-separated tags, such as `Lobby,Floor2`. |
| `--add TAGS` | string | exactly one action | none | Add these comma-separated tags. |
| `--remove TAGS` | string | exactly one action | none | Remove these comma-separated tags. |
| `--clear` | boolean | exactly one action | off | Remove every tag. |
| `--expect-rev REVISION` | string | no | none | One screen id only. Optional revision precondition. |

```console
screenrig screen tag scr_01EXAMPLESCREEN00000000 --set Lobby,Floor2
screenrig screen tag scr_01EXAMPLESCREEN00000000 --add Spring
screenrig screen tag --tag Lobby --add Spring
screenrig screen tag --folder sfd_01EXAMPLEFOLDER000000000 --add Spring
```

A tag is 1 to 32 letters or digits, and a screen carries at most 16 unique tags. Tags select fleets; they are never authorization and never reach the Player. Changing tags bumps the screen revision, not the manifest revision. For one screen the envelope is the updated screen. `--add` and `--remove` read the screen first and write the new set guarded by `--expect-rev` or the revision just read, so a concurrent change returns `revision_conflict` (exit 6) instead of being overwritten; rerun the command. In a fleet, `--add` and `--remove` apply against each screen's stored set atomically, and a screen that would pass 16 tags fails in its own result with `invalid_request`.

Wherever tags change (`screen tag`, a screen update through the API, the dashboard tag editor), the answer's `levels` lists, under `gained` and `lost`, the campaigns whose tag audience the screen entered or left, each with its `source` (`campaign`), `id`, the `screens` count and, when the campaign has a [reach](https://screenrig.ai/docs/cli/campaigns.md#cmd-campaign-show), `reach_before` and `reach_after`. A fleet `screen tag` sums them over the fleet. Read `levels.lost` before calling a tag edit harmless: removing the last matching tag from a campaign's screens empties that group and raises the `reach_dropped` and `group_empty` [notifications](https://screenrig.ai/docs/cli/notifications.md#notifications).

### `screen move`

Move screens and screen folders into a screen folder, or back to the project root.

```console
screen move <id>... (--folder ID | --root) [--note TEXT]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>...` | string | yes | none | `scr_` screen ids and `sfd_` screen folder ids in any mix, each once, at most 1000 in all. |
| `--folder ID` | string | one of `--folder` or `--root` | none | The destination `sfd_` screen folder. |
| `--root` | boolean | one of `--folder` or `--root` | off | Move to the project root, outside every folder. |
| `--note TEXT` | string | no | none | A note for the person who reviews the change: plain text, at most 280 characters. Ignored when the change applies at once. |

```console
screenrig screen move scr_01EXAMPLESCREEN00000000 --folder sfd_01EXAMPLEFOLDER000000000
screenrig screen move sfd_01EXAMPLEFOLDER000000000 --root
```

The move is one all-or-nothing request. A folder moves with everything inside it and cannot move into itself or a folder inside it; folders nest at most 10 deep. Items already in the destination stay as they are and are not counted. `data.screens` and `data.folders` count what moved. `data.levels` lists, under `gained` and `lost`, the campaigns whose folder audience the moved screens entered or left (`source` `campaign`, with `reach_before` and `reach_after`) and the folder default playlists they started or stopped showing (`source` `folder`). The screens re-resolve their playlist within about 15 seconds; `screen show` reports `effective_playlist`, and `campaign explain SCREEN_ID` says why.

A caller limited to screen folders moves only screens and folders inside its scope into a folder inside it; anything else, including the project root, refuses the whole move with `outside_screen_scope` (exit 3). A move into or out of a protected folder answers `pending_review` with a `protected_change` review that a second approver decides, so do not repeat the write; `--note` tells the approver why. After an interrupted request, rerun the identical command to reuse the saved idempotency key.

### `screen folder list`

List the project's screen folders as a tree, with the live screens directly in each and its default playlist.

```console
screen folder list
```

```console
screenrig screen folder list
```

Returns every screen folder of the project, at most 1000, ordered by name. `data.items` carries each folder's `id`, `name`, `parent_id` (`null` at the project root), `default_playlist_id` when set, `protected` when set, `screens` (the live screens directly inside, not those of the folders below) and `revision`. With `--human`, the output is the tree, one row per folder with its path. A screen's folder is `folder_id` on `screen list` and `screen show`, absent at the project root. Read only access is enough.

### `screen folder show`

Inspect a screen folder.

```console
screen folder show <id>
```

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

```console
screenrig screen folder show sfd_01EXAMPLEFOLDER000000000
```

Returns one folder with the fields `screen folder list` returns. Read only access is enough.

### `screen folder create`

Create a screen folder at the project root or inside another folder.

```console
screen folder create --name NAME [--parent ID] [--default-playlist-id ID] [--note TEXT]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `--name NAME` | string | yes | none | Folder name, trimmed: 1 to 100 characters, no control characters, not `.` or `..`, and unique among its siblings (`resource_conflict`, exit 5). |
| `--parent ID` | string | no | project root | Create it inside this `sfd_` screen folder. |
| `--default-playlist-id ID` | string | no | none | The approved `pl_` playlist its screens show when they have none of their own, unless a folder nearer the screen sets one (`playlist_not_approved`, exit 5). |
| `--note TEXT` | string | no | none | A note for the person who reviews the change: plain text, at most 280 characters. Ignored when the change applies at once. |

```console
screenrig screen folder create --name "North Tower"
screenrig screen folder create --name "Level 2" --parent sfd_01EXAMPLEFOLDER000000000 --default-playlist-id pl_01EXAMPLEPLAYLIST000000
```

An organization holds at most 1000 folders across its projects (`quota_exceeded`), nested at most 10 deep (`invalid_request`). Put screens in with `screen move`. A folder then targets its screens in the fleet commands and as a campaign audience: see [Screen folders](https://screenrig.ai/docs/cli/screens.md#screen-folders).

A caller limited to screen folders creates folders only inside its scope: `--parent` must be a folder of it (`outside_screen_scope`, exit 3). A folder created inside a protected folder answers `pending_review` with a `protected_change` review that an approver other than you, and other than the person you work for, decides, so do not repeat the write; `--note` tells the approver why. After an interrupted request, rerun the identical command to reuse the saved idempotency key.

### `screen folder update`

Rename a screen folder, move it with everything inside, or set or clear its default playlist.

```console
screen folder update <id> [--name NAME] [--parent ID | --root] [--default-playlist-id ID | --clear-default] [--expect-rev REVISION] [--note TEXT]
screen folder update <id> --unprotect [--expect-rev REVISION] [--note TEXT]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `sfd_` screen folder id. |
| `--name NAME` | string | no | unchanged | New folder name, with the rules of `screen folder create`. |
| `--parent ID` | string | no | unchanged | Move it, with everything inside, under this `sfd_` screen folder. Not with `--root`. |
| `--root` | boolean | no | off | Move it to the project root. Not with `--parent`. |
| `--default-playlist-id ID` | string | no | unchanged | The approved `pl_` playlist its screens show when they have none of their own. Not with `--clear-default`. |
| `--clear-default` | boolean | no | off | Remove its default playlist. Not with `--default-playlist-id`. |
| `--unprotect` | boolean | no | off | Propose removing the folder's protection. It is sent alone, with none of the options above, and a second approver decides the review. |
| `--expect-rev REVISION` | string | no | none | Optional revision precondition. Omit to write the current resource. A supplied stale value is `revision_conflict`: refetch, reapply, retry. |
| `--note TEXT` | string | no | none | A note for the person who reviews the change: plain text, at most 280 characters. Ignored when the change applies at once. |

Provide at least one of `--name`, `--parent`, `--root`, `--default-playlist-id`, `--clear-default` or `--unprotect`.

```console
screenrig screen folder update sfd_01EXAMPLEFOLDER000000000 --name "Northern"
screenrig screen folder update sfd_01EXAMPLEFOLDER000000000 --root
screenrig screen folder update sfd_01EXAMPLEFOLDER000000000 --default-playlist-id pl_01EXAMPLEPLAYLIST000000 --expect-rev 3
screenrig screen folder update sfd_01EXAMPLEFOLDER000000000 --clear-default
screenrig screen folder update sfd_01EXAMPLEFOLDER000000000 --unprotect --note "Menu boards move next week"
```

A folder cannot move into itself or a folder inside it, and the same values are a no-op. A move or a default change re-resolves the screens inside within about 15 seconds, and `data.levels` lists the campaigns whose folder audience those screens entered or left (`source` `campaign`) and the folder default playlists they gained or lost (`source` `folder`).

A rename stays live on a protected folder. A move, or a default playlist, of a folder that is or holds a protected one answers `pending_review` with a `protected_change` review that a second approver decides (an approver other than you, and other than the person you work for; `second_approver_required`, exit 3, otherwise). Nothing changes until then, so do not repeat the write; `--note` tells the approver why. A person who can approve protects a folder in the dashboard; the CLI only proposes removing protection with `--unprotect`. See [Protected folders](https://screenrig.ai/docs/permissions.md#protected-folders). A caller limited to screen folders changes only folders inside its scope and moves them only to a parent inside it (`outside_screen_scope`, exit 3), and never the protection of a folder its scope names.

### `screen folder delete`

Delete a screen folder; its screens and folders move up one level.

```console
screen folder delete <id> [--expect-rev REVISION] [--note TEXT]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `sfd_` screen folder id. |
| `--expect-rev REVISION` | string | no | none | Optional revision precondition. Omit to write the current resource. A supplied stale value is `revision_conflict`: refetch, reapply, retry. |
| `--note TEXT` | string | no | none | A note for the person who reviews the change: plain text, at most 280 characters. Ignored when the change applies at once. |

```console
screenrig screen folder delete sfd_01EXAMPLEFOLDER000000000
screenrig screen folder delete sfd_01EXAMPLEFOLDER000000000 --expect-rev 3
```

Deletes only the folder: its screens and folders move up into its parent (the project root when it has none) and those screens re-resolve their playlist. `data` is `id`, `deleted` `true` and `levels`, the campaigns and folder default playlists the moved screens gained or lost, with a campaign's `reach_before` and `reach_after`. A campaign audience that names the folder refuses the delete with `resource_conflict` (exit 5): change or delete those campaigns first.

A caller limited to screen folders deletes only folders inside its scope, never a folder its scope names (`outside_screen_scope`, exit 3). A folder that is or holds a protected one answers `pending_review` with a `protected_change` review that a second approver decides, so do not repeat the write; `--note` tells the approver why.

### Fleet results

With several screen ids or `--tag`, `screen assign`, `screen reload`, `screen toast`, and `screen tag` run one action with no screen-count cap. The CLI follows bounded result pages automatically, waits when control budgets are exhausted, and retains the same key and selected fleet throughout. `--tag` selects active screens only; ids may name any screen. One screen id keeps the single-screen answer. Fleet commands take no `--expect-rev`, because revision guards are per screen. Each fleet command carries an idempotency key. After an interrupted request, such as a lost connection, rerun the identical command to replay finished screens without repeating their side effects. After a definite answer with failures, retry only the failed ids: rerunning the whole command acts on every screen again. Reusing the same explicit `--idempotency-key` within 24 hours returns the recorded answer instead.

A fleet command takes screen ids, `--tag TAG` or `--folder ID`; giving more than one of them is a usage error (exit 2).

The envelope keeps `ok: true`. Partial success is a normal answer, not a transport error:

```json
{
  "ok": true,
  "data": {
    "action": "reload",
    "matched": 3, "succeeded": 2, "failed": 1,
    "results": [
      { "screen_id": "scr_LOBBY", "status": "ok", "reload": { "reload_id": "…", "expires_at": "…", "state": "sent" } },
      { "screen_id": "scr_GONE", "status": "failed", "problem": { "code": "not_found", "status": 404 } },
      { "screen_id": "scr_ENTRANCE", "status": "ok", "reload": { "reload_id": "…", "expires_at": "…", "state": "queued" } }
    ]
  },
  "warnings": [{ "code": "fleet_partial_failure", "message": "1 of 3 screens failed; 2 succeeded." }]
}
```

An `ok` result carries the single-screen result: `revision` for `assign`, `revision` and `tags` for tag changes, `reload` for reload, and `toast` for toast. A `failed` result carries the problem that screen's own request would have returned. Per-screen limits still apply inside a fleet action, so a fleet reload or toast can report `rate_limited` for a screen that was just reloaded or toasted; retry that screen id later. Per-screen limits are 6 reloads and 20 toasts a minute. Fleet reloads and toasts also draw on an organization budget of 600 screens a minute: the server pauses when the remaining budget is exhausted, and the CLI honors `Retry-After` before continuing automatically.

| Outcome | Exit code | Warning |
| --- | --- | --- |
| Every matched screen succeeded | `0` | none |
| Some or all screens failed | The first failed screen's code, such as `4` for `not_found` or `6` for `revision_conflict` | `fleet_partial_failure` |
| No screen matched | `0` | `fleet_no_match` |
| Malformed selector or action | `2` or the request's own code; no screen changes | none |

### Screen folders

A screen folder organizes screens by place. A folder ID starts `sfd_`, and a screen sits in at most one folder, reported as `folder_id` on `screen show` and `screen list` and absent at the project root. An organization holds at most 1000 folders across its projects, nested at most 10 levels deep, with names of 1 to 100 characters that are unique among their siblings. Tags stay for groups that cut across places.

```console
screen folder list
screen folder show <id>
screen folder create --name NAME [--parent ID] [--default-playlist-id ID] [--note TEXT]
screen folder update <id> [--name NAME] [--parent ID | --root] [--default-playlist-id ID | --clear-default] [--unprotect] [--expect-rev REVISION] [--note TEXT]
screen folder delete <id> [--expect-rev REVISION] [--note TEXT]
screen move <id>... (--folder ID | --root) [--note TEXT]
```

```console
screenrig screen folder create --name "North Tower"
screenrig screen folder create --name "Level 2" --parent sfd_01EXAMPLEFOLDER000000000 --default-playlist-id pl_01EXAMPLEPLAYLIST000000
screenrig screen move scr_01EXAMPLESCREEN00000000 --folder sfd_01EXAMPLEFOLDER000000000
```

`screen folder list` returns every folder in `data.items` with `id`, `name`, `parent_id` (`null` at the project root), `default_playlist_id` when set, `screens` (the live screens directly inside) and `revision`. `screen move` takes screen and folder IDs in any mix, at most 1000 in one all-or-nothing request; a folder moves with everything inside it and cannot move into itself or a folder inside it. `screen list --folder ID` lists only the screens directly inside one folder.

On every fleet command that takes `--tag TAG`, `--folder ID` selects every active screen in that folder and in every folder inside it. A campaign takes the same: `--folder ID` on `campaign create` and `campaign update`, or a `{"folder":"sfd_…"}` rule in `--audience`, which combines with tag rules as in `{"all":[{"folder":"sfd_BUILDING"},{"tag":"overhead"}]}`.

A folder's default playlist is what its screens show when they have no playlist of their own. The nearest folder above a screen that sets one wins, and `effective_playlist` reports `source` `folder` with the folder id as `entry_id`. A takeover, a campaign, a schedule and the screen's own assigned playlist all rank above it; see [Schedules and takeover](https://screenrig.ai/docs/cli/schedules.md#schedules). The playlist must be approved (`playlist_not_approved`, exit 5).

Moving screens or a folder, or changing a folder's default playlist, re-resolves the affected screens within about 15 seconds. The answer's `levels` lists, under `gained` and `lost`, the campaigns whose folder audience those screens entered or left and the folder default playlists they started or stopped showing, each with its `source` (`campaign` or `folder`), `id` and `screens` count. Deleting a folder deletes nothing inside it: its screens and folders move up into its parent, and `resource_conflict` (exit 5) refuses the delete while a campaign audience names the folder. The delete answers `200` with `levels`, as a move does.

Writes need Manage access: a Propose session answers `403 insufficient_access`. A change that reaches a [protected folder](https://screenrig.ai/docs/permissions.md#protected-folders) waits for a second approver and answers `pending_review`, and `--note TEXT` tells the approver why. Each write appends `screen.folder_created`, `screen.folder_updated`, `screen.folder_deleted` or `screen.moved` to the project events.

## 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)
- [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)
- [Notifications](https://screenrig.ai/docs/cli/notifications.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)
