# Playlists CLI commands

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

Prepare, validate, create, patch, export and import playlists, manage their pages, revisions and instances, and replace an application release. 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.

## Playlists

A playlist is a JSON document of pages. `playlist init` prepares a full-screen document from ordered inputs. `playlist create` and `playlist update` read that document from a file; `playlist patch`, `playlist rename` and `playlist page add|replace|remove` change part of it. The shape is summarised under the commands.

### `playlist init`

Prepare an editable full-screen playlist from local media files, ready media IDs, application release IDs, HTTP or HTTPS URLs, or pasted iframe embed codes.

```console
playlist init <inputs...> --name NAME --output FILE [--overwrite] [--screen-id ID]
              [--target-width PX --target-height PX] [--duration-ms MS] [--fit contain|cover|fill] [--sound]
              [--no-transcode] [--no-progress] [--poll-ms MS]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<inputs...>` | files, `med_` IDs, `rel_` IDs, HTTP or HTTPS URLs, or pasted `<iframe>` embed codes | yes | none | Playback order. Local image, video and audio files upload and wait for readiness unless `--no-transcode`. Existing media must already be ready. An audio file or a ready audio `med_` ID becomes the soundtrack, in input order, instead of a page; at least one input must be a page. HTTP and HTTPS URLs may include LAN hosts, embedded credentials and screen tokens such as `{screen.field.kiosk_id}`, kept as written, and are not fetched. A pasted embed code is read for its `src`. The Player web app requires HTTPS URLs without a username or password. |
| `--name NAME` | string | yes | none | Playlist display name. |
| `--output FILE` | path | yes | none | Write the editable playlist document. Parent directories are created. Exclusive by default: an existing non-empty file fails with `usage_error` (exit 2) and is left untouched. |
| `--overwrite` | boolean | no | off | Replace an existing output file atomically with a document built from these inputs. |
| `--screen-id ID` | string | no | none | Read the target screen's dimensions and revision. A screen with a video wall gives the wall's canvas and the matching `format`. |
| `--target-width PX` / `--target-height PX` | integer | together | unset | Override canvas size. Both or neither. Required when no screen is supplied or the screen reports unknown or multiple surfaces. |
| `--duration-ms MS` | integer | no | 8000 | Duration for non-video pages. An application page holds for 24 hours (86400000) unless this is given. |
| `--fit` | string | no | `contain` | `contain`, `cover`, or `fill` for image and video pages. Applications and iframes use `fill`. |
| `--sound` | boolean | no | off | Play the audio of video pages. Without it every video page is silent. A page with sound pauses the soundtrack while it shows. |
| `--no-transcode` | boolean | no | off | Upload accepted file bytes unchanged. |
| `--no-progress` | boolean | no | off | Suppress upload progress on stderr. |
| `--poll-ms MS` | integer | no | 1000 | Upload polling interval. |

```console
screenrig playlist init med_01EXAMPLEIMAGE0000000000 med_01EXAMPLEVIDEO0000000000 --name "Lobby loop" --screen-id scr_01EXAMPLESCREEN00000000 --output lobby.json
```

Preparation writes the local document and may upload media; it neither creates a remote playlist nor assigns a screen. Inspect the document, then `screen publish`. See [Prepare, publish, and edit playlists](https://screenrig.ai/docs/cli.md#playlist-workflows).

`data` reports `output`, `width`, `height`, `page_count`, `soundtrack_tracks`, `release_ids`, `media_ids` and `uploads`, and with `--screen-id` also `screen_id`, `screen_revision` and `publish.argv`; a screen with a video wall adds `format`. An application page set under 60 seconds earns an `application_page_short` warning.

### `playlist validate`

Validate a playlist document locally before create or update. No network.

```console
playlist validate <file>
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<file>` | path | yes | none | The playlist document, or `-` for stdin. |

```console
screenrig playlist validate ./lobby-loop.json
```

Errors name exact JSON paths. A pass means local shape and cross-field semantics are valid; authorization, media readiness, dynamic selector counts, durations, and remote availability still require server checks. `playlist create`, `playlist update` and `screen publish` run this check before their write. `data` reports `valid`, `scope` and `server_checks`, and carries geometry `lint` findings, each with `page_id`, `code`, `id` and `message`, that do not fail the check: `motion_overuse`, `collision`, `safe_margin` and `adjacent_repeat`. An application page that advances in under 60 seconds earns an `application_page_short` warning. To see how a page looks, publish it and run `screen screenshot`.

### `playlist create`

Create a playlist from a JSON document, from a template, or from the built-in Urgent notice template.

```console
playlist create [file] [--from-template ID --name NAME [--set NAME=VALUE...] [--dry-run]] [--starter KEY [--name NAME]] [--format-region REGION --format-orientation ORIENTATION] [--note TEXT]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `[file]` | path | no | none | The playlist document, or `-` for stdin. Give it, or `--from-template`, or `--starter`. It holds `name`, `pages` and optionally `audio`, `format` and `fields`; any other member is a `usage_error`. |
| `--from-template ID` | string | no | none | Make the playlist from this template (a `pl_` id): its current pages with the `--set` values filled in, checked like any playlist. |
| `--name NAME` | string | with `--from-template` | none | The new playlist's name. `--dry-run` does not need it, and `--starter` takes it optionally. |
| `--set NAME=VALUE` | string | no | none | With `--from-template`: a field's value, the id of a ready image or video for a media field. Repeatable. NAME is a field name of 1 to 32 lower case letters, digits or underscores, given once. |
| `--dry-run` | boolean | no | off | With `--from-template`: check the values and report the page count without creating anything. |
| `--starter KEY` | string | no | none | Add a built-in template: `urgent-notice`, a headline and a message for quick announcements. |
| `--format-region REGION` | string | no | none | `header`, `footer` or `side`: make the playlist a strip drawn over the page along that edge, replacing a `format` in the file. Give it with `--format-orientation`. See [strips](https://screenrig.ai/docs/players.md#strips). |
| `--format-orientation ORIENTATION` | string | with `--format-region` | none | `landscape` or `portrait`: the orientation of the display the strip is made for, as the viewer sees it. |
| `--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 playlist create ./lobby-loop.json
```

```console
screenrig playlist create --from-template pl_01EXAMPLETEMPLATE00000 --name "Monday notice" --set headline="Open late"
```

The envelope's `data.id` is the `pl_` id that `screen assign` takes. When the change waits for review, `data` is `{status: pending_review, review, playlist, next}`: the playlist exists with `approved_revision: 0`, and no screen can show it until a person approves (`playlist_not_approved`); `next` is `screenrig reviews show ID`. A template is a playlist that declares `fields`; it is never shown on a screen (`playlist_is_template`), and a playlist made from it is an ordinary one to assign, schedule and revise.

### `playlist update`

Replace a playlist document.

```console
playlist update <id> <file> [--format-region REGION --format-orientation ORIENTATION] [--expect-rev REVISION] [--note TEXT]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `pl_` playlist id. |
| `<file>` | path | yes | none | The replacement document, or `-` for stdin. It holds `name`, `pages` and optionally `audio`, `format` and `fields`; any other member is a `usage_error`. Omitting `audio` removes the soundtrack, omitting `format` makes the playlist an ordinary one, and omitted `fields` are kept. |
| `--format-region REGION` | string | no | none | `header`, `footer` or `side`: make the playlist a strip drawn over the page along that edge, replacing a `format` in the file. Give it with `--format-orientation`. See [strips](https://screenrig.ai/docs/players.md#strips). |
| `--format-orientation ORIENTATION` | string | with `--format-region` | none | `landscape` or `portrait`: the orientation of the display the strip is made for, as the viewer sees it. |
| `--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 playlist update pl_01EXAMPLEPLAYLIST000000 ./lobby-loop.json
```

`playlist show ID --output FILE` writes a file in this shape. To change only part of a playlist, use `playlist patch`, `playlist rename` or `playlist page add|replace|remove`.

When the change waits for review, the live playlist keeps playing unchanged
until a person approves the proposal.

### `playlist patch`

Change only the supplied top-level members of a playlist and keep everything else.

```console
playlist patch <id> [file] [--name NAME] [--clear-audio] [--expect-rev REVISION] [--note TEXT]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `pl_` playlist id. |
| `[file]` | path | no | none | A partial JSON object with any of `name`, `pages`, `audio`, `format` and `fields`, or `-` for stdin. Supplied `pages`, `audio` and `format` replace the whole value; arrays are not merged. `audio: null` removes the soundtrack and `format: null` removes the format. |
| `--name NAME` | string | no | unchanged | Set the name, 1 to 120 characters after trimming, without sending the content. Not together with `name` in the file. |
| `--clear-audio` | boolean | no | off | Remove the soundtrack, as `audio: null` does. No page may keep an `audio_cue`. Not together with `audio` in the file. |
| `--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 playlist patch pl_01EXAMPLEPLAYLIST000000 --name "Lobby loop" --expect-rev 7
screenrig playlist patch pl_01EXAMPLEPLAYLIST000000 changes.json --expect-rev 7
```

Give a file, `--name` or `--clear-audio`. A name-only change does not check the existing content again. Supplied `pages` and `audio` are checked together by the server. A region `format` is `{"region": "header", "orientation": "landscape"}`; see [strips](https://screenrig.ai/docs/players.md#strips). Under Propose access, or in a project that requires review for agent changes, the change waits as for `playlist update` and answers `pending_review`.

### `playlist rename`

Change a playlist's name without sending its content.

```console
playlist rename <id> --name NAME [--expect-rev REVISION] [--note TEXT]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `pl_` playlist id. |
| `--name NAME` | string | yes | none | The new name, 1 to 120 characters after trimming. |
| `--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 playlist rename pl_01EXAMPLEPLAYLIST000000 --name "Lobby loop" --expect-rev 7
```

The name-only form of `playlist patch`: pages, soundtrack and comments stay as they are, and the existing content is not checked again. A proposed rename waits for a person's approval like any other change.

### `playlist page add`

Add one page to a playlist without sending the others.

```console
playlist page add <playlist-id> (--file PAGE.json | --media MED_ID) [--seconds N] [--fit contain|cover] [--sound]
                  [--from WHEN] [--until WHEN] [--before PAGE_ID] [--page-id ID]
                  [--expect-rev REVISION] [--note TEXT]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<playlist-id>` | string | yes | none | The `pl_` playlist id. |
| `--file PAGE.json` | path | exactly one of `--file`, `--media` | none | A page object added as written, or `-` for stdin. `--seconds` and `--fit` do not apply to it. |
| `--media MED_ID` | string | exactly one of `--file`, `--media` | none | A ready image or video. The page shows it full-canvas on the playlist's canvas (the wall's canvas for a video wall, else the first ordinary page's) with the first ordinary page's transition, and has no dates unless you give them. An image advances after `--seconds`; a video plays to its end, silent unless `--sound`. |
| `--seconds N` | integer | no | 10 | How long an image shows, 1 to 86400. |
| `--fit` | string | no | `contain` | `contain` or `cover`. |
| `--sound` | boolean | no | off | Play the video's audio. A video only; the page pauses the soundtrack while it shows and cannot carry an `audio_cue`. Not with `--file`, whose page carries its own `muted`. |
| `--from WHEN` | string | no | none | First time the page is shown: `YYYY-MM-DDTHH:MM`, or `YYYY-MM-DD` for 00:00, in each screen's timezone. |
| `--until WHEN` | string | no | none | End of the page's showing, exclusive: `YYYY-MM-DDTHH:MM`, or `YYYY-MM-DD` for the end of that day. |
| `--before PAGE_ID` | string | no | at the end | Put the page in front of this page. |
| `--page-id ID` | string | no | `up-` and eight lower case letters or digits | The new page's id: a letter, then letters, digits, `-` or `_`, at most 64 characters. |
| `--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 playlist page add pl_01EXAMPLEPLAYLIST000000 --media med_01EXAMPLEIMAGE0000000000 --from 2026-12-01 --until 2026-12-24
screenrig playlist page add pl_01EXAMPLEPLAYLIST000000 --media med_01EXAMPLEVIDEO0000000000 --before up-ab12cd34 --note "Winter menu"
screenrig playlist page add pl_01EXAMPLEPLAYLIST000000 --file page.json --page-id welcome
```

The page is shown from `--from` up to, not including, `--until`. Ended pages stay until someone removes them. The answer is the updated playlist.

Under Propose access, in a project that requires review for agent changes, or as an uploader member whose changes are reviewed, the change answers `pending_review` (`ok: true`, exit 0). A later page change to the same playlist by the same proposer joins that review while it waits, so successive uploads do not replace each other. A playlist no person approved yet answers `playlist_not_approved`. An uploader adds only media pages of its own folders to its own playlists; anything else is `outside_upload_scope`, whose `upload_scope` lists what it may use. See [Uploaders](https://screenrig.ai/docs/authentication.md#uploaders).

### `playlist page replace`

Replace one page of a playlist, keeping its place and id.

```console
playlist page replace <playlist-id> <page-id> [--file PAGE.json | --media MED_ID] [--seconds N] [--fit contain|cover]
                      [--sound] [--from WHEN] [--until WHEN | --always] [--expect-rev REVISION] [--note TEXT]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<playlist-id>` | string | yes | none | The `pl_` playlist id. |
| `<page-id>` | string | yes | none | The page to replace. |
| `--file PAGE.json` | path | no | none | The complete new page, or `-` for stdin. Its `id` must be `<page-id>` or absent. Not together with `--media`. |
| `--media MED_ID` | string | no | none | Swap the page's media; everything else, dates included, stays as stored. An image for an image keeps the canvas, fit, seconds and dates, and so does a video for a video. An image for a video, or the other way, is rebuilt as a media page on the playlist's canvas that keeps the id, transition and dates. Needs a page with a single primitive. |
| `--seconds N` | integer | no | unchanged | How long an image shows, 1 to 86400. Not with `--file`. |
| `--fit` | string | no | unchanged | `contain` or `cover`. Not with `--file`. |
| `--sound` | boolean | no | off | Play the audio of the page's video, on its own or with `--media`. A page with a single video primitive only; without it a swap keeps whether the page had sound. Not with `--file`. |
| `--from WHEN` | string | no | unchanged | First time the page is shown: `YYYY-MM-DDTHH:MM`, or `YYYY-MM-DD` for 00:00, in each screen's timezone. Changes that bound alone. |
| `--until WHEN` | string | no | unchanged | End of the page's showing, exclusive: `YYYY-MM-DDTHH:MM`, or `YYYY-MM-DD` for the end of that day. Changes that bound alone. |
| `--always` | boolean | no | off | Remove the page's dates, `from` and `until`. A switch-off, weekly windows or data condition the project set stay. Not together with `--from` or `--until`. |
| `--expect-rev REVISION` | string | no | none | Optional revision precondition. A page derived from the stored one is held to the revision it was read at unless you give one. A stale value is `revision_conflict`. |
| `--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 playlist page replace pl_01EXAMPLEPLAYLIST000000 up-ab12cd34 --media med_01EXAMPLEIMAGE0000000000
screenrig playlist page replace pl_01EXAMPLEPLAYLIST000000 up-ab12cd34 --until 2026-12-31
screenrig playlist page replace pl_01EXAMPLEPLAYLIST000000 up-ab12cd34 --always
```

Give at least one of `--file`, `--media`, `--sound`, `--from`, `--until` or `--always`. Date flags also work on a page with several primitives; changing the media, fit or seconds needs a page with a single primitive, or the whole page in `--file`. An unknown page id is refused. Review and uploader rules are those of `playlist page add`; an uploader replaces only a media page of its own folders.

### `playlist page remove`

Remove one page from a playlist.

```console
playlist page remove <playlist-id> <page-id> [--expect-rev REVISION] [--note TEXT]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<playlist-id>` | string | yes | none | The `pl_` playlist id. |
| `<page-id>` | string | yes | none | The page to remove. |
| `--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 playlist page remove pl_01EXAMPLEPLAYLIST000000 up-ab12cd34
```

A playlist keeps 1 to 100 pages and one ordinary page with no `visibility`, so removing the last undated page is `invalid_request`. An unknown page is `not_found`. Review and uploader rules are those of `playlist page add`.

### `playlist show`

Read one playlist, including its current revision, or write it as an editable document.

```console
playlist show <id> [--output FILE [--overwrite]] [--editable]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `pl_` playlist id. |
| `--output FILE` | path | no | none | Write the editable document to FILE and report its path, `playlist_id` and `revision`. The file is ready for `playlist update` as written. Parent directories are created. Exclusive by default. Takes precedence over `--editable`. |
| `--overwrite` | boolean | no | off | Replace an existing `--output` file atomically. An empty file left by an interrupted run is replaced without it. |
| `--editable` | boolean | no | off | Return the editable document and its revision in the envelope as `data.document`, `data.playlist_id` and `data.revision`. |

```console
screenrig playlist show pl_01EXAMPLEPLAYLIST000000
screenrig playlist show pl_01EXAMPLEPLAYLIST000000 --output playlist.json
screenrig playlist show pl_01EXAMPLEPLAYLIST000000 --editable
```

Plain `show` is an inspection response. It returns the live version. `approved_revision` is `0` for a playlist no
person has approved yet, and `approval` names the open review while a change
waits; `reviews show ID` returns the proposal. When open notes are about the playlist, the warning `notes_about_resource` counts them and names `screenrig notes --about ID`, and `--human` lists up to five.

### `playlist list`

List the project's playlists.

```console
playlist list [--templates] [--library]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `--templates` | boolean | no | off | Only templates: playlists that declare fields. |
| `--library` | boolean | no | off | List the organization Library's playlists instead, read-only: assign them to screens, schedules and stacks, or copy one with `playlist create --from-template`; change them only in the Library project. |

```console
screenrig playlist list --templates
screenrig playlist list --library
```

### `playlist fields`

Show a template's fields or a copy's values, or change the values and bring the copy up to date.

```console
playlist fields <id> [--set NAME=VALUE...] [--clear NAME...] [--refresh] [--expect-rev REVISION] [--note TEXT]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `pl_` id of a template, or of a playlist made from one. |
| `--set NAME=VALUE` | string | no | none | Set a field's value. Repeatable; NAME is a field name of 1 to 32 lower case letters, digits or underscores, given once. |
| `--clear NAME` | string | no | none | Clear an optional field's value. Repeatable. |
| `--refresh` | boolean | no | off | Build the pages again from the template's current revision, whatever the values. |
| `--expect-rev REVISION` | string | no | none | Optionally require this resource revision. |
| `--note TEXT` | string | no | none | A note for the person who reviews the change. |

```console
screenrig playlist fields pl_01EXAMPLECOPY00000000 --set headline="Closed today"
```

Plain, it shows a template's fields and how many playlists were made from it, or a copy's values, the template revision it was built from and whether the template has a newer one. `data.kind` is `template` or `instance`: a template reports `fields` and `instances`, a copy `template_id`, `template_revision`, `template_current_revision`, `outdated` and `values`. Every change, and `--refresh` alone, builds the copy's pages again from the template's current revision, which is how a brand change made to the template reaches existing copies; nothing pushes to them. Values are checked like a create checks them. Under Propose access a change becomes a review.

### `playlist instances`

List the playlists made from a template and the template revision each was built from.

```console
playlist instances <id>
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `pl_` id of the template. |

```console
screenrig playlist instances pl_01EXAMPLETEMPLATE00000
```

`outdated` marks a playlist built from an older revision of the template; `playlist fields ID --refresh` brings it up to date.

### `playlist revisions`

List a playlist's revisions, newest first.

```console
playlist revisions <id> [--after CURSOR]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `pl_` playlist id. |
| `--after CURSOR` | string | no | none | The `data.next_cursor` of the previous page. |

```console
screenrig playlist revisions pl_01EXAMPLEPLAYLIST000000
```

Each revision in `data.items` says what made it (`created`, `edited`, `promoted` with the revision it copied as `restored_from`, or `baseline`, the revision the playlist held when its history began), who made it, when, its name, how many pages it holds and whether it is `current`. A page holds at most 50 revisions; while `data.next_cursor` is a string, older revisions remain, and `null` marks the last page. Revisions are kept as long as the playlist is.

### `playlist revision show`

Read one revision and the playlist as it stood at it.

```console
playlist revision show <id> --revision N
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `pl_` playlist id. |
| `--revision N` | integer | yes | none | The revision to read. |

```console
screenrig playlist revision show pl_01EXAMPLEPLAYLIST000000 --revision 3
```

`data.revision` holds the fields `playlist revisions` lists, and `data.playlist` the playlist as it stood: name, pages, soundtrack and format, with `revision` set to this one. A revision the playlist never had, or one from before its history began, is `not_found`.

### `playlist promote`

Make an earlier revision current again by copying it forward as a new revision.

```console
playlist promote <id> --revision N --expect-rev CURRENT
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `pl_` playlist id. |
| `--revision N` | integer | yes | none | The revision to copy forward. |
| `--expect-rev CURRENT` | string | yes | none | The playlist's current revision (`playlist show`). A stale value is `revision_conflict`: read the playlist again and retry. |

```console
screenrig playlist promote pl_01EXAMPLEPLAYLIST000000 --revision 3 --expect-rev 9
```

The revision's pages, soundtrack and format become the next revision. The name stays as it is, earlier revisions stay in the list, and every screen showing the playlist updates; the command returns once the playlist is written and does not wait for Players. Pages are resolved again as for any write: a selector matches today's media, and media or an application the revision used that is gone or not ready refuses the promote with the problem a write would get. Promoting needs a credential that publishes live; one whose changes wait for review gets `forbidden`, and a playlist no person approved yet is `playlist_not_approved`. A revision whose content is already current changes nothing. Promoting is a Premium feature: a plan without it answers `feature_not_in_plan` (exit 8), while `playlist revisions` and `playlist revision show` are in every plan.

### `playlist delete`

Delete one playlist.

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

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `pl_` playlist 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 playlist delete pl_01EXAMPLEPLAYLIST000000
```

A playlist is not deleted while a screen, archived or still pairing, has it as its default, in its playlist schedule or as its takeover, while a screen folder has it as its default, or while a campaign plays it: the answer is `resource_conflict` (exit 5). Change those first.

### `playlist export`

Export image and video files, soundtrack audio and iframe URLs as a portable playlist bundle.

```console
playlist export <id> --output DIRECTORY [--skip-applications]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `pl_` playlist id. |
| `--output DIRECTORY` | path | yes | none | Destination directory. It must not already exist; the bundle is written through a temporary sibling and renamed into place. |
| `--skip-applications` | boolean | no | off | Drop application primitives and empty or application-controlled pages. At least one page must remain. The result lists the skipped primitive and page ids. |

```console
screenrig playlist export pl_01EXAMPLEPLAYLIST000000 --output ./lobby-bundle
```

The bundle holds a manifest, the playlist document, and content-addressed image, video and soundtrack audio files. It carries `name`, `audio` and `pages`; a `format` and a template's `fields` stay behind. Dynamic `all`, `tag` and `folder` selectors are snapshotted to exact ids, iframe URLs are preserved, and comments are excluded.

Cards, streams and adslot pages stop export before media download. Applications also stop it by default; `--skip-applications` removes application primitives and empty or application-controlled pages, reporting the skipped primitive and page ids. At least one page must remain. Cards, streams and adslot pages still stop export with this flag.

For an editable document retaining card and application references, use `screenrig playlist show pl_01EXAMPLEPLAYLIST000000 --output playlist.json`. The document uses existing resources in the same project.

### `playlist import`

Create a playlist from a bundle directory, or update an existing one from it.

```console
playlist import <directory> [--name NAME] [--update ID [--expect-rev REVISION]] [--poll-ms MS] [--note TEXT]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<directory>` | path | yes | none | A bundle directory written by `playlist export`. |
| `--name NAME` | string | no | the bundle's playlist name | 1 to 120 characters. Names the new playlist, or renames the one `--update` replaces. |
| `--update ID` | string | no | a new playlist is created | Update this `pl_` playlist instead of creating one. Affects every screen assigned to it. Accepts optional `--expect-rev`. |
| `--expect-rev REVISION` | string | no | none | With `--update`: the destination playlist's current revision. |
| `--poll-ms MS` | integer | no | 1000 | Interval for polling uploads while media becomes ready. |
| `--note TEXT` | string | no | none | A note for the person who reviews the change when it waits for review, at most 280 characters. |

```console
screenrig playlist import ./lobby-bundle
screenrig playlist import ./lobby-bundle --name "Lobby loop (copy)"
screenrig playlist import ./lobby-bundle --update pl_01EXAMPLEPLAYLIST000000
```

Playlist display names may repeat. Import creates a separate playlist even when its name matches; use `--update` with an ID to replace an existing playlist.

Import validates the whole bundle locally before it touches the project: manifest, document, paths, media kinds, lengths, and hashes. Existing media are reused on an exact filename, media type, byte length, hash, and tag match; the rest are uploaded without conversion, and the playlist is written only after every object is ready. `data` reports `mode` (`create` or `update`), `source_playlist_id`, `playlist`, `directory` and `media` with `total`, `reused` and `uploaded`.

Under Propose access, or in a project that requires review for agent changes, the media still uploads and the playlist write becomes a review: `data.status` is `pending_review`, `data.review` names it and `data.next` is `screenrig reviews show ID`. Nothing changes on glass until a person approves.

Import never deletes or prunes remote media. If a later step fails after an upload has started, the error lists the objects confirmed ready and says whether the playlist write began. Retrying the same bundle reuses those objects.

### `playlist replace-release`

Preview or apply one application release replacement in a playlist, with the screens it reaches.

```console
playlist replace-release <id> --page PAGE_ID --primitive PRIMITIVE_ID --release-id REL_ID
                         [--apply [--expect-rev REVISION] [--expect-impact TOKEN] [--note TEXT]]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `pl_` playlist id. |
| `--page PAGE_ID` | string | yes | none | The exact id of the page that holds the application. |
| `--primitive PRIMITIVE_ID` | string | yes | none | The exact id of the `application` primitive on that page. |
| `--release-id REL_ID` | string | yes | none | The new immutable `rel_` release, from `data.application.release_id` of `app update`. |
| `--apply` | boolean | no | off | Write the replacement. Without it the command is a read-only preview. |
| `--expect-rev REVISION` | string | no | none | With `--apply`: the playlist revision from the preview. A different current revision is `revision_conflict`, and the server checks it atomically. |
| `--expect-impact TOKEN` | string | no | none | With `--apply`: `data.impact` from the preview. If the replacement or the affected screens changed since, the write is `resource_conflict`: preview again. |
| `--note TEXT` | string | no | none | With `--apply`: a note for the person who reviews the change when it waits for review. |

```console
screenrig playlist replace-release pl_01EXAMPLEPLAYLIST000000 --page board-page --primitive board --release-id rel_NEW
screenrig playlist replace-release pl_01EXAMPLEPLAYLIST000000 --page board-page --primitive board --release-id rel_NEW --apply
```

The preview lists the active, pairing-pending and archived screens whose default assignment, stored schedule entries (paused and future ones included), takeover or current effective playlist (campaign and inherited folder playback included) references the playlist. `data` carries `previous_release_id`, `release_id`, `revision`, `affected_screens` (each with `id`, `label`, `state` and `revision`), `impact` and `applied`. A direct `--apply` skips the screen survey, so `affected_screens` is omitted unless `--expect-impact` is given. An already pinned release succeeds without writing. Under review, `--apply` proposes the new pin. See [Replace one application release](https://screenrig.ai/docs/cli.md#playlist-workflows).

### The playlist document

A playlist is `name` (1 to 120 characters) plus `pages` (1 to 100), with an optional `audio` soundtrack and an optional `format`. Each page carries `id`, `canvas`, `transition`, `advance`, optional `visibility`, optional `audio_cue`, and `primitives` (1 to 24). Page, primitive and track ids are a letter followed by up to 63 letters, digits, `_` or `-`; page ids are unique in the playlist and primitive ids on their page. A `canvas` takes `width` and `height` from 1 to 1,000,000, a `background` and an optional `viewport_fit`: see [Page background and motion](https://screenrig.ai/docs/compose.md#page-background-and-motion).

A `format` makes a playlist a video wall, `{displays, arrangement, orientation}` with `columns` for a `grid`, or a strip, `{region, orientation}`. A wall has 2 to 12 displays of 1920 x 1080 (1080 x 1920 in portrait), every ordinary page has the wall's canvas, and a primitive's `displays` names the tiles it covers in place of `rect`. See [Video walls](https://screenrig.ai/docs/players.md#video-walls) and [Strips](https://screenrig.ai/docs/players.md#strips). An ad slot page, `{id, type: "adslot", adslot_id}` with optional `visibility`, holds no canvas or primitives; a playlist holds at most 16 of them and keeps one ordinary page with no `visibility`.

A template adds `fields` to the document, `[{name, label, type, required, max_length?}]` with `type` of `text`, `media` or `url`, and may mark any primitive `locked: true`. A page reads a field as `{field.NAME}` in a card's `params` (text and url fields), as an image or video `selector.media_id` (a media field) or as the whole `src` of an iframe (a url field). Every token must name a declared field of the right type, every field must be read, and a locked primitive reads none; `playlist validate` checks this offline. Only the owner or a member who can approve changes a locked primitive (`primitive_locked`). A template declares at most 16 fields: `name` is 1 to 32 lower case letters, digits or underscores, `label` up to 60 characters, and a text field's `max_length` (default 200) up to 2000. An optional media or url field left without a value leaves out the primitive that reads it. Declaring `fields` and locking a primitive are playlist templates, a Premium feature. A video wall `format` of 2 or 3 displays needs Plus and one of 4 to 12 displays Enterprise. A plan without the feature answers `feature_not_in_plan` with `required_plan`, while a write that keeps what the playlist already holds stays open. Making a copy from a template is in every plan.

The six primitives are `image`, uploaded `video`, live `stream`, web `iframe` and `application`, and `card`. Copy and chrome are not wire types: place a `card` for live text, or upload a finished still your agent made and place it as an image.

A stream takes one or two ordered sources with unique protocols and a ready image from the same project as fallback_media_id. Use at most one stream per page and duration-based advance. HLS uses a public HTTPS URL; Android and Qt Players connect directly to RTSP camera feeds, login included. See [camera transport details](/docs/players#streams) for the URL forms each Player opens; optional direct UDP/MPEG-TS multicast on Android and Qt uses an IPv4 group in 239/8 and a port. A source can give only its `url` and let the scheme name the protocol. Other Players select the HLS alternative or display the fallback. Stream bytes are not cached for replay. Audio defaults to muted; `muted` set to `false` plays it, with the rules for a video's sound below. Playlist bundle import and export do not support streams.

```json
{
  "id": "live",
  "primitive": "stream",
  "sources": [
    {
      "protocol": "udp-mpegts",
      "group": "239.10.0.1",
      "port": 5000
    },
    {
      "protocol": "hls",
      "url": "https://example.com/live.m3u8"
    }
  ],
  "fallback_media_id": "med_fallback",
  "muted": true,
  "rect": {
    "x": 0,
    "y": 0,
    "width": 1920,
    "height": 1080
  },
  "layer": 0,
  "content_fit": "contain"
}
```

Use this primitive on a page with `advance: {"mode":"duration","after_ms":30000}`. Replace the example feed and fallback ID. [Player requirements and transport setup](https://screenrig.ai/docs/players.md#streams).

Every primitive is flat: `id`, `primitive`, that primitive's own fields, then `rect`, `layer`, `content_fit`, and optional `enter` and `motion`. There is no nested content object. `rect` is `x`, `y`, `width` and `height` in canvas units and `layer` a whole number from 0 to 1024. A `rect` may extend past the canvas for bleed: Players fit content to the whole `rect` and then clip to the canvas, so use `content_fit` `cover` or `fill` to fill the visible canvas. A card is placed by `layout` and needs a `rect` only for `free`; on a wall playlist `displays` replaces `rect`.

Image and video primitives require a `selector`. `selector.by` is `id` with one ready `media_id` and `one_at_a_time` false, `ids` with 1 to 1,024 unique ready `media_ids`, `all` for every ready object of that primitive, `tag` matching `^[A-Za-z0-9]{1,32}$`, or `folder` with the `folder_id` of an existing `mfd_` media folder, which plays the ready files directly inside it and none of its subfolders. Do not put `media_id` on the primitive itself or send server-resolved items. An `application` primitive pins one `release_id` and an `iframe` primitive takes a `src` and a `title` of 1 to 200 characters, so neither of those takes a selector. `src` is any `http` or `https` page the screen can reach, a LAN page or a page behind a login included; a YouTube link is stored as its autoplaying embed. `src`, and an iframe's optional `fallback_src`, may carry [screen tokens](https://screenrig.ai/docs/cli/media.md#screen-tokens-in-iframe-addresses-and-application-params) such as `{screen.field.kiosk_id}`, and an `application` primitive may carry `params` with the same tokens, so one playlist gives every screen its own page. Android, Amazon Signage Stick, Linux, Raspberry Pi and Windows Players open the page itself. The Player web app and the macOS, Samsung Tizen, LG webOS and BrightSign Players frame it, so a page that sends `X-Frame-Options` (`DENY`, `SAMEORIGIN` or the obsolete `ALLOW-FROM`), or a `Content-Security-Policy` `frame-ancestors` list without the Player's origin, shows a blocked frame on them, and the Player web app shows only `https` pages without a login in the address. When publishing detects a public HTTPS page that refuses framing, the write still succeeds and the CLI prints the server's `warnings` entry: code `iframe_framing_refused`, the `page_id`, `primitive_id` and `src`, the header that refused, and the next step (use a page that allows framing from the Player origin, or ask its owner to allow that origin). The server also appends a `playlist.iframe_framing_refused` warning to the event log, one per refusing page, which `events list` prints with `primitive_id`. A LAN page, an `http` page and a page the server cannot reach within a few seconds are not checked. A Player reports a playlist as playing once its manifest is on glass, so a verified publish does not prove that an iframe page rendered: act on the warning, or look at the screen. Both web primitives require `content_fit` of `fill`, because neither has an intrinsic size.

An `iframe` also takes `reload_s` (0, the default, never reloads; 60 to 86400 loads `src` again every `reload_s` seconds while the page is on glass), `fallback_src` (the page shown when `src` does not load, with the forms and rules of `src`; a recovered `src` replaces it at the next reload) and `navigation_allowlist` (1 to 64 host-sources of the form `[scheme://]host[:port][path]`, such as `example.com`, `*.example.com` or `https://example.com/docs/`; with a list, the page's links, scripted navigations and redirects reach only `src`, `fallback_src` and matching addresses; omit it for no restriction, an empty list is refused). A pasted `<iframe>` embed code is read for its `src`. A Google Slides, Docs or Sheets share, Publish to web or embed link, or a Microsoft 365 Embed link, is stored rebuilt for the screen: Slides and PowerPoint decks loop, a Slides deck advances on the link's own delay held to the page duration (give the page the time of the whole deck), and Docs, Sheets, Word and Excel reload every 5 minutes. A file that asks viewers to sign in is refused with `document_not_shared`, whose detail names the sharing setting to change. See [Show a web page, a video or a document](https://screenrig.ai/docs/compose.md#show-a-web-page-a-video-or-a-document).

`advance.mode` is `duration` with `after_ms`, which on a page holding exactly one YouTube video (not a playlist or live stream) may also carry `until` set to `video_end` so the page lasts until the video ends and `after_ms` is how long it shows when the screen cannot read the video's length, `media_end` on a page with exactly one image or video primitive, `audio_end` on a page whose `audio_cue` names a soundtrack track, or `application` with `max_ms` when the app ends its own turn by calling `window.screenrig.nextPage()`. An `application` page needs exactly one primitive marked `controller`; every other mode forbids it. `after_ms`, `max_ms` and `dwell_ms` are whole milliseconds from 1,000 to 86,400,000, and an image on a `media_end` page needs `dwell_ms`. A page holds at most one `stream` and at most two `application` or `iframe` primitives, and `media_end` forbids both of those.

`transition.type` is one of `crossfade`, `swipe-left`, `swipe-right`, `swipe-up`, `swipe-down`, `dip`, `wipe-left`, `wipe-right`, `wipe-up`, `wipe-down`, `slide-left`, `slide-right`, `slide-up`, `slide-down`, or `stinger`. `duration_ms` is required, 0 through 60000. `playlist init` writes a 200 ms crossfade. Swipe moves both pages together like a carousel. Slide moves the incoming page over the outgoing one, which drifts a quarter as far. Wipe uncovers the incoming page behind a moving hard edge. Dip fades the outgoing page to a solid colour and then in to the incoming page; `color` is an optional `#RRGGBB`, default `#000000`, and is valid only on `dip`. Direction names are the direction of motion: `wipe-left` is an edge moving left. `ease` is optional: `linear`, `in`, `out` or `in-out`; the default is `linear` for `crossfade` and `swipe-*` and `in-out` for `dip`, `wipe-*` and `slide-*`; it is not allowed on `stinger`. The incoming page owns its transition. The first page of a playlist plays no transition.

A stinger is a themed full-frame graphic that covers the outgoing page, swaps in the incoming page while the screen is covered, then uncovers it. Use it to name a section of a playlist: put it on the first page of the section. The `stinger` object is required for `type: "stinger"` and not allowed on other types. All its members are optional. `theme` is one of `neomustard` (the default), `cyanotype`, `propaganda`, `signal`, `avant-garde`, `bloom` or `primetime`, and matches the card themes. `title` is up to 40 characters and `subtitle` up to 60, set in the theme's display type. `logo` is `media:` plus the id of a ready image in the project, for example `media:med_01EXAMPLE`. `accent` is an optional `#RRGGBB` that replaces the theme accent. `duration_ms` for a stinger is 2000 through 6000 and covers the whole bumper; typical lengths are 2700 to 4000. The title stays fully displayed for `hang_ms`, 0 through 6000, default 1000 (one second), and `hang_ms` is at most 60 percent of `duration_ms`; a longer hang needs a longer `duration_ms`. A Player that cannot draw a stinger plays a crossfade of the same length. See the live [Stinger card](https://screenrig.ai/docs/cards/stinger) page to watch the transition play.

The following example shows a stinger page transition:

```json
{ "type": "stinger", "duration_ms": 3500, "stinger": { "theme": "primetime", "title": "BREAKFAST", "subtitle": "Served until 11", "logo": "media:med_01EXAMPLE", "hang_ms": 1000 } }
```

An optional `enter` object on a primitive adds one object animation: `fade-up`, `fade-down`, `fade-left`, `fade-right`, `fade-in`, `zoom-in`, or `zoom-out`, and an optional `stagger` from 0 to 8 delays its start by 120 ms a step. An optional `motion` keeps a primitive moving after it enters: `spin` and `drift` turn or slide an image or video, and `path` moves an image, video, application or iframe along waypoints. See [Page background and motion](https://screenrig.ai/docs/compose.md#page-background-and-motion).

An optional page `visibility` object limits when the page plays: a required `enabled` boolean, optional civil `from` and `until` bounds, and up to 16 recurring `windows` of days and civil times. `from` and `until` are `YYYY-MM-DDTHH:MM` in each screen's timezone, `from` inclusive and `until` exclusive and later than `from`; a window is `days` (`mon` to `sun`) with `start` and `end` as `HH:MM`, given together or both left out for the whole day, and an `end` at or before `start` crosses midnight. `enabled: false` hides the page; `enabled: true` needs at least one of `from`, `until`, `windows` or `data`. Every playlist keeps at least one page with no `visibility` field at all. A screen reads these times in its effective time zone: its override, else the zone its Player reports, else UTC.

A page `visibility.data` condition, `{key, path?, equals?}`, shows the page only while a card data document matches: `key` is a card data key or card ID, `path` an RFC 6901 pointer into the document, and `equals` a string, number, boolean or null (omit it to test that the value exists). The key `{screen.weather}` reads the weather at each screen's own location, which `screen update ID --location LAT,LON` sets: `{"enabled":true,"data":{"key":"{screen.weather}","path":"/condition","equals":"rain"}}` shows the page only while it rains where that screen is. The weather document has `condition` (`clear`, `cloud`, `fog`, `rain`, `snow`, `storm` or `wind`), `band` (`freezing` below 0 °C, `cold` 0 to 9, `mild` 10 to 17, `warm` 18 to 24, `hot` from 25), `temperatureC` in whole degrees and `isDay`. It is the current weather for the screen's 0.25 degree cell, refreshed about hourly, not a forecast. A screen without a location never shows the page, and publishing refuses a `path` or an `equals` the document cannot hold, such as `cloudy` for `cloud`. A Player that does not declare the `page.conditions` host capability gets such a page disabled. Data conditions are a Plus feature: without it, a write that adds a `visibility.data` condition a page does not already hold answers `feature_not_in_plan`, while keeping or removing a stored condition and time-window visibility stay open. Keep an ordinary page without `visibility` as the fallback.

A `video` or `stream` primitive plays silent unless it sets `muted` to `false`. Then it plays its sound at the screen's volume while its page is current, and that page pauses the playlist soundtrack, which resumes from the same point on the next page without sound. Several primitives on one page all play their sound. A page with sound cannot carry an `audio_cue`: a write that adds one answers `invalid_request` at `pages[i].audio_cue`, "a page with sound cannot cue the soundtrack", while a stored older page that has both keeps playing. Ads stay muted.

An optional top-level `audio` object adds a soundtrack that plays continuously while pages change and pauses on a page with sound. `tracks` lists 1 to 128 entries in playback order, each with a unique `id` (the page id pattern) and the `media_id` of a ready `audio` media object. `loop` defaults to `true` and restarts at the first track after the last; `volume` defaults to `1` and ranges from 0 to 1, multiplied by the Player volume; `cued` defaults to `false`, and `true` stops the soundtrack after each track so that a track plays only when a page `audio_cue` starts it. An ordinary page may carry `audio_cue` with a `track` id from that list and an optional `restart` boolean (default `false`): when the page appears, the Player switches to that track from its start, restarting it only when it is already playing and `restart` is `true`. Cues are hints, adslot pages cannot carry them, and a cue without a soundtrack is rejected. A page with `advance` `{"mode":"audio_end"}` ends when the track its `audio_cue` names ends, so it requires that cue and a soundtrack, takes no `after_ms`, and follows the `duration` rules for image and video primitives; a `stream` primitive still needs a `duration` page, and an application on the page is never its controller; `max_ms` is an optional hint that the server overrides. `playlist update` replaces the whole document, so omitting `audio` removes the soundtrack; `playlist patch` keeps it unless `audio` is supplied, and `--clear-audio` or `audio: null` removes it. A strip has no soundtrack. [Soundtrack authoring](https://screenrig.ai/docs/compose.md#add-a-soundtrack).

## 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)
- [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)
- [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)
