# Campaigns CLI commands

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

Create, preview, publish, promote, pause and restore campaigns that switch a whole audience of screens to a playlist while their rules hold. 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.

## Campaigns

A campaign switches every screen in an audience to one playlist while its rules hold, and back when they stop. One campaign runs lunch menus on every `north` store, and a screen tagged `north` later gets it too. With none of its rules set it holds always.

Per screen the server picks its takeover, else the first campaign that holds (`--priority` 0 to 100, higher first, then the newest), else the playlist schedule, else the assigned playlist; see [Schedules and takeover](https://screenrig.ai/docs/cli/schedules.md#schedules) for the stack and the rules. A campaign reaches a screen that has a default playlist (`screen assign`). Its dates, days and hours are civil times read in each screen's own timezone: the override set with `screen set-timezone`, else the zone its Player reports, else UTC. Screens switch within about a minute, and Players need no update.

A paused campaign is a draft: it is on no screen until `campaign publish`, which can publish it to a test folder or a few test screens first, and `campaign promote` then puts its audience on it. `campaign preview` shows a change before it is made, and `campaign revisions` and `campaign restore` keep and bring back earlier versions. The same drafts, previews and history exist for schedule entries below; see [drafts, test and promote, history](https://screenrig.ai/docs/agents.md#change-control).

Campaign commands use the `screens` capability. Writes need Manage access: a Propose session answers `403 insufficient_access`. An organization has at most 100 campaigns across its projects (`campaign_limit_reached`, exit 5), and the playlist must be approved (`playlist_not_approved`, exit 5). Campaigns are a Plus feature: without it, `campaign create`, `campaign resume` and a `campaign update` other than pausing answer `feature_not_in_plan` (exit 8), while list, show, screens, explain, preview, pause and delete stay open, so a campaign already set up keeps running. A draft (`campaign create --paused`) and `campaign promote` belong to schedule change control, an Enterprise feature that answers the same way. [Pricing](https://screenrig.ai/pricing.md#plus-features) lists the plans. Each write appends `campaign.created`, `campaign.updated` or `campaign.deleted` to the project events, and a screen a campaign moves records `screen.playlist_switched` with `source` `campaign` and the campaign id as `entry_id`. `ads campaigns` buy ad time on other screens and are a different resource.

A caller limited to screen folders creates, changes and deletes only a campaign whose whole audience is inside its scope, else `outside_screen_scope` (exit 3), and `campaign list` and `campaign show` print `editable: false` for a campaign that reaches outside it. A `campaign create`, `update`, `pause`, `resume` or `delete` whose audience reaches a screen in a [protected folder](https://screenrig.ai/docs/permissions.md#protected-folders) answers `pending_review` with a `protected_change` review that a second approver decides: do not repeat the write, read the decision with `reviews show ID`, and give `--note TEXT` to tell the approver why. A [reserved campaign](https://screenrig.ai/docs/permissions.md#reserved-campaigns) is changed only by a person who can approve, and an agent is refused with `reserved_item` (exit 3) at every access level.

### `campaign list`

List the project's campaigns in the order screens read them.

```console
campaign list
```

No parameters.

```console
screenrig campaign list
```

`data.items` holds at most 100 campaigns, highest priority first and the newest first within a priority. Each carries the fields `campaign show` returns, with `reach` once it has been counted. A paused campaign is listed and applies nowhere. `reserved` marks a reserved campaign, and `editable` appears only for a caller limited to screen folders: `false` means the campaign reaches screens outside that scope.

### `campaign show`

Read one campaign.

```console
campaign show <id>
```

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

```console
screenrig campaign show cmp_LUNCH
```

Returns `name`, `playlist_id`, `audience` (the exact rule), `priority`, `paused`, `reserved` (`true` for a reserved campaign, which only a person who can approve changes), the `from`, `until`, `windows`, `monthly`, `dates`, `except`, `data`, `interaction`, `covers`, `displays` and `start` that are set, and `revision`, the value for `--expect-rev` on a later write. `editable` appears only for a caller limited to screen folders: `false` means the campaign reaches screens outside that scope and is read-only to that caller. `import` is present on a campaign a [schedule import](https://screenrig.ai/docs/cli/schedules.md#schedule-import) made. `reach` reports how many screens the audience matches: `published` when the campaign was last published (created, or updated with a new audience, playlist or paused state), `current` and `at` for the latest count, and `empty_groups`, the `tag:NAME` and `folder:ID` leaves of the audience that match no screen. A tag, folder, archive or delete that changes the count is recounted within about 15 seconds; `campaign list` carries `reach` too, and the [`reach_dropped` and `group_empty` notifications](https://screenrig.ai/docs/cli/notifications.md#notifications) watch it. An id from `ads campaigns list` answers `not_found` (exit 4) here.

### `campaign create`

Switch the screens of an audience to a playlist, always or while its rules hold.

```console
campaign create --name NAME --playlist-id ID (--tag TAG | --any-tag TAG | --screen-id ID | --folder ID | --all-screens | --audience RULE) [--from TIME] [--until TIME] [--window SPEC] [--monthly SPEC] [--date SPEC] [--except DATES] [--data KEY[:PATH[=VALUE]]] [--on-touch [IDLE_S]] [--displays N[,N]] [--start on-time|next-item] [--covers WHERE] [--priority N] [--paused] [--note TEXT]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `--name NAME` | string | yes | none | A name of 1 to 120 characters. |
| `--playlist-id ID` | string | yes | none | The `pl_` playlist the audience shows while the campaign holds. It must be approved. |
| `--tag TAG` | string | one audience option | none | Screens that carry this tag. Repeat or comma-separate to require every listed tag. |
| `--any-tag TAG` | string | one audience option | none | Screens that carry at least one of these tags. Repeat or comma-separate. |
| `--screen-id ID` | string | one audience option | none | These screens. Repeat or comma-separate. |
| `--folder ID` | string | one audience option | none | Screens in this `sfd_` screen folder and in every folder inside it. To combine a folder with tags, use `--audience`. |
| `--all-screens` | boolean | one audience option | off | Every screen in the project. |
| `--audience RULE` | string | one audience option | none | A rule as JSON, `@FILE` to read it from a file, or `-` for stdin. |
| `--from TIME` | string | no | no start | Start at this civil time in each screen's timezone, `YYYY-MM-DDTHH:MM` with no offset. |
| `--until TIME` | string | no | no end | End before this civil time in each screen's timezone, in the same form and later than `--from`. |
| `--window SPEC` | string | no | none | Hold only in this weekly window, `DAYS` or `DAYS@HH:MM-HH:MM`, such as `mon-fri@09:00-17:00` or `sat,sun`. Repeat up to 16 times; the campaign holds in any of them. |
| `--monthly SPEC` | string | no | none | Hold only on this monthly occurrence: a day of the month (`15` or `day15`; a month without that day skips it) or the nth weekday (`1st-mon`, `2nd-tue`, `3rd-wed`, `4th-thu`, `last-sun`), each with an optional `@HH:MM-HH:MM`. Repeat up to 16 times. |
| `--date SPEC` | string | no | none | Hold only on this day, `YYYY-MM-DD` or `YYYY-MM-DD@HH:MM-HH:MM`. Repeat up to 100 times. |
| `--except DATE` | string | no | none | Skip these dates, `YYYY-MM-DD`, repeated or comma-separated, up to 100. An exception removes every occurrence that starts on that date, overnight tails included. |
| `--data KEY[:PATH[=VALUE]]` | string | no | none | Hold only while card data `KEY` (a card data key or card ID, or `{screen.weather}`) exists, optionally at the RFC 6901 pointer `PATH` or equal to `VALUE`. Give it once. |
| `--on-touch [IDLE_S]` | integer | no | off | Hold only while someone is using the screen by touch or click, until `IDLE_S` seconds, 5 to 3600 (default 60), pass without input. |
| `--displays N[,N]` | string | no | every output | Play only on these video wall outputs, 1 to 12, comma-separated. |
| `--start WHEN` | string | no | `next-item` | `on-time` switches at the edge and interrupts the current item; `next-item` waits for it to end. |
| `--covers WHERE` | string | no | `full` | Where the playlist plays: `full` (the whole page), `main` (the page itself), or `header`, `footer` or `side`, a strip drawn over the page that needs a playlist made for that region (`playlist create --format-region`). See [strips](https://screenrig.ai/docs/players.md#strips). A strip is refused with `playlist_format_mismatch` (exit 5) when it is laid out on none of the audience's screens. |
| `--priority N` | integer | no | 0 | 0 to 100. The higher number wins when two campaigns hold on one screen. |
| `--paused` | boolean | no | off | Create it paused: a draft that is on no screen until `campaign publish` or `campaign resume`. |
| `--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 campaign create --name "Lunch menu" --playlist-id pl_LUNCH --tag north --window mon-fri@11:00-14:00
screenrig campaign create --name "North Tower welcome" --playlist-id pl_WELCOME --folder sfd_01EXAMPLEFOLDER000000000
screenrig campaign create --name "Holiday hours" --playlist-id pl_HOLIDAY --tag Lobby --from 2026-12-20T00:00 --until 2027-01-02T00:00 --priority 10
screenrig campaign create --name "North cafes" --playlist-id pl_CAFE --audience '{"all":[{"tag":"North"},{"any":[{"tag":"Cafe"},{"tag":"Patio"}]}]}' --paused
screenrig campaign create --name "Brunch" --playlist-id pl_BRUNCH --tag Cafe --monthly 1st-sun@10:00-14:00 --except 2026-12-06
screenrig campaign create --name "Touch to browse" --playlist-id pl_MENU --screen-id scr_KIOSK --on-touch 90
screenrig campaign create --name "Holiday opening" --playlist-id pl_HOLIDAY --tag Lobby --date 2026-12-24@18:00-22:00 --start on-time
screenrig campaign create --name "Wall outputs 1 and 2" --playlist-id pl_WALL --screen-id scr_WALL --displays 1,2
screenrig campaign create --name "Room availability" --playlist-id pl_LIVE --folder sfd_01EXAMPLEFOLDER000000000 --data "crd_ROOM:/available=true"
screenrig campaign create --name "Fire exits" --playlist-id pl_EXITS --folder sfd_01EXAMPLEFOLDER000000000 --note "Fire safety"
```

Tags are 1 to 32 letters or digits. `DAYS` is `daily`, a day (`mon`), a range (`mon-fri`; `fri-mon` wraps past the week's end) or a comma list (`sat,sun`). Without a time the window is the whole day, and an end at or before the start crosses midnight and belongs to the start day. In an `--audience` rule each member has exactly one of `all`, `any`, `tag`, `screen_id` or `folder`, groups nest at most 3 deep, an audience has at most 500 rules, and `{"all":[]}` is every screen. The server refuses a dead rule: an `--except` date outside `--from` and `--until`, a `--date` that starts outside them, and a `--date` that is also an exception (`invalid_request`, exit 8).

`--data KEY` holds while card data `KEY` exists, `--data KEY:PATH` while a value exists at the RFC 6901 pointer `PATH`, and `--data KEY:PATH=VALUE` while it equals `VALUE`, a JSON scalar (`true`, `false`, `null`, a number or a JSON-quoted string) or a bare string such as `rain`. Objects and arrays are refused. Missing or expired data never matches, and a present `null` or `false` counts as existing. `KEY:=VALUE` compares the whole document. Escape a colon in `KEY` as `\:`, and quote the argument to keep JSON string quotes: `--data 'crd_ROOM:/status="true"'`. The key `{screen.weather}` reads the weather at each screen's own location (`screen update ID --location LAT,LON`): `--data '{screen.weather}:/condition=rain'`.

After an interrupted or ambiguous create, rerun the identical command within 24 hours: it reuses the saved idempotency key, and the server returns the same campaign instead of creating another.

### `campaign update`

Change a campaign; the screens of its old and new audience re-resolve within a minute.

```console
campaign update <id> [--name NAME] [--playlist-id ID] [audience option] [--from TIME | --clear-from] [--until TIME | --clear-until] [--window SPEC | --clear-windows] [--monthly SPEC | --clear-monthly] [--date SPEC | --clear-dates] [--except DATES | --clear-except] [--data KEY[:PATH[=VALUE]] | --clear-data] [--on-touch [IDLE_S] | --clear-on-touch] [--displays N[,N] | --clear-displays] [--start WHEN] [--covers WHERE] [--priority N] [--expect-rev REVISION] [--note TEXT]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `cmp_` campaign id. |
| `--name NAME` | string | no | unchanged | Rename the campaign. |
| `--playlist-id ID` | string | no | unchanged | Show this playlist instead. It must be approved. |
| `--tag`, `--any-tag`, `--screen-id`, `--folder`, `--all-screens`, `--audience` | string | no | unchanged | At most one, spelled as in `campaign create`. It replaces the whole audience. |
| `--from TIME`, `--until TIME` | string | no | unchanged | A civil time as in `campaign create`. `--from` must stay earlier than `--until`, else `invalid_request` (exit 8). |
| `--clear-from`, `--clear-until` | boolean | no | off | Remove the start or the end. |
| `--window SPEC` | string | no | unchanged | Replaces every window with these. Repeat up to 16 times. |
| `--clear-windows` | boolean | no | off | Remove every window. |
| `--monthly SPEC`, `--date SPEC`, `--except DATE` | string | no | unchanged | As in `campaign create`; each replaces every rule of its kind. |
| `--data KEY[:PATH[=VALUE]]` | string | no | unchanged | As in `campaign create`; it replaces the card-data condition. |
| `--on-touch [IDLE_S]`, `--displays N[,N]`, `--start WHEN` | string | no | unchanged | As in `campaign create`; `--on-touch` without a value is 60 seconds. |
| `--clear-monthly`, `--clear-dates`, `--clear-except`, `--clear-data`, `--clear-on-touch`, `--clear-displays` | boolean | no | off | Remove them. A campaign with none of its rules holds always. |
| `--covers WHERE` | string | no | unchanged | `full`, `main`, `header`, `footer` or `side`, as in `campaign create`. |
| `--priority N` | integer | no | unchanged | 0 to 100. |
| `--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 campaign update cmp_LUNCH --playlist-id pl_LUNCH2 --expect-rev 2
screenrig campaign update cmp_LUNCH --tag Lobby --window sat,sun --priority 20
screenrig campaign update cmp_LUNCH --clear-until --clear-windows
screenrig campaign update cmp_BRUNCH --monthly 2nd-sun@10:00-14:00 --except 2026-12-13,2027-01-10
screenrig campaign update cmp_BROWSE --clear-on-touch --clear-displays
screenrig campaign update cmp_ROOMS --data crd_ROOM:/available=true
screenrig campaign update cmp_ROOMS --clear-data
screenrig campaign update cmp_LUNCH --playlist-id pl_LUNCH2 --note "Safety review done"
```

Pass at least one change; only the fields you pass change. A new `--playlist-id` is checked like create. After an interrupted or ambiguous request, rerun the identical command: it reuses the saved idempotency key, and the server returns the update that already happened instead of applying it again.

### `campaign pause`

Stop a campaign on every screen without deleting it.

```console
campaign pause <id> [--expect-rev REVISION] [--note TEXT]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `cmp_` campaign 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 campaign pause cmp_LUNCH
screenrig campaign pause cmp_LUNCH --note "Holiday over"
```

Screens that showed it return to what they would show without it within a minute. `campaign resume` starts it again.

### `campaign resume`

Start a paused campaign again.

```console
campaign resume <id> [--expect-rev REVISION] [--note TEXT]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `cmp_` campaign 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 campaign resume cmp_LUNCH
screenrig campaign resume cmp_LUNCH --note "Holiday again"
```

Every screen in the audience that the campaign can apply to switches to its playlist within a minute while the campaign holds.

### `campaign delete`

Delete a campaign.

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

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `cmp_` campaign 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 campaign delete cmp_LUNCH
screenrig campaign delete cmp_LUNCH --note "Replaced by the winter campaign"
```

The campaign does not come back. Every screen showing it returns to what it would show without it within a minute; playlists, screens and tags stay. `campaign pause` stops a campaign and keeps it.

### `campaign screens`

Show the screens a campaign reaches and how each stands.

```console
campaign screens <id> [--limit N] [--cursor CURSOR]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `cmp_` campaign id. |
| `--limit N` | integer | no | 100 | Screens per page, 1 to 500. |
| `--cursor CURSOR` | string | no | the first page | The `data.next_cursor` of the previous page. |

```console
screenrig campaign screens cmp_LUNCH
```

`data.counts` covers every live screen in the audience: `audience`, then one count per standing present. `data.items` is one page of those screens in screen id order, each with the campaign's `status`. `data.next_cursor` is absent on the last page. On a screen whose Player resolves its own stack, the standing is where that stack puts the campaign: a strip or `main` campaign plays in its region and a `--displays` campaign on its displays.

| Status | Meaning |
| --- | --- |
| `playing` | The screen shows the campaign's playlist. |
| `pending` | It should show, and the switch lands within a minute. |
| `waiting` | Outside `--from`, `--until`, the windows, the monthly rules or the dates now; `next_start` when it starts within eight days. |
| `ended` | `--until` has passed. |
| `outranked` | It holds, but a screen takeover (`reason` `takeover`) or another campaign (`reason` `campaign`, with `by`) wins. |
| `conditional` | It holds by time and plays while someone is using the screen (`reason` `interaction`) or while its data condition holds (`reason` `data`). Only the Player sees its input and card data. |
| `paused` | The campaign is paused. |
| `blocked` | It cannot show here. `reason` is `no_default_playlist` (`screen assign`), `playlist_unavailable` (the playlist is missing, unapproved or does not fit the screen), `region_not_laid_out` (it fits no display of the screen: its strip fits none, or its `--displays` names none, and the campaign is left out of its stack) or `stack_only` (the screen's Player does not resolve its own stack, and only one that does honors a region, a display lock, `--on-touch` or a data condition). |

### `campaign explain`

Show why each campaign plays on one screen or not, now or at a chosen time.

```console
campaign explain <screen-id> [--at TIME] [--to TIME]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<screen-id>` | string | yes | none | The `scr_` screen id. |
| `--at TIME` | string | no | now | Explain this time instead of now: an instant with a zone (`2026-12-24T10:00:00-08:00`) or a time on the screen's own clock (`2026-12-24T18:00`). A clock time that daylight saving skips or repeats is refused. |
| `--to TIME` | string | no | none | Also list the playlist switches from `--at` (default now) to this time, in the same forms. There is no limit on how far; one answer lists at most 500 spans, and each stretch of eight days with no change counts as one. |

```console
screenrig campaign explain scr_LOBBY
screenrig campaign explain scr_LOBBY --at 2026-12-24T18:00 --to 2026-12-26T00:00
```

`data.items` lists every campaign of the project in the order the screen reads them, each with a standing from `campaign screens` or `not_in_audience`; a `blocked` row names the fix. `data.effective_playlist` is the playlist the screen plays now (`id`, `source`, which is `takeover`, `campaign`, `schedule`, `default` or `folder`, and `entry_id`, the campaign, schedule entry or folder id), or `null` when the screen shows no playlist. It changes nothing.

With `--at`, `data.at` holds the answer for that time: `instant`, `timezone` (the screen's effective timezone) and `local` (the instant on the screen's clock), `effective_playlist` (its `until` is the next change) and a `verdicts` row for the takeover, every campaign and every schedule entry, in the order the screen reads them.

A verdict is `won` (the playlist shown), `outranked` (it holds, and `by` names the winner), `not_started` (before its start), `ended` (past its end), `outside_hours` (inside its dates, outside its windows, monthly rules or dates, or on an exception date; `next_start` says when it holds again within eight days), `conditional` (it holds by time and plays while someone is using the screen, `reason` `interaction`, or while its data condition holds, `reason` `data`) or `off` (it does not apply, and `reason` says why: `paused`, `not_in_audience`, `no_default_playlist`, `playlist_unavailable`, `region_not_laid_out` or `stack_only`). On a screen whose Player resolves its own stack, the verdicts read that stack: a strip or `main` level wins or is outranked in its region, and a display lock on its displays. `effective_playlist` and the timeline stay the page on display 1, and `data.at.not_judged` lists `player_stack`.

`--to` adds `data.at.timeline`, the spans in which the playlist does not change, and `timeline_truncated` marks an answer that stopped before `--to`: ask again with `--at` set to the last span's `to`. `data.at.not_judged` says what is not judged: the takeover, schedule and campaigns are read as they are now, and `data.items` stays the state now.

### `campaign preview`

Show what creating or changing a campaign would do before making it.

```console
campaign preview [<id>] [--name NAME] [--playlist-id ID] [audience option] [rule flags] [clear flag] [--paused | --publish] [--calendar-from INSTANT] [--calendar-to INSTANT]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | no | a new campaign | The `cmp_` campaign to preview a change to. Omit it to preview a new campaign from the `campaign create` flags. |
| `--name NAME`, `--playlist-id ID` | string | for a new campaign | unchanged | As in `campaign create`; a new campaign needs both. |
| `--tag`, `--any-tag`, `--screen-id`, `--folder`, `--all-screens`, `--audience` | string | for a new campaign | unchanged | At most one, spelled as in `campaign create`; a new campaign needs one. With an `<id>` it replaces the whole audience. |
| `--from TIME`, `--until TIME`, `--window SPEC`, `--monthly SPEC`, `--date SPEC`, `--except DATE` | string | no | unchanged | The rule flags of `campaign create`. With an `<id>`, each replaces every rule of its kind. |
| `--data KEY[:PATH[=VALUE]]`, `--on-touch [IDLE_S]`, `--displays N[,N]`, `--start WHEN`, `--covers WHERE` | string | no | unchanged | As in `campaign create`. |
| `--priority N` | integer | no | unchanged | 0 to 100. |
| `--clear-from`, `--clear-until`, `--clear-windows`, `--clear-monthly`, `--clear-dates`, `--clear-except`, `--clear-data`, `--clear-on-touch`, `--clear-displays` | boolean | no | off | As in `campaign update`: preview removing the rule. They need an `<id>`. |
| `--paused` | boolean | no | off | Preview it as a draft: paused, on no screen. Not with `--publish`. |
| `--publish` | boolean | no | off | Preview unpausing the campaign. With an audience option, preview publishing it to that audience. It needs an `<id>` and excludes `--paused`. |
| `--calendar-from INSTANT` | string | no | now | Start the calendars at this instant with a zone, such as `2026-12-01T00:00:00Z`. |
| `--calendar-to INSTANT` | string | no | a week after the start | End the calendars at this instant with a zone, at most 93 days after the start. |

```console
screenrig campaign preview cmp_LUNCH --tag Lobby
screenrig campaign preview cmp_LUNCH --clear-until --priority 20
screenrig campaign preview cmp_LUNCH --publish --folder sfd_TEST
screenrig campaign preview cmp_LUNCH --publish --calendar-from 2026-12-01T00:00:00Z --calendar-to 2027-01-01T00:00:00Z
screenrig campaign preview --name "Holiday hours" --playlist-id pl_HOLIDAY --tag Lobby --from 2026-12-20T00:00 --until 2027-01-02T00:00
```

The flags are checked as the write checks them, so a preview refuses what the write refuses. With an `<id>`, pass at least one change: a `campaign update` flag, `--paused` or `--publish`. Nothing is written. `data.gained` and `data.lost` are `{count, screen_ids}` (the first 500 by id): the screens that would carry the campaign and do not now, and those that would no longer carry it. A paused campaign is on no screen, so publishing one gains its audience; an edit that keeps a screen's campaign shows in the calendars alone. `data.calendar.before` and `data.calendar.after` are the schedule calendars over `--calendar-from` to `--calendar-to` for the screens of the old and the new audience, with drafts drawn as paused levels. `--human` prints the first display's stretches and the drafts that would hold, and says when the calendar does not change.

### `campaign publish`

Unpause a draft campaign, optionally only to a test folder or a few test screens.

```console
campaign publish <id> [--test-folder ID | --test-screen ID] [--expect-rev REVISION]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `cmp_` campaign id. |
| `--test-folder ID` | string | no | the campaign's audience | Publish only to the screens in this `sfd_` screen folder and every folder inside it. |
| `--test-screen ID` | string | no | the campaign's audience | Publish only to these screens. Repeat or comma-separate. |
| `--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 campaign publish cmp_LUNCH
screenrig campaign publish cmp_LUNCH --test-folder sfd_TEST
screenrig campaign publish cmp_LUNCH --test-screen scr_LOBBY,scr_CAFE --expect-rev 2
```

A draft is a paused campaign: it is on no screen until it is published. Without a test option the campaign reaches its own audience within a minute. A test option replaces the audience in the same write, so only the test screens switch. The usual flow: `campaign create --paused` with the real audience, `campaign publish ID --test-folder FOLDER`, look at the test screens, then `campaign promote ID --revision N`. `campaign preview ID --publish` shows the effect first.

### `campaign promote`

Put a draft's audience on the campaign after its test.

```console
campaign promote <id> --revision N [--expect-rev REVISION]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `cmp_` campaign id. |
| `--revision N` | integer | yes | none | The draft's paused revision, as `campaign revisions` numbers it. The campaign takes that revision's audience. |
| `--expect-rev REVISION` | string | no | the revision just read | Require this current campaign revision. Without it the CLI reads the campaign just before the write and sends that revision; a change in between is `revision_conflict` (exit 6). |

```console
screenrig campaign promote cmp_LUNCH --revision 1
screenrig campaign promote cmp_LUNCH --revision 1 --expect-rev 2
```

The audience of revision `N` becomes the campaign's in one write, a new revision whose `change` is `promoted`, and every screen of the old and the new audience re-resolves within a minute. Revision `N` must be a paused revision (`invalid_request` names it otherwise), and the campaign must be live: a campaign that is paused now has no test to promote. A revision whose audience is already the campaign's changes nothing. `--expect-rev` is the campaign's current revision, not `N`. Promote needs the access `campaign update` needs.

### `campaign revisions`

List a campaign's revisions, newest first.

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

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `cmp_` campaign id. |
| `--after CURSOR` | string | no | the first page | Continue from the `data.next_cursor` of the previous page. |

```console
screenrig campaign revisions cmp_LUNCH
screenrig campaign revisions cmp_LUNCH --after CURSOR
```

`data.items` holds at most 50 revisions, each with `revision`, `created_at`, `change`, `current`, `author` when known and `campaign`, the campaign as it was after that write, with its paused state. `change` is `created`, `edited` (an update or a schedule import), `restored` or `promoted` (a copy of the earlier revision it names in `restored_from`), or `baseline`, the revision the campaign held when its history began. A campaign whose history has no unpaused revision is a draft. While `data.next_cursor` is a string, older revisions remain: run the command again with `--after` set to it. Revisions are kept as long as the campaign is and go when it is deleted. Read only access is enough.

### `campaign restore`

Make an earlier revision current again by copying it forward.

```console
campaign restore <id> --revision N [--expect-rev REVISION]
```

| Parameter | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `<id>` | string | yes | none | The `cmp_` campaign id. |
| `--revision N` | integer | yes | none | The revision to copy forward, as `campaign revisions` numbers it. |
| `--expect-rev REVISION` | string | no | the revision just read | Require this current campaign revision. Without it the CLI reads the campaign just before the write and sends that revision; a change in between is `revision_conflict` (exit 6). |

```console
screenrig campaign restore cmp_LUNCH --revision 3
screenrig campaign restore cmp_LUNCH --revision 3 --expect-rev 5
```

The name, playlist, audience, rules, region, priority and paused state of revision `N` become a new revision whose `change` is `restored`, checked as `campaign update` is checked: its playlist must still exist and be approved (`playlist_not_approved`, exit 5). Every screen of the old and the new audience re-resolves within a minute. Earlier revisions stay as they were, and restoring the current content changes nothing. Restore needs the access `campaign update` needs.

## Other command groups

- [Projects](https://screenrig.ai/docs/cli/projects.md)
- [Service clients](https://screenrig.ai/docs/cli/service-clients.md)
- [Help and support](https://screenrig.ai/docs/cli/help.md)
- [Media](https://screenrig.ai/docs/cli/media.md)
- [Media sync](https://screenrig.ai/docs/cli/media-sync.md)
- [Playlists](https://screenrig.ai/docs/cli/playlists.md)
- [Cards](https://screenrig.ai/docs/cli/cards.md)
- [Applications](https://screenrig.ai/docs/cli/applications.md)
- [Screens](https://screenrig.ai/docs/cli/screens.md)
- [Schedules](https://screenrig.ai/docs/cli/schedules.md)
- [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)
