Documentation

CLI command reference

Find the commands, parameters, JSON output, and revision rules for publishing content and operating screens.

Every command runs the same way

screenrig [--json | --human] [--api-url URL] [--config PATH] [--request-id ID]
      [--project-id ID] [--idempotency-key KEY] [--timeout MS] [--beta-key KEY] <command> [args]

Global flags

FlagTypeDefaultNotes
--jsonbooleanJSON for operationsExplicit JSON output, retained for compatibility. Also selects structured help. Cannot be combined with --human.
--humanbooleanoffSelect human-readable operational output. Cannot be combined with --json.
--project-id IDstringlast-used projectFix this invocation to a cached project without changing the selection. Each command keeps its target and recovery state for its full run.
--api-url URLstringhttps://api.screenrig.aiControl-plane origin. SCREENRIG_API_URL sets the same thing.
--config PATHpaththe user configuration fileUse a specific configuration file. SCREENRIG_CONFIG sets the same thing.
--request-id IDstringgenerated per runYour own request id, echoed back as request_id. Keep it in reports.
--idempotency-key KEYstringderived per writeReuse the same key to retry an ambiguous write. The server returns the original result for twenty-four hours; a different body under the same key is idempotency_mismatch.
--timeout MSinteger30000 per requestBounds each HTTP request. On a command that waits for an operation it is the wait deadline instead, and the default is 120000.
--beta-key KEYstringunsetSent only when the control plane asks for one. SCREENRIG_BETA_KEY sets the same thing.
--help, -hbooleanoffPrint focused help for the command or group and exit. Add --json for structured help.
--version, -VbooleanoffPrint the bundled CLI version and exit. Same as the version command.

Environment

VariableEffect
SCREENRIG_API_URLControl-plane origin, overriding the stored configuration.
SCREENRIG_CONFIGPath to the configuration file.
XDG_CONFIG_HOMEBase of the configuration directory. Otherwise %APPDATA%\screenrig on Windows, or ~/.config/screenrig.
SCREENRIG_BETA_KEYBeta key, when the control plane asks for one.
SCREENRIG_FFMPEGAbsolute path to ffmpeg, when media upload should not take it from PATH.
SCREENRIG_FFPROBEAbsolute path to ffprobe.
SCREENRIG_CWEBPAbsolute path to cwebp, the fallback still encoder.
SCREENRIG_CLIENT_IDRun as this service client (scl_…), with exactly one of the next two.
SCREENRIG_CLIENT_KEY_FILEThe service client's private key file, a JWK or PEM, signed into a private_key_jwt assertion.
SCREENRIG_CLIENT_SECRETThe service client's secret, sent with HTTP Basic.

There is no token flag and no token environment variable. The CLI reads the session the plugin stored for this installation, in a user-private file that survives a plugin upgrade, and renews it on its own. With SCREENRIG_CLIENT_ID set, it signs in as that service client instead, keeps the token in memory, and never reads or writes the stored config.

Output

Operational commands write JSON to stdout by default, including errors. A single result carries an envelope: ok, then data, request_id, operation_id, and warnings[] on success, or error on failure. Progress, safe URLs, and human notes go to stderr. events follow emits NDJSON: one JSON envelope per event, on its own line. Redirecting stdout does not change the format.

Use --human for manual inspection. Event commands then print one logfmt line per event. Help and bare command groups remain readable text by default; use screenrig --json playlist --help for structured discovery. --json and --human cannot be combined.

Older plugin bundles may require explicit --json for operational results. Check the installed CLI's help when working with an older bundle. Playlist and Compose authoring files remain JSON documents; a command result envelope is not an editable playlist document.

An error envelope carries error.status, a stable error.code, and often error.next.command naming exactly what to run next. Branch on those rather than on prose.

Exit codes

CodeMeaning
0Success.
1Unexpected failure.
2Usage error. Nothing was sent.
3Credential problem. This installation cannot authenticate.
4Not found.
5Conflict.
6Precondition failed, including revision_conflict: the --expect-rev revision was stale.
7Rate limited. Honor Retry-After, surfaced as retry_after_seconds.
8Other client-side rejection.
9Server error.
10Network failure.
11Timed out.
12Configuration error.
13The operation itself failed.

Common problem codes in the envelope:

CodeMeaningNext step
not_enrolledThis installation holds no project credential yet.Create a project with screenrig agent enroll --email ADDRESS --organization NAME, or sign this installation in to a project you belong to with screenrig login.
session_endedThe sign-in was revoked, expired or ended.screenrig login, approved by a person in the dashboard.
insufficient_accessA Read only session tried to change, publish or spend.screenrig login --access manage, approved by a person.
login_denied, login_expiredThe person denied the sign-in, or its 10-minute code expired.Run screenrig login again.
client_auth_failedA service client's key, secret or id was refused.Check the SCREENRIG_CLIENT_* variables against screenrig service-client show ID.
capability_requiredThe project lacks the capability this command needs, such as screens or advertiser.screenrig project capabilities, then choose the matching --intent for a new project or change the project’s purpose.
rate_limitedToo many requests in the current window.Wait for retry_after_seconds, then retry.

Revisions and retries

Revision guards are optional. Omit --expect-rev to write the current resource without a prior revision read. Supply it with a known revision to reject stale writes. On revision_conflict, refetch the resource, reapply the change, and retry with the returned revision. On an ambiguous transport failure, retry with the same --idempotency-key; the control plane returns the original result rather than writing twice.

Credits

The control plane meters one credit per billed authenticated command and one per billed project-stream event. Five control-plane commands do not debit it and keep working at zero: project show, agent status, screen reload, screen toast, and screen screenshot. Local Compose work is never billed. Opening events follow costs one credit as the subscribe, and replay after a reconnect is free. A fleet command, screen assign, screen reload, screen toast, or screen tag with several screen ids or --tag, is one billed action with no screen-count cap, including all continuations, so a fleet reload or toast is billed even though a single one is free. screen screenshot --tag follows all list pages, each billed, to resolve the tagged screens; the captures themselves are free. Each webhook delivery attempt is metered like a billed listen-stream event. Each JSON page of playback plays or playback list is one billed request, and a playback CSV export is one billed request for its whole range.

Bundled CLI surface

Commands are grouped the way the CLI groups them. Installation and first use are covered in Start; local still rendering is covered in Compose.

Command names and discovery

Use show to inspect a resource, list for collections, and kv get / kv set for values. Use --name for display names, --screen-id, --app-id, --playlist-id, and --release-id for resource selectors, --expect-rev for revision guards, and --after for event cursors. The group is app; “application” is the resource name. Comments keep comment ACTION screen|playlist ID.

Compatibility aliases remain: operations get, playlist get, --if-match, K/V --application-id, screen pair/provision --label, playlist init --screen, and events --cursor. Supply only one spelling of an option. Response field names are unchanged.

Use screenrig help --all for the full inventory or screenrig playlist init --help for focused help. Add --json for argument metadata, choices, defaults, examples, aliases, and option relationships. exactlyOne, atLeastOne, and together groups are checked before configuration or network access; requires means the first option requires all remaining options. Read stdin only where command help explicitly supports -.

Create a new project

screenrig agent enroll --email ADDRESS --project-name "Lobby"

agent enroll creates an identity, organization, Screens project and membership, and stores identity and project credentials privately. Reuse the organization name already provided, or use Screens without a choice prompt. --organization is required; --name separately names the agent. Use the person's actual contact email. Enrollment emails a project-member invitation; it does not disclose whether that address already has a login. The person opens the invitation and creates a login with a passkey, or signs in to add the project to their existing login.

Add --intent advertising for a project that buys ads rather than pairing screens. --force replaces this installation's enrollment or pending connection state when intentionally creating another project.

agent enroll --email ADDRESS --organization NAME [--name NAME]
    [--intent signage|advertising] [--force]

Sign in with AgentID

An agent with an AgentID signs in at https://api.screenrig.ai/agentid/start, receives a claim code, installs the official plugin, and runs screenrig agent enroll --agentid-claim CODE --organization NAME. The owner's verified email receives the dashboard invitation.

Sign in to an existing account

screenrig login

login signs this installation in to an account that already exists. It prints a dashboard address and a code, then waits up to 10 minutes. A person opens the address, checks that the code matches, chooses the project, Read only or Manage, and the capability areas, and approves. The CLI stores the session, selects the approved project and returns status: signed_in with the project, organization and access.

login [--project ID] [--access read|manage] [--name NAME] [--no-wait] [--resume ID]
logout

--access asks for a level; the default is manage, and the person decides. --project suggests the project. --no-wait returns status: pending with the verification_uri_complete, the user_code and a login_ handle that holds no secret; login --resume ID waits for the approval later. Run login again on a signed-in installation to add another project or to raise a Read only project to Manage.

Each session carries capability areas: screens, content, playlists, advertising, reports, and project. A session calls only commands in its areas; reports also reads any area. A command outside them returns 403 forbidden with detail This agent credential lacks the <name> capability. Read only sessions list and read; a change returns 403 insufficient_access.

logout revokes the sign-in on the server and removes the stored tokens. A failure that might not have reached the server keeps them, so rerunning logout is safe. doctor shows when the session ends: 90 days after its last use, and at most one year after sign-in. See Authentication.

Project and organization lifecycle

The current project is the last one used. Every JSON envelope includes organization and project context; human output names both. Identity credentials are limited by the selected project's membership. Retry state stays separate per project.

project list
project use ID
project create NAME [--organization-id ID | --organization NAME]
project moves
project move --organization-id ID
project transfer-owner --user-id ID
project deletion-preview
project delete --name NAME --revision N --yes
organization list
organization rename ID NAME
agent revoke-identity --yes

project list verifies accessible memberships. project use selects one. Create requires a name unique in its organization and defaults to the current organization; specify an organization when there is no current project. The responsible person becomes owner. Ten free Standard projects per owner are allowed at creation, with enrollment governed by rate limits and covered projects allowed.

The owner can move into a destination returned by project moves, or transfer ownership to an existing verified member. A move carries screens and checks the destination payer's limit. Deletion uses the exact current name and positive revision from its preview. It requires zero non-archived screens, its own unshared Standard plan, no wallet obligations and no live advertising. It emails members and retains a tombstone.

Organization names can repeat; use explicit IDs when renaming an organization you administer. An organization has designated admins and no members. Its project list does not grant access to project content. Shared plan holders pool money, allowances and screen limits; usage remains attributed to projects and historical charges retain their original payer.

login --project ID asks for that project on the same identity. Only explicit approval of this request grants a membership. agent disconnect --yes revokes one membership; agent revoke-identity --yes separately revokes the identity and all memberships. An ambiguous response retains local credentials and retry state for reconciliation.

Advertising reports retain settlement facts beyond event retention. Read their coverage metadata: missing historical plays or unknown approved duration are unknown, never zero. A report can have complete financial totals and incomplete outcome history.

Projects, invitations, and diagnostics

Project commands list memberships, select a project, create projects, and manage their names, owners, moves and deletion. Invitations bring people into projects or advertising networks.

project show#

Read the project record and its remaining credit.

project show

No parameters.

screenrig project show

data.credit_remaining is a whole integer credit count. Below 1000 the envelope may carry a credits_low warning. Until 1 Jan 2027 usage is free within reason, so remaining credit is an integer meter an agent can read, not a shutoff instruction. Standard's free monthly allowances continue after that; extra usage is optional prepaid.

project rename#

Set the name people see in the dashboard project switcher.

project rename NAME

project capabilities#

Read the project's plan, independent screens and advertiser feature flags, and effective capability set. A route denial wins over a stale capability read.

project capabilities

invitations#

Create, list, or revoke project-member and ad-buyer invitations.

invitations create --email ADDRESS[,ADDRESS] [--kind member|ad-buyer] [--link]
    [--screen-id ID] [--slot-id ID] [--policy trusted|review_required]
invitations list [--kind member|ad-buyer] [--status STATUS]
invitations revoke ID

--kind defaults to member. Email delivery is the default and expires after seven days. --email accepts comma-separated recipients, up to fifty per request. --link is member-only, requires exactly one intended recipient, and returns a twenty-four-hour invitation URL once for direct handoff. Never put that URL in logs, notes, or a shared transcript. Lists never return it.

Ad buyers use email only and need an owned screen or slot scope (or both). --screen-id and --slot-id accept comma-separated IDs; --policy selects trusted or review_required. Status filters are queued, sent, issued, accepted, revoked, expired, and failed.

screenrig invitations create --email teammate@example.com
screenrig invitations create --kind ad-buyer --email buyer@example.com --screen-id scr_LOBBY --policy review_required

Agents do not accept invitations. The invited person opens the email, signs in or creates a passkey, and accepts in the dashboard. Link signup verifies their email first. Revoking an accepted invitation does not undo a membership; use the buyer membership controls to revoke advertising access.

agent status#

Report whether this installation holds a usable credential.

agent status

No parameters.

screenrig agent status

Read-only. It never changes local or server state, and it costs nothing.

login#

Sign this installation in to an existing account, with a person's approval in the dashboard.

login [--project ID] [--access read|manage] [--name NAME] [--no-wait] [--resume ID]
ParameterTypeRequiredDefaultNotes
--project IDstringnononeSuggest the project the person approves.
--access LEVELstringnomanageread or manage. The person makes the final choice.
--name NAMEstringnononeName this installation, at most 80 characters.
--no-waitbooleannooffReturn status: pending with the address, the code and a login_ handle.
--resume IDstringnononeWait for the pending sign-in that --no-wait started.
screenrig login --access read

Prints the dashboard address and an eight-letter code to stderr, then waits up to 10 minutes. The person checks that the code matches, chooses the project, the access level and the capabilities, and approves. The envelope reports status: signed_in, the project, the organization and access. Running it again on a signed-in installation adds a project or raises a Read only project to Manage. Denial is login_denied and expiry is login_expired.

logout#

End this installation's sign-in and remove its tokens.

logout

No parameters.

screenrig logout

Revokes the sign-in on the server, then removes the stored tokens and reports status: signed_out. A failure that might not have reached the server keeps them, so rerunning is safe. Run screenrig login to sign in again.

dashboard#

Open the ordinary dashboard URL. This command does not mint a sign-in link, create a session, or need an agent credential.

dashboard [open]
screenrig dashboard

The dashboard shows the person's projects after sign-in. Browser project selection does not change the project bound to an agent.

dashboard reset-sign-in#

Request a sign-in reset email without an agent credential.

dashboard reset-sign-in --email ADDRESS

The acknowledgment does not reveal whether the email has a login. The emailed reset expires after one hour; the person creates a new passkey. Completion replaces their sign-in credentials and revokes other sessions.

doctor#

Check this installation and report every check by name.

doctor [--repair-config]
Parameter Type Required Default Notes
--repair-config boolean no off Tighten the permissions on an existing configuration file that is readable by group or others.
screenrig doctor

data.checks names each check and its status: the Node runtime, the resolved configuration path and its permissions, the control-plane origin and its service routes, the advertised feedback capability, and the optional media toolchain (ffmpeg, ffprobe, encoder_libx264, encoder_libx265, encoder_libwebp, cwebp, filter_hdr_tonemap).

Any failed check makes doctor exit non-zero, so read the individual names before calling the installation unhealthy. A missing media toolchain affects media upload alone.

version#

Print the bundled CLI version and its protocol adapter.

version

No parameters.

screenrig version

Run this first in a session. A successful envelope proves the launcher resolved the bundled CLI. --version and -V do the same thing from anywhere on the command line.

Service clients

A service client is a server, script or CI job owned by the project, signing in with its own key or secret instead of a person's approval. Managing service clients needs the project capability and Manage; a service client never manages clients. To run commands as the client, set SCREENRIG_CLIENT_ID with SCREENRIG_CLIENT_KEY_FILE or SCREENRIG_CLIENT_SECRET. See Service clients.

service-client list#

List this project's service clients, live and revoked, with their keys, secret ids and usage.

service-client list

No parameters.

screenrig service-client list

Never returns a secret.

service-client show#

Show one service client.

service-client show ID
ParameterTypeRequiredDefaultNotes
IDstringyesnoneThe client id, scl_….
screenrig service-client show scl_ID

Lists each key's kid and each secret's css_ id for rotation.

service-client create#

Create a service client owned by this project.

service-client create --name NAME [--capability NAME]... [--access read|manage] [--key-file PATH] [--secret-file PATH]
ParameterTypeRequiredDefaultNotes
--name NAMEstringyesnoneAt most 80 characters.
--capability NAMEstringnoall sixRepeatable: screens, content, playlists, advertising, reports, project, each within yours.
--access LEVELstringnoreadread or manage.
--key-file PATHpathone of thesenoneA JWK or PEM: Ed25519, EC P-256 or RSA of at least 2048 bits. Only the public half is sent.
--secret-file PATHpathone of thesenoneGenerate a secret and write it to this new 0600 file. It is shown once.
screenrig service-client create --name "Nightly report" --access read --capability reports --key-file screenrig-client.pem

Needs the project capability and Manage. At most 50 live clients per project. A key without its own kid is known by its RFC 7638 thumbprint.

service-client add-key#

Register a second public key for a rotation without downtime.

service-client add-key ID --key-file PATH
ParameterTypeRequiredDefaultNotes
IDstringyesnoneThe client id.
--key-file PATHpathyesnoneThe new public key, a JWK or PEM.
screenrig service-client add-key scl_ID --key-file new.pem

A client holds at most two keys.

service-client add-secret#

Generate a second secret for a rotation without downtime.

service-client add-secret ID --secret-file PATH
ParameterTypeRequiredDefaultNotes
IDstringyesnoneThe client id.
--secret-file PATHpathyesnoneA new 0600 file that receives the secret.
screenrig service-client add-secret scl_ID --secret-file new-secret

A client holds at most two secrets. The current one keeps working until you remove it.

service-client remove-key#

Remove a key; tokens issued with it stop working within seconds.

service-client remove-key ID --kid KID
ParameterTypeRequiredDefaultNotes
IDstringyesnoneThe client id.
--kid KIDstringyesnoneThe key id from service-client show.
screenrig service-client remove-key scl_ID --kid KID

A client keeps at least one key or secret.

service-client remove-secret#

Remove a secret; tokens issued with it stop working within seconds.

service-client remove-secret ID --secret-id css_ID
ParameterTypeRequiredDefaultNotes
IDstringyesnoneThe client id.
--secret-id IDstringyesnoneThe css_ secret id from service-client show.
screenrig service-client remove-secret scl_ID --secret-id css_ID

service-client revoke#

Revoke a service client permanently.

service-client revoke ID --yes
ParameterTypeRequiredDefaultNotes
IDstringyesnoneThe client id.
--yesbooleanyesoffConfirm the permanent revocation.
screenrig service-client revoke scl_ID --yes

Deletes its keys and secrets; its tokens stop working within seconds. The record stays for audit.

Media

A media object is one still, one clip, or one MP3 audio track, stored as exactly one rendition. There is no per-device fallback, so whatever media upload or media generate stores is what every screen has to decode.

media generate#

Generate a presentable whole-page still into the project media store.

media generate --prompt TEXT [--aspect-ratio RATIO] [--quality low|medium|high] [--tag TAG]
               [--no-progress]
ParameterTypeRequiredDefaultNotes
--prompt TEXTstringyesnone1 to 4000 characters. Put every fact and all copy in the prompt so the image model typesets it.
--aspect-ratio RATIOstringno16:91:1, 16:9, 9:16, 4:3, 3:4, 3:2, or 2:3.
--qualitystringnomediumlow, medium, or high. Changes how detailed the still is. Generate is billed per token: $10 / 1M text input, $16 / 1M image input, $60 / 1M image output.
--tag TAGstringnononeSame 1 to 32 letter-or-digit tag as upload.
--no-progressbooleannooffSuppress the stderr started line.
screenrig media generate --prompt "A 16:9 restaurant menu poster for North Lobby Cafe. Dark walnut, gold type. Title NORTH LOBBY CAFE. Items: Espresso $4, Cortado $4.50, House Blend $3. Footer Open 7am to 7pm. Typeset every line of copy in the image." --aspect-ratio 16:9 --quality high --tag LobbyMenu

The command stores a lossy WebP in the project media store and returns data.media_id (med_…). Do not re-upload the bytes. Never print pixels or the prompt. The authoring tree and prompt contract live on Compose.

media upload#

Convert a local file to delivery bytes, upload it, and wait until the object is ready.

media upload <file> [--content-type TYPE] [--tag TAG] [--no-wait] [--poll-ms MS]
             [--timeout MS] [--no-transcode] [--codec h264|hevc] [--max-fps N]
             [--max-edge PIXELS] [--webp-quality 1-100] [--no-progress]
             [--preset signage-1080p30|signage-4k30] [--no-audio]
ParameterTypeRequiredDefaultNotes
<file>pathyesnoneThe source still, clip, or audio track. The filename becomes the human handle in listings, playlists, and playback reporting, so name it for what it shows.
--content-type TYPEstringnodetected from the uploaded bytesDeclare the media type yourself instead of letting the CLI detect it.
--tag TAGstringnononeOne to 32 letters or digits, matching ^[A-Za-z0-9]{1,32}$. Hyphens are rejected. A tag is what makes media list --tag TAG a reliable filename-to-id map.
--no-waitbooleannooffReturn as soon as the commit is accepted. The object is not ready yet; finish with operations wait.
--poll-ms MSintegerno1000Interval between operation polls while waiting.
--timeout MSintegerno120000Deadline for the wait. It also bounds each underlying request.
--no-transcodebooleannooffUpload the source bytes unchanged. ffmpeg, ffprobe, and cwebp are not run. Lossless WebP is still rejected.
--codecstringnoh264h264 or hevc; avc and h265 are accepted as aliases. Use hevc only when every screen that will play the clip is a native Player.
--max-fps Nnumberno30Frame-rate cap. Greater than 0 and at most 240.
--max-edge PIXELSintegerno3840Bound applied to each edge, from 16 to 3840. Aspect ratio is preserved and a smaller source is never upscaled.
--webp-qualityintegerno90Still quality for the WebP encode, from 1 to 100.
--presetstringnounsetsignage-1080p30 or signage-4k30. Orientation-aware video size and 30 fps caps. --max-edge and --max-fps can tighten a preset but cannot enlarge it. Video only; cannot combine with --no-transcode.
--no-audiobooleannooffOmit the audio track from video delivery. Video only; cannot combine with --no-transcode.
--no-progressbooleannooffEmit no progress output. Progress always goes to stderr; stdout stays reserved for the one result envelope.
screenrig media upload ./lobby-welcome-loop.mp4 --tag LobbyLoop

Video becomes an MP4: H.264 High profile level 4.2 by default, or H.265 with --codec hevc, in yuv420p tagged Rec. 709. An HDR source is tone mapped to Rec. 709. Audio, where the source has any, is AAC at 192 kbit/s, 48 kHz, stereo.

Stills become lossy WebP at quality 90, keeping alpha where the source has it. An animated source loops forever. A WebP that already fits the size bound is passed through rather than re-encoded.

Audio becomes an MP3 for a playlist soundtrack. An MP3 source uploads unchanged; WAV, AAC, M4A, OGG and FLAC sources are converted to a 192 kb/s MP3, which needs an ffmpeg build with libmp3lame. A track runs from 1 second to 4 hours. playlist init turns audio inputs into soundtrack tracks rather than pages. The ready object is an audio media object with a duration. It cannot be placed on a page; name it in the playlist audio soundtrack instead.

The ready object id is data.media_id. The same value is data.id and data.operation.result.media_id. That id is what a playlist selector takes.

A low-information filename such as video.mp4 adds an advisory generic_filename warning to the envelope. The upload still succeeds and the CLI never renames the file; pick the name before you run the command.

Conversion runs ffmpeg and ffprobe from PATH, or from the absolute paths in SCREENRIG_FFMPEG and SCREENRIG_FFPROBE. Stills prefer the ffmpeg libwebp encoder and fall back to cwebp on PATH or SCREENRIG_CWEBP. Run doctor before the first upload of a session.

--preset signage-1080p30 bounds the long edge to 1920, the short edge to 1080, and the rate to 30 fps. signage-4k30 bounds the long edge to 3840, the short edge to 2160, and the rate to 30 fps. Both work with landscape, portrait, and square content. --no-audio omits audio encoding for silent clips. These options apply only to video.

screenrig media upload ./lobby.mov --preset signage-1080p30 --no-audio

media upload-batch#

Upload many local files from a manifest, resuming from a required state file.

media upload-batch <manifest.json> [--state FILE] [--concurrency N]
                   [--no-transcode] [--tag TAG] [--no-progress]
ParameterTypeRequiredDefaultNotes
<manifest.json>pathyesnone{ "items": [ { "path": "./a.png", "tag"?: "Lobby", "content_type"?: "image/png" } ] } with 1 to 1000 items. Paths are relative to the manifest file.
--state FILEpathnoprivate file beside configA 0600 JSON file keyed by the SHA-256 of each source file's local bytes. Items already present with a media_id are reported as resumed. Do not share one state file across projects.
--concurrency Nintegernothe CLI defaultParallel uploads.
--no-transcodebooleannooffSame as media upload.
--tag TAGstringnononeApplied when an item omits its own tag.
--no-progressbooleannooffEmit no progress output.
screenrig media upload-batch ./images.json --state ./upload-state.json

The envelope carries data.items[] in manifest order, each with path, source_filename, sha256, outcome (accepted or resumed), and media_id. data.failed[] carries items that did not land.

media show#

Read one media object, including its current revision.

media show <id>
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe med_ object id.
screenrig media show med_01EXAMPLEIMAGE0000000000

media download#

Write a stored rendition to disk for inspection. Never print the bytes.

media download <id> [--output FILE]
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe med_ object id.
--output FILEpathno./<id>.<ext> in the current directoryA file path, not a directory. An existing file is overwritten. The extension follows the content type.
screenrig media download med_01EXAMPLEIMAGE0000000000 --output ./lobby.webp

The envelope reports media_id, path, bytes, sha256, content_type, primitive, filename, optional source_filename, and width / height. It never carries pixels. Use this to proof a generated still before it goes on glass.

media list#

List media objects, optionally filtered by tag and primitive.

media list [--tag TAG] [--primitive image|video|audio]
ParameterTypeRequiredDefaultNotes
--tag TAGstringnononeMatch ^[A-Za-z0-9]{1,32}$. Untagged objects are omitted whenever this filter is present.
--primitivestringnoallimage, video, or audio. Restrict the listing to stills, clips, or soundtrack tracks.
screenrig media list --tag LobbyLoop --primitive video

media update#

Change or clear the tag on one object. Nothing else about a media object is patchable.

media update <id> (--tag TAG | --tags TAG1,TAG2 | --clear-tag) [--expect-rev REVISION]
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe med_ object id.
--tag TAGstringone of the twononeThe replacement tag, one to 32 letters or digits.
--clear-tagbooleanone of the twooffSend null and leave the object untagged.
--expect-rev REVISIONstringnononeOptional revision precondition. Omit to write the current resource. A supplied stale value is revision_conflict: refetch, reapply, retry.
screenrig media update med_01EXAMPLEIMAGE0000000000 --tag LobbyLoop

media delete#

Delete one media object.

media delete <id> [--expect-rev REVISION]
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe med_ object id.
--expect-rev REVISIONstringnononeOptional revision precondition. Omit to write the current resource. A supplied stale value is revision_conflict: refetch, reapply, retry.
screenrig media delete med_01EXAMPLEIMAGE0000000000

Applications

An application is one already-built static directory with a root index.html. The packer injects the browser SDK at _screenrig/runtime.js and adds its script tag, so the app reaches window.screenrig with no build step and nothing to add to its dependencies. Nothing in the directory is ever built or executed during packing.

app pack#

Pack a static directory into a deterministic archive without uploading it.

app pack <directory> [--output FILE]
ParameterTypeRequiredDefaultNotes
<directory>pathyesnoneThe built static directory. It needs a root index.html.
--output FILEpathnonothing is writtenAlso write the archive to this path. Without it the command reports the archive and keeps it in memory.
screenrig app pack ./lobby-board --output ./lobby-board.tgz

The envelope reports sha256, compressed_bytes, expanded_bytes, file_count, the entry list, and whether the SDK runtime was injected. app upload packs the directory itself, so reach for this command only when you want to inspect the archive first.

Applications run under a content security policy that blocks inline code. Packing moves inline style and script blocks into files under _screenrig/inline/ and links them in. Inline event handlers such as onclick= and javascript: URLs cannot be moved; packing refuses them and names the file and line.

app upload#

Pack a directory, publish it as a new application release, and wait for the publication to succeed.

app upload <directory> [--name NAME] [--no-wait] [--poll-ms MS] [--timeout MS]
ParameterTypeRequiredDefaultNotes
<directory>pathyesnoneThe built static directory.
--name NAMEstringnononeApplication name, at most 120 characters.
--no-waitbooleannooffReturn before the publication operation finishes. Run operations wait before pinning the release into a playlist.
--poll-ms MSintegerno1000Interval between operation polls while waiting.
--timeout MSintegerno120000Deadline for the wait. It also bounds each underlying request.
screenrig app upload ./lobby-board --name "Lobby board"

Read three fields: data.operation.state is succeeded, data.application.release_id is the rel_ id an application primitive pins, and data.application.id is the app_ id the kv commands take. A succeeded operation, not a release id on its own, is what means the release is usable.

Every app upload creates a new application and a first immutable release. To publish a later release of the same app, use app update. A playlist keeps serving the release it already pins until you edit that primitive.

app update#

Publish a new release of an existing application, preserving identity, name, and application K/V.

app update <id> <directory> [--expect-rev REVISION] [--no-wait] [--poll-ms MS]
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe app_ application id.
<directory>pathyesnoneThe built static directory.
--expect-rev REVISIONstringnononeOptional revision precondition. Omit to write the current resource. A supplied stale value is revision_conflict: refetch, reapply, retry.
--no-waitbooleannooffReturn before the publication operation finishes. Run operations wait before pinning the release into a playlist.
--poll-ms MSintegerno1000Interval between operation polls while waiting.
screenrig app update app_01EXAMPLEAPP00000000000 ./lobby-board

Wait for data.operation.state: "succeeded", then use playlist replace-release to review the exact page, primitive, new release, and affected screens. Apply with the returned impact token and an optional revision guard as described in the playlist workflow. Existing playlist pins remain unchanged until apply. On a revision conflict, read the app again before deciding whether to retry; do not create a replacement application just to bypass the conflict.

app list#

List the project's applications.

app list

No parameters.

screenrig app list

app show#

Read one application.

app show <id>
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe app_ application id.
screenrig app show app_01EXAMPLEAPP00000000000

latest_ready_release is absent until a first publish reaches ready. Publish state lives on the operation and the release, not on the application.

Compose

Local unbilled stills for slide-deck-like experiences. The compositing language stays on Compose. Presentable posters use media generate, not these commands.

compose catalog#

Print the fail-closed catalog of page keys, named regions, region fields, card plates, enter and motion enums, viewing distances, installed fonts, and examples.

compose catalog

No parameters.

screenrig compose catalog

compose render#

Render a spec to a directory of region PNGs and manifest.json.

compose render <file> [--output DIRECTORY] [--combined] [--target-width PX --target-height PX] [--safe-area] [--open] [--lint-only]
ParameterTypeRequiredDefaultNotes
<file>pathyesnoneThe JSON spec. Page width and height define the canvas.
--output DIRECTORYpathnothe spec path with .json removedWhere to write region PNGs and manifest.json. A directory, not a .png.
--combinedbooleannooffAlso write a flattened PNG for inspection. Default agent output stays layered.
--target-width PX / --target-height PXnumbernounsetPhysical content viewport for quality checks. Both or neither. These flags do not resize the PNGs.
--safe-areabooleannooffWarn when measured text ink crosses a 5% TV-safe margin. Nonblocking.
--openbooleannooffOpen the combined PNG with the platform opener. Implies --combined. Agent vision reads file paths instead.
--lint-onlybooleannooffCompose in memory and report lint without writing files.
screenrig compose render ./lobby-welcome.json --output ./lobby-welcome

Never print image bytes. Read manifest.json and the region PNGs before any upload.

compose batch#

Render a deck and write a contact-sheet preview.

compose batch <file> --output DIRECTORY [--only ID] [--target-width PX --target-height PX] [--safe-area] [--lint-only]
ParameterTypeRequiredDefaultNotes
<file>pathyesnoneThe same JSON as compose render, including { "pages": [ { "id": "intro", ... } ] }.
--output DIRECTORYpathyesnoneDestination for layered PNGs, manifest.json, a contact sheet, and a batch manifest.
--only IDstringnounsetRender one page and mark the others not_selected.
--target-width PX / --target-height PXnumbernounsetSame quality checks as compose render.
--safe-areabooleannooffSame TV-safe warning as compose render.
--lint-onlybooleannooffReport lint without writing files.
screenrig compose batch ./deck.json --output ./rendered --target-width 3840 --target-height 2160 --safe-area

One command accepts 1 to 2000 pages. compose render of a deck file is enough when you do not need the contact sheet.

Application key/value

Key/value storage belongs to one application and is binary-safe. Every command takes --app-id with the app_ id from app upload, never a release id. Use exactly one value mode per write.

kv list#

List the keys stored for one application.

kv list --app-id ID
ParameterTypeRequiredDefaultNotes
--app-id IDstringyesnoneThe app_ application id.
screenrig kv list --app-id app_01EXAMPLEAPP00000000000

kv get#

Read one key.

kv get --app-id ID <key>
ParameterTypeRequiredDefaultNotes
--app-id IDstringyesnoneThe app_ application id.
<key>stringyesnoneThe key to read.
screenrig kv get --app-id app_01EXAMPLEAPP00000000000 menu

kv set#

Write one key from JSON, a file, or base64 bytes.

kv set --app-id ID <key> --json-value JSON [--expect-rev REVISION]
kv set --app-id ID <key> --file FILE [--content-type TYPE] [--expect-rev REVISION]
kv set --app-id ID <key> --value-base64 BASE64 [--content-type TYPE] [--expect-rev REVISION]
ParameterTypeRequiredDefaultNotes
--app-id IDstringyesnoneThe app_ application id.
<key>stringyesnoneThe key to write.
--json-value JSONstringone modenoneA JSON value, canonicalized before it is sent, always stored as application/json. An explicit application/json content type is also accepted.
--file FILEpathone modenoneRead the value bytes from a file, or stdin with -. Defaults to application/octet-stream.
--value-base64 BASE64stringone modenoneCanonical padded standard base64 without whitespace, at most 1,398,104 characters. Defaults to application/octet-stream.
--content-type TYPEstringnoapplication/octet-stream for bytesOne to 127 characters.
--expect-rev REVISIONstringnononeWrite only if the stored entry is still at this revision.
screenrig kv set --app-id app_01EXAMPLEAPP00000000000 menu --json-value '{"soup":"lentil"}'

kv delete#

Delete one key.

kv delete --app-id ID <key> [--expect-rev REVISION]
ParameterTypeRequiredDefaultNotes
--app-id IDstringyesnoneThe app_ application id.
<key>stringyesnoneThe key to delete.
--expect-rev REVISIONstringnononeOptional revision precondition. Omit to write the current resource. A supplied stale value is revision_conflict: refetch, reapply, retry.
screenrig kv delete --app-id app_01EXAMPLEAPP00000000000 menu

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; 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, or HTTPS URLs.

playlist init <inputs...> --name NAME --output FILE [--overwrite] [--screen-id ID]
              [--target-width PX --target-height PX] [--duration-ms MS] [--fit contain|cover|fill]
              [--no-transcode] [--no-progress] [--poll-ms MS]
ParameterTypeRequiredDefaultNotes
<inputs...>files, med_ IDs, rel_ IDs, or HTTPS URLsyesnonePlayback order. Local image/video files upload and wait for readiness unless --no-transcode. Existing media must already be ready. HTTPS URLs must contain no username or password and are not fetched.
--name NAMEstringyesnonePlaylist display name.
--output FILEpathyesnoneWrite the editable {name, pages} document. Parent directories are created. Exclusive by default.
--overwritebooleannooffReplace an existing output file atomically.
--screen-id IDstringnononeRead the target screen's dimensions and revision. --screen remains an alias.
--target-width PX / --target-height PXintegertogetherunsetOverride canvas size. Both or neither. Required when no screen is supplied or the screen reports unknown or multiple surfaces.
--duration-ms MSintegerno8000Duration for non-video pages.
--fitstringnocontaincontain, cover, or fill for image and video pages. Applications and iframes use fill.
--no-transcodebooleannooffUpload accepted file bytes unchanged.
--no-progressbooleannooffSuppress upload progress on stderr.
--poll-ms MSintegerno1000Upload polling interval.
screenrig playlist init med_POSTER med_VIDEO --name "Lobby loop" --screen-id scr_LOBBY --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 and preview, then screen publish. See Prepare, publish, and edit playlists.

playlist templates#

Print the local catalog of slide template ids.

playlist templates

No parameters.

screenrig playlist templates

Templates run locally and expand to wire primitives. A template that would emit vector chrome fails with usage_error pointing at the Compose reference, because copy and chrome are composed locally and placed as one image.

playlist validate#

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

playlist validate <file> [--lint-only]
ParameterTypeRequiredDefaultNotes
<file>pathyesnoneThe playlist document.
--lint-onlybooleannooffReport visual lint without the full write-time checks that create/update also run.
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. Create and update run this check before their write.

playlist preview#

Composite every page to PNG frames for inspection. Never print pixels.

playlist preview <file|id> --output DIR [--frame-ms MS] [--contact-sheet] [--lint-only]
ParameterTypeRequiredDefaultNotes
<file|id>path or pl_ idyesnoneA JSON file is local. A pl_… id uses the project API.
--output DIRpathyesnoneDestination directory for the frame PNGs.
--frame-ms MSintegerno1000Poster frame from a video primitive, 0 through 60000.
--contact-sheetbooleannooffAlso write one 6-column PNG of every rest frame.
--lint-onlybooleannooffReport lint without writing files.
screenrig playlist preview ./lobby-loop.json --output ./frames --contact-sheet

Writes three PNGs per page: <page-id>.rest.png, <page-id>.entry.png, and <page-id>.motion-mid.png. Iframe and application regions are labelled placeholders. Look at the contact sheet before publishing.

playlist create#

Create a playlist from a JSON document.

playlist create <file>
ParameterTypeRequiredDefaultNotes
<file>pathyesnoneThe playlist document.
screenrig playlist create ./lobby-loop.json

The envelope's data.id is the pl_ id that screen assign takes.

playlist update#

Replace a playlist document.

playlist update <id> <file> [--expect-rev REVISION]
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe pl_ playlist id.
<file>pathyesnoneThe replacement document.
--expect-rev REVISIONstringnononeOptional revision precondition. Omit to write the current resource. A supplied stale value is revision_conflict: refetch, reapply, retry.
screenrig playlist update pl_01EXAMPLEPLAYLIST000000 ./lobby-loop.json

playlist show#

Read one playlist, including its current revision.

playlist show <id>
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe pl_ playlist id.
screenrig playlist show pl_01EXAMPLEPLAYLIST000000

playlist list#

List the project's playlists.

playlist list

No parameters.

screenrig playlist list

playlist delete#

Delete one playlist.

playlist delete <id> [--expect-rev REVISION]
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe pl_ playlist id.
--expect-rev REVISIONstringnononeOptional revision precondition. Omit to write the current resource. A supplied stale value is revision_conflict: refetch, reapply, retry.
screenrig playlist delete pl_01EXAMPLEPLAYLIST000000

playlist export#

Write one playlist and every rendition it resolves into a portable bundle directory.

playlist export <id> --output DIRECTORY [--skip-applications]
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe pl_ playlist id.
--output DIRECTORYpathyesnoneDestination directory. It must not already exist; the bundle is written through a temporary sibling and renamed into place.
--skip-applicationsflagnooffDrop application primitives and any page left with no primitives. The result lists the skipped primitive and page ids.
screenrig playlist export pl_01EXAMPLEPLAYLIST000000 --output ./lobby-bundle

The bundle holds a manifest, the playlist document, and content-addressed media files. Dynamic all and tag selectors are snapshotted to exact ids, iframe URLs are preserved, and comments are excluded. Application primitives cannot be exported: by default they stop the export before anything is downloaded, and --skip-applications exports the rest.

playlist import#

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

playlist import <directory> [--name NAME] [--update ID [--expect-rev REVISION]]
ParameterTypeRequiredDefaultNotes
<directory>pathyesnoneA bundle directory written by playlist export.
--name NAMEstringnothe bundle's playlist name1 to 120 characters. Import the bundle as a new playlist under this name.
--update IDstringnoa new playlist is createdUpdate this pl_ playlist instead of creating one. Accepts optional --expect-rev.
--expect-rev REVISIONstringnononeThe destination playlist's current revision.
screenrig playlist import ./lobby-bundle
screenrig playlist import ./lobby-bundle --name "Lobby loop (copy)"

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.

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.

The playlist document

A playlist is name plus pages, with an optional audio soundtrack. Each page is either a template page or a primitives page; do not mix the two on one page. A full page carries id, canvas, transition, advance, optional visibility, optional audio_cue, and primitives.

The five primitives are image, uploaded video, live stream, and web iframe and application. Copy and chrome are not wire types: compose them locally, upload the still, 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; optional direct UDP/MPEG-TS multicast on Android and Qt uses an IPv4 group in 239/8 and a port. Other Players select the HLS alternative or display the fallback. Stream bytes are not cached for replay. Audio defaults to muted. Preview displays the fallback; playlist bundle import/export does not support streams in this version.

{
  "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.

Every primitive is flat: id, primitive, that primitive's own fields, then rect, layer, content_fit, and optional enter. There is no nested content object.

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, or tag matching ^[A-Za-z0-9]{1,32}$. 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 public HTTPS src and a title, so neither of those takes a selector. Publish accepts that HTTPS URL after rejecting private addresses; it does not check whether the target allows framing. A blocked page fails at runtime and is recorded in the event log. Both web primitives require content_fit of fill, because neither has an intrinsic size.

advance.mode is duration with after_ms, media_end on a page with exactly one image or video primitive, 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.

transition.type is crossfade, swipe-left, swipe-right, swipe-up, or swipe-down, with a required duration_ms from 0 through 60000. The default page is a 200 ms crossfade. 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.

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. Every playlist keeps at least one page with no visibility field at all, and a screen running a scheduled playlist needs a timezone. Set that timezone first; screen assign and playlist update both refuse locally without one and name the command that fixes it.

An optional top-level audio object adds a soundtrack that plays continuously while pages change. tracks lists 1 to 32 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. 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. playlist update replaces the whole document, so omitting audio removes the soundtrack. Soundtrack authoring.

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.

screen list#

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

screen list [--state archived] [--tag TAG]
ParameterTypeRequiredDefaultNotes
--statestringnoactive screensThe only accepted value is archived, which lists archived screens only.
--tag TAGstringnoevery screenLists only screens carrying this exact tag.
screenrig screen list --tag Lobby

Each row carries the screen's tags. With --human, the table adds a TAGS column when any listed screen has tags.

screen show#

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

screen show <id>
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe scr_ screen id.
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. health and the Player's host capabilities are in Device health and power. 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.

screen update#

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

screen update <id> [--name NAME] [--playlist-id ID] [--timezone ZONE] [--expect-rev REVISION]
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe scr_ screen id.
--name NAMEstringnounchangedDisplay name for the screen.
--playlist-id IDstringnounchangedThe pl_ playlist this screen should run.
--timezone ZONEstringnounchangedAn IANA zone identifier such as America/Los_Angeles. Validated against the server's zone database. A patch never clears a zone.
--expect-rev REVISIONstringnononeOptional revision precondition. Omit to write the current resource. A supplied stale value is revision_conflict: refetch, reapply, retry.
screenrig screen update scr_01EXAMPLESCREEN00000000 --name "Lobby"

screen publish#

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

screen publish <id> <file> [--expect-rev REVISION]
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe scr_ screen id.
<file>pathyesnoneThe prepared playlist document, or - for stdin.
--expect-rev REVISIONstringnononeOptional screen revision precondition. Omit to write the current resource. A supplied stale value is revision_conflict: refetch, reapply, retry.
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 and preview first. Repeat identical input to resume an ambiguous failure. Assignment readback does not prove playback; request a screenshot separately. See Prepare, publish, and edit playlists.

screen assign#

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

screen assign (<id>... | --tag TAG) --playlist-id ID [--expect-rev REVISION]
ParameterTypeRequiredDefaultNotes
<id>...stringone of <id> or --tagnoneOne or more scr_ screen ids. Several ids run as one fleet request.
--tag TAGstringone of <id> or --tagnoneEvery active screen carrying this tag.
--playlist-id IDstringyesnoneThe pl_ playlist to run.
--expect-rev REVISIONstringnononeOne screen id only. Optional revision precondition. Omit to write the current resource. A supplied stale value is revision_conflict: refetch, reapply, retry.
screenrig screen assign scr_01EXAMPLESCREEN00000000 --playlist-id pl_01EXAMPLEPLAYLIST000000
screenrig screen assign --tag Lobby --playlist-id pl_01EXAMPLEPLAYLIST000000

If the playlist schedules any page and the screen has no timezone, the CLI refuses before sending anything and names the screen set-timezone command to run. No revision is consumed by that refusal. In a fleet, each screen applies that rule on its own: a screen with no timezone fails in its result and the others are assigned.

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.

screen set-timezone#

Set the civil timezone a scheduled playlist is read in.

screen set-timezone <id> --timezone ZONE [--expect-rev REVISION]
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe scr_ screen id.
--timezone ZONEstringyesnoneAn IANA zone identifier such as Europe/Berlin.
--expect-rev REVISIONstringnononeOptional revision precondition. Omit to write the current resource. A supplied stale value is revision_conflict: refetch, reapply, retry.
screenrig screen set-timezone scr_01EXAMPLESCREEN00000000 --timezone America/Los_Angeles

Page schedules are civil times, so they are meaningless without a zone. Changing the zone remints the manifest revision.

screen archive#

Hide a screen and darken its glass.

screen archive <id> [--expect-rev REVISION]
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe scr_ screen id.
--expect-rev REVISIONstringnononeOptional revision precondition. Omit to write the current resource. A supplied stale value is revision_conflict: refetch, reapply, retry.
screenrig screen archive scr_01EXAMPLESCREEN00000000

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

screen unarchive#

Restore an archived screen to the default listing.

screen unarchive <id> [--expect-rev REVISION]
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe scr_ screen id.
--expect-rev REVISIONstringnononeOptional revision precondition. Omit to write the current resource. A supplied stale value is revision_conflict: refetch, reapply, retry.
screenrig screen unarchive scr_01EXAMPLESCREEN00000000

A Player can also archive its own screen: a confirmed reset on a native Player, or unpairing a browser Player. 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.

screen delete#

Delete a screen.

screen delete <id> [--expect-rev REVISION]
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe scr_ screen id.
--expect-rev REVISIONstringnononeOptional revision precondition. Omit to write the current resource. A supplied stale value is revision_conflict: refetch, reapply, retry.
screenrig screen delete scr_01EXAMPLESCREEN00000000

Delete is not a way to detach a Player: a live screen returns screen_archive_required. Archive it instead.

screen rotate-public-id#

Issue a new public id for a screen.

screen rotate-public-id <id> [--expect-rev REVISION]
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe scr_ screen id.
--expect-rev REVISIONstringnononeOptional revision precondition. Omit to write the current resource. A supplied stale value is revision_conflict: refetch, reapply, retry.
screenrig screen rotate-public-id scr_01EXAMPLESCREEN00000000

screen reload#

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

screen reload (<id>... | --tag TAG) [--expect-rev REVISION]
ParameterTypeRequiredDefaultNotes
<id>...stringone of <id> or --tagnoneOne or more scr_ screen ids. Several ids run as one fleet request.
--tag TAGstringone of <id> or --tagnoneEvery active screen carrying this tag.
--expect-rev REVISIONstringnononeOne screen id only. Optional revision precondition.
screenrig screen reload scr_01EXAMPLESCREEN00000000
screenrig screen reload --tag Lobby

The answer carries a reload_id and an expires_at ten minutes later. 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.

screen toast (<id>... | --tag TAG) --text TEXT [--level info] [--duration-ms MS]
ParameterTypeRequiredDefaultNotes
<id>...stringone of <id> or --tagnoneOne or more scr_ screen ids. Several ids run as one fleet request.
--tag TAGstringone of <id> or --tagnoneEvery active screen carrying this tag.
--text TEXTstringyesnoneOne to 120 characters, line feed as the only break, at most three lines.
--level LEVELstringnoinfoinfo, alert, or error. The hosted Player shows error toasts. Info and alert are diagnostic levels that may be suppressed; command success does not establish visibility.
--duration-ms MSintegerno10000, applied by the serverBetween 2000 and 60000 when supplied.
screenrig screen toast scr_01EXAMPLESCREEN00000000 --text "Lobby closed"
screenrig screen toast --tag Lobby --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 screenshot#

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

screen screenshot <id> [--output FILE] [--timeout MS] [--poll-ms MS]
screen screenshot (<id> <id>... | --tag TAG) [--output DIRECTORY] [--concurrency N] [--timeout MS] [--poll-ms MS]
ParameterTypeRequiredDefaultNotes
<id>...stringone of <id> or --tagnoneOne or more scr_ screen ids.
--tag TAGstringone of <id> or --tagnoneEvery active screen carrying this tag, with no screen-count cap.
--output FILEpathno./<id>.webpFor one screen, a file path; an existing file is overwritten without a prompt. For several ids or --tag, a directory, created if missing, that receives one <screen_id>.webp per screen.
--concurrency Nintegerno4Captures in flight at once for several ids or --tag, from 1 to 8.
--timeout MSintegerno35000How long to wait for the capture.
--poll-ms MSintegerno500Interval between capture-status polls.
screenrig screen screenshot scr_01EXAMPLESCREEN00000000 --output ./lobby.webp
screenrig screen screenshot --tag Lobby --output ./lobby-shots

The command blocks until the WebP is on disk; there is no --no-wait. 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.

For several ids or --tag, the answer is a fleet result 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 billed and the captures themselves are free, 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.

screen tag#

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

screen tag (<id>... | --tag TAG) (--set TAGS | --add TAGS | --remove TAGS | --clear) [--expect-rev REVISION]
ParameterTypeRequiredDefaultNotes
<id>...stringone of <id> or --tagnoneOne or more scr_ screen ids. Several ids run as one fleet request.
--tag TAGstringone of <id> or --tagnoneEvery active screen carrying this tag.
--set TAGSstringexactly one actionnoneReplace the whole set with these comma-separated tags, such as Lobby,Floor2.
--add TAGSstringexactly one actionnoneAdd these comma-separated tags.
--remove TAGSstringexactly one actionnoneRemove these comma-separated tags.
--clearbooleanexactly one actionoffRemove every tag.
--expect-rev REVISIONstringnononeOne screen id only. Optional revision precondition.
screenrig screen tag scr_01EXAMPLESCREEN00000000 --set Lobby,Floor2
screenrig screen tag scr_01EXAMPLESCREEN00000000 --add Spring
screenrig screen tag --tag Lobby --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.

Fleet results

With several screen ids or --tag, screen assign, screen reload, screen toast, and screen tag run one metered 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.

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

{
  "ok": true,
  "data": {
    "action": "reload",
    "matched": 3, "succeeded": 2, "failed": 1,
    "results": [
      { "screen_id": "scr_LOBBY", "status": "ok", "reload": { "reload_id": "…", "expires_at": "…" } },
      { "screen_id": "scr_GONE", "status": "failed", "problem": { "code": "not_found", "status": 404 } },
      { "screen_id": "scr_ENTRANCE", "status": "ok", "reload": { "reload_id": "…", "expires_at": "…" } }
    ]
  },
  "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 a project budget of 600 screens a minute: the server pauses when the remaining budget is exhausted, and the CLI honors Retry-After before continuing automatically.

OutcomeExit codeWarning
Every matched screen succeeded0none
Some or all screens failedThe first failed screen's code, such as 4 for not_found or 6 for revision_conflictfleet_partial_failure
No screen matched0fleet_no_match
Malformed selector or action2 or the request's own code; no screen changesnone

Schedules and takeover

The server decides what each screen plays, in this order: its takeover, else the first playlist schedule entry whose windows match the screen's civil time, else the playlist assigned with screen assign. Switches land within about a minute of a boundary, and Players need no update.

screen show, screen list, and every command below return effective_playlist ({id, source, entry_id?, until?}, where source is takeover, schedule, or default), takeover, and playlist_schedule. until is the next moment the choice is known to change. screen show --human prints it in the screen's timezone, as in Playing: pl_LUNCH (schedule entry lunch until 2026-08-14 15:00 America/Los_Angeles) (UTC, labelled, when the screen has no zone); JSON output keeps RFC 3339 instants. Human schedule tables are labelled with the screen timezone, and screen list --human adds a PLAYING column when any listed screen is on a takeover or schedule entry.

Problem codeExitMeaning
invalid_request8The screen has no assigned default playlist (next is screen assign), a schedule targets a screen with no timezone (next is screen set-timezone), or a named playlist cannot be assigned.
screen_archived5An archived screen refuses schedules and takeovers. next is screen unarchive.
revision_conflict6The --expect-rev revision was stale.
rate_limited7Schedule and takeover writes share per-minute limits: 600 per project, 20 per screen, and 60 per IP. Wait for retry_after_seconds.

With one screen id, these commands return the updated screen and --expect-rev guards its revision. Several ids or --tag run as one fleet request with the fleet envelope and exit rules; each screen fails alone, and the CLI pauses and resumes automatically when the project budget is exhausted.

screen schedule show#

Show a screen's playlist schedule and the playlist it currently selects.

screen schedule show <id>
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe scr_ screen id.
screenrig screen schedule show scr_CAFE

The JSON output can go straight back to screen schedule set --file.

screen schedule set#

Replace the playlist schedule on a screen or a fleet.

screen schedule set (<id>... | --tag TAG) --file FILE [--expect-rev REVISION]
ParameterTypeRequiredDefaultNotes
<id>...stringone of <id> or --tagnoneOne or more scr_ screen ids.
--tag TAGstringone of <id> or --tagnoneEvery active screen carrying this tag.
--file FILEpathyesnoneThe schedule document, or - for stdin.
--expect-rev REVISIONstringnononeOne screen id only. Optional revision precondition.
screenrig screen schedule set scr_CAFE --file dayparts.json
screenrig screen schedule set --tag Cafe --file dayparts.json

Every playlist a schedule names is validated as an assignment, and the screen needs a timezone and an assigned default playlist. Outside every window the assigned playlist plays. If a scheduled playlist later cannot be shown, its entries are skipped and screen.playlist_unavailable is recorded.

screen schedule clear#

Remove the playlist schedule from a screen or a fleet.

screen schedule clear (<id>... | --tag TAG) [--expect-rev REVISION]
ParameterTypeRequiredDefaultNotes
<id>...stringone of <id> or --tagnoneOne or more scr_ screen ids.
--tag TAGstringone of <id> or --tagnoneEvery active screen carrying this tag.
--expect-rev REVISIONstringnononeOne screen id only. Optional revision precondition.
screenrig screen schedule clear --tag Cafe

screen takeover#

Show one playlist on a screen or a fleet ahead of the schedule and the assignment.

screen takeover [set] (<id>... | --tag TAG) --playlist-id ID [--until TIME | --for DURATION] [--reason TEXT] [--expect-rev REVISION]
ParameterTypeRequiredDefaultNotes
<id>...stringone of <id> or --tagnoneOne or more scr_ screen ids.
--tag TAGstringone of <id> or --tagnoneEvery active screen carrying this tag.
--playlist-id IDstringyesnoneThe pl_ playlist to show.
--until TIMEstringnoheld until clearedA strict RFC 3339 instant with seconds and an offset, in the form YYYY-MM-DDTHH:MM:SSZ or YYYY-MM-DDTHH:MM:SS-07:00, sent normalized to UTC; strictly in the future and at most 7 days ahead. Or none to hold until screen takeover clear.
--for DURATIONstringnoheld until cleared30m, 2h, or 3d, up to 6d23h59m, converted to an until from this computer's clock. 7d is refused locally; use --until for a full 7 days. Not with --until.
--reason TEXTstringnononeAn operator note on the takeover and its events. Trimmed, at most 120 characters, and no control characters.
--expect-rev REVISIONstringnononeOne screen id only. Optional revision precondition.
screenrig screen takeover scr_LOBBY --playlist-id pl_DRILL --for 30m --reason "Fire drill"
screenrig screen takeover --tag Lobby --playlist-id pl_LAUNCH --for 2h
screenrig screen takeover scr_A scr_B --playlist-id pl_NOTICE --until none

screen takeover ID is short for screen takeover set ID. A new takeover replaces the previous one. When it ends, the screen returns to its schedule or assigned playlist on its own. A takeover whose playlist cannot be shown ends. A --for rerun after an ambiguous failure computes a new end and is sent as a new request.

screen takeover clear#

End the takeover on a screen or a fleet.

screen takeover clear (<id>... | --tag TAG) [--expect-rev REVISION]
ParameterTypeRequiredDefaultNotes
<id>...stringone of <id> or --tagnoneOne or more scr_ screen ids.
--tag TAGstringone of <id> or --tagnoneEvery active screen carrying this tag.
--expect-rev REVISIONstringnononeOne screen id only. Optional revision precondition.
screenrig screen takeover clear --tag Lobby

The schedule file

A schedule lists 1 to 32 entries in priority order; the first entry that matches wins. Each entry has 1 to 16 windows in the screen timezone. days uses mon to sun, and start and end are HH:MM; omit both for the whole day. An end at or before start crosses midnight and belongs to the start day. Optional from (inclusive) and until (exclusive) are civil minutes such as 2026-12-24T18:00, with no offset. An omitted id becomes entry_N. The CLI checks this shape before sending anything. Breakfast, lunch, and dinner dayparts:

{
  "entries": [
    { "id": "breakfast", "playlist_id": "pl_BREAKFAST",
      "windows": [{ "days": ["mon", "tue", "wed", "thu", "fri"], "start": "06:00", "end": "11:00" },
                  { "days": ["sat", "sun"], "start": "08:00", "end": "12:00" }] },
    { "id": "lunch", "playlist_id": "pl_LUNCH",
      "windows": [{ "days": ["mon", "tue", "wed", "thu", "fri"], "start": "11:00", "end": "15:00" }] },
    { "id": "dinner", "playlist_id": "pl_DINNER",
      "windows": [{ "days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "start": "17:00", "end": "22:00" }] }
  ]
}

A playlist schedule chooses between playlists; page visibility inside one playlist still chooses between its pages.

Schedule and takeover events

events list and events follow print screen.playlist_switched (playlist_id, previous_playlist_id, source, entry_id, until), screen.takeover_started (playlist_id, until, where none means held until cleared, and reason), screen.takeover_ended (reason expired, cleared, replaced, playlist_deleted, or playlist_unavailable), and the warning screen.playlist_unavailable (playlist_id, entry_ids, and code playlist_deleted or playlist_unavailable).

Deleting a playlist that a live screen can still show fails with resource_conflict (exit 5): assigned, named by a schedule entry, held by a takeover, or effective. next points at screen list.

Device health and power

Every paired Player reports device health on session start, every fifteen minutes, and at once when its display connection or power changes. It also declares the host capabilities it supports: reboot, and the display power methods it can use (display_power.cec, display_power.ddc, display_power.backlight, or display_power.scrim, an on-screen blackout). screen show returns both.

screen show returns health as data.health with the server's reported_at; stale turns true after 35 minutes without a report, and the last report stays visible. With --human it prints a Health block: report time, device and Player uptime, memory in use, CPU load and cores, temperature, display connection and power, network kind and Wi-Fi signal, and crashes and renderer restarts in the last 24 hours. screen list --human adds a HEALTH column when any listed screen needs attention: display disconnected, hot (80 °C or more), crashing (3 or more crashes in 24 hours), or stale.

Transitions arrive as screen.health_changed events, printed by events list and events follow with a compact changes= value such as display_disconnected,temperature_high temperature_c=82. Power commands append screen.reboot_requested and screen.display_changed. Like every screen.* event, they are free on the listen stream and as webhook deliveries.

Problem codeExitMeaning
reboot_unsupported5The screen's Player does not declare reboot. Nothing was sent; screen reload refreshes content without a device reboot.
resource_conflict5The screen is still waiting to pair and has no Player to switch yet. A display schedule can still be set.
invalid_request8A display schedule targets a screen with no timezone (next is screen set-timezone), or the file is malformed.
rate_limited7Reboots: 2 per screen per 10 minutes and 600 per project a minute. Display writes: 10 per screen and 600 per project a minute. Wait for retry_after_seconds.

Several ids or --tag run as one fleet request with the fleet envelope and exit rules.

screen reboot#

Reboot a screen's device, or a fleet's devices.

screen reboot (<id>... | --tag TAG) [--yes] [--expect-rev REVISION]
ParameterTypeRequiredDefaultNotes
<id>...stringone of <id> or --tagnoneOne or more scr_ screen ids.
--tag TAGstringone of <id> or --tagnoneEvery active screen carrying this tag.
--yesbooleanfor several ids or --tagoffConfirms a fleet reboot. The CLI never prompts.
--expect-rev REVISIONstringnononeOne screen id only. Optional revision precondition.
screenrig screen reboot scr_LOBBY
screenrig screen reboot --tag Lobby --yes

The Player reboots the device, not just itself. A reboot request expires after ten minutes. A refused or failed request, and an exact replay of the same request, do not count toward the per-screen limit.

screen display#

Turn a screen's display, or a fleet's displays, on or off now, or clear that override.

screen display [set] (<id>... | --tag TAG) --power on|off [--until TIME | --for DURATION] [--expect-rev REVISION]
screen display clear (<id>... | --tag TAG) [--expect-rev REVISION]
ParameterTypeRequiredDefaultNotes
<id>...stringone of <id> or --tagnoneOne or more scr_ screen ids. A trailing on or off after the ids is the same as --power.
--tag TAGstringone of <id> or --tagnoneEvery active screen carrying this tag.
--power on|offstringyes, for setnoneThe power to apply now.
--until TIMEstringnothe display schedule's next boundaryA strict RFC 3339 instant, strictly in the future and at most 7 days ahead.
--for DURATIONstringnothe display schedule's next boundary30m, 2h, or 3d, up to 6d23h59m. Not with --until.
--expect-rev REVISIONstringnononeOne screen id only. Optional revision precondition.
screenrig screen display scr_LOBBY --power off --for 2h
screenrig screen display --tag Lobby --power on
screenrig screen display clear --tag Lobby

screen display is a manual override of the display schedule. Without --until or --for it ends at the schedule's next boundary; a later display-schedule or timezone change moves that end to the new boundary. With no boundary in the next eight days, for example with no enabled schedule, it holds until screen display clear or a replacement. screen display clear ends the override, so the display schedule applies again; without one, the display is on. The Player applies every change at once and reports the power it achieved.

screen display-schedule#

Show, set, or clear when a screen's display is on.

screen display-schedule show <id>
screen display-schedule set (<id>... | --tag TAG) --file FILE [--expect-rev REVISION]
screen display-schedule clear (<id>... | --tag TAG) [--expect-rev REVISION]
ParameterTypeRequiredDefaultNotes
<id>...stringone of <id> or --tagnoneOne or more scr_ screen ids; show takes one.
--tag TAGstringone of <id> or --tagnoneEvery active screen carrying this tag.
--file FILEpathyes, for setnoneThe display schedule, or - for stdin.
--expect-rev REVISIONstringnononeOne screen id only. Optional revision precondition.
screenrig screen display-schedule set scr_LOBBY --file hours.json
screenrig screen display-schedule set --tag Cafe --file hours.json

A display schedule lists 1 to 16 windows when the display is on, in the screen timezone; outside every window the display goes to standby. The Player evaluates it on the device, so it keeps working offline. A window whose end is at or before its start crosses midnight, and a window without start and end covers the whole day. enabled: false keeps the windows and leaves the display on. The saved output of display-schedule show can be edited and sent back with set --file.

{"enabled": true, "windows": [{"days": ["mon", "tue", "wed", "thu", "fri"], "start": "07:00", "end": "19:00"}, {"days": ["sat"]}]}

screen show --human prints Display: with the requested power, its source (manual override, display schedule, or default) and until when in the screen timezone, the schedule windows, and the power the Player reported, marked stale after 35 minutes without a report.

Comments

A comment is the agent's own JSON object attached to a screen, a playlist, or one playlist page. Compact UTF-8 of that object is at most 1 KiB, and the value must be an object. screenRIG never reads it, never sends it to Players, and never treats it as authorization. It does not appear on the runtime manifest and does not bump a revision, so these commands take no --expect-rev: last write wins.

comment show#

Read the comment object on a screen, a playlist, or one playlist page.

comment show screen <id>
comment show playlist <id> [--page PAGE_ID]
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe scr_ or pl_ id, matching the target word.
--page PAGE_IDstringnothe playlist itselfA playlist page id: a letter, then up to 63 letters, digits, underscores, or hyphens. Playlist targets only.
screenrig comment show screen scr_01EXAMPLESCREEN00000000

An unset comment reads as { "comments": null }. screen show and playlist show include comments when the server sends it.

comment set#

Write the comment object.

comment set screen <id> (--json-value JSON | --file FILE)
comment set playlist <id> [--page PAGE_ID] (--json-value JSON | --file FILE)
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe scr_ or pl_ id.
--json-value JSONstringone of the twononeA JSON object, inline.
--file FILEpathone of the twononeA file holding a JSON object.
--page PAGE_IDstringnothe playlist itselfPlaylist targets only.
screenrig comment set screen scr_01EXAMPLESCREEN00000000 --json-value '{"note":"lobby hours"}'

Playlist create and update cannot set comments; that is what these commands are for.

comment delete#

Remove the comment object.

comment delete screen <id>
comment delete playlist <id> [--page PAGE_ID]
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe scr_ or pl_ id.
--page PAGE_IDstringnothe playlist itselfPlaylist targets only.
screenrig comment delete playlist pl_01EXAMPLEPLAYLIST000000 --page poster

Operations

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

operations show#

Read one operation's current state.

operations show <id>
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe operation id from the envelope that started it.
screenrig operations show op_01EXAMPLEOPERATION00000

operations wait#

Poll one operation until it finishes.

operations wait <id> [--timeout MS] [--poll-ms MS]
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe operation id.
--timeout MSintegerno120000Deadline for the wait. It also bounds each underlying request.
--poll-ms MSintegerno1000Interval between operation polls while waiting.
screenrig operations wait op_01EXAMPLEOPERATION00000

operations cancel#

Ask the control plane to cancel one operation.

operations cancel <id>
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe operation id.
screenrig operations cancel op_01EXAMPLEOPERATION00000

Events

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

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

events list#

Read one finite page of project events.

events list [--after CURSOR] [--limit N]
ParameterTypeRequiredDefaultNotes
--after CURSORstringnothe start of the page windowResume after this cursor.
--limit Nintegernothe server's page sizeRequested page size.
screenrig events list --limit 25

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

events follow#

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

events follow [--after CURSOR] [--timeout MS]
ParameterTypeRequiredDefaultNotes
--after CURSORstringnothe live tailResume from this cursor.
--timeout MSintegernono deadlineEnds the whole follow, including reconnect backoff.
screenrig events follow

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

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

Webhooks

A webhook sends this project's own events to an HTTPS endpoint you run as signed POSTs, so a matching event can wake an agent, a bot, or your own service. A project has at most 10 webhooks. Each matching event arrives as the same JSON object events list returns.

Each delivery attempt is metered like a billed listen-stream event: 1 credit after the monthly 10,000 API and listen-stream allowance. The same events that are free on the listen stream are free here: screen.*, runtime.*, application.event, playback.page_failed, and agent.command. An attempt refused before anything is sent is not billed.

Problem codeExitMeaning
webhook_url_rejected8The URL is not https on port 443 or 8443, or its host does not resolve only to public Internet addresses. error.detail carries the server's reason. Nothing was created or changed.
webhook_limit_reached5The project already has 10 webhooks. Delete one first.
revision_conflict6The --expect-rev revision was stale. Run webhooks show, then retry.
rate_limited7More than 20 webhooks test calls in a minute for this project.

webhooks create#

Register an HTTPS endpoint and receive its signing secret once.

webhooks create --url URL --event-types TYPES [--description TEXT] [--disabled]
ParameterTypeRequiredDefaultNotes
--url URLstringyesnoneAn https URL on port 443 (the default) or 8443 whose host resolves only to public Internet addresses. The server checks it on every write and again before every delivery. A URL carrying credentials (user:pass@ or token@) or a # fragment is refused locally with exit 2.
--event-types TYPESstringyesnone1 to 32 comma-separated exact types, such as screen.online, or prefixes ending in .*, such as screen.*.
--description TEXTstringnononeAt most 200 characters.
--disabledbooleannooffCreate it disabled. Nothing is delivered until webhooks update --enable.
screenrig webhooks create --url https://hooks.example.com/screenrig --event-types "screen.*,playlist.updated"

The envelope is the webhook with its signing secret in data.secret (whsec_…) and the warning webhook_secret_shown_once. Store the secret in the receiver's secret store right away. No other command returns it, and the CLI never writes it to its configuration, operation log, or write-recovery state. After an interrupted or ambiguous create, rerun the identical command within 24 hours: it reuses the saved idempotency key, and the server returns the same answer, secret included.

webhooks list#

List this project's webhooks, never their secrets.

webhooks list
screenrig webhooks list

webhooks show#

Inspect a webhook and its delivery health.

webhooks show <id>
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe whk_ webhook id.
screenrig webhooks show whk_01EXAMPLEWEBHOOK00000

status is active, failing while no delivery has succeeded since failing_since, or disabled. After 72 hours of continuous failure the webhook is disabled with disabled_reason: delivery_failures, and the event log records webhook.disabled. last_success_at and last_failure_at report the latest outcomes.

webhooks update#

Change a webhook's URL, event types, description, or enabled state.

webhooks update <id> [--url URL] [--event-types TYPES] [--description TEXT | --clear-description]
                     [--enable | --disable] [--expect-rev REVISION]
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe whk_ webhook id.
--url URLstringat least one changeunchangedChecked like create.
--event-types TYPESstringat least one changeunchangedReplaces the whole list.
--description TEXTstringat least one changeunchangedAt most 200 characters.
--clear-descriptionbooleanat least one changeoffRemove the description.
--enablebooleanat least one changeoffClears the failure state and starts delivery at the current event.
--disablebooleanat least one changeoffStops delivery; pending deliveries fail with webhook_disabled.
--expect-rev REVISIONstringnononeOptional revision precondition.
screenrig webhooks update whk_01EXAMPLEWEBHOOK00000 --enable

update changes only the fields you pass. Events from while the webhook was disabled are not delivered after --enable.

webhooks delete#

Delete a webhook. Its pending deliveries fail with webhook_deleted.

webhooks delete <id> [--expect-rev REVISION]
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe whk_ webhook id.
--expect-rev REVISIONstringnononeOptional revision precondition.
screenrig webhooks delete whk_01EXAMPLEWEBHOOK00000

webhooks rotate-secret#

Replace a webhook's signing secret and receive the new one once.

webhooks rotate-secret <id> [--expect-rev REVISION]
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe whk_ webhook id.
--expect-rev REVISIONstringnononeOptional revision precondition.
screenrig webhooks rotate-secret whk_01EXAMPLEWEBHOOK00000

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

webhooks test#

Queue one webhook.test delivery to this webhook only.

webhooks test <id>
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe whk_ webhook id.
screenrig webhooks test whk_01EXAMPLEWEBHOOK00000

The envelope is the queued delivery. It is attempted once without retries, works on a disabled webhook, is metered like any delivery, and never changes the webhook's failure state. At most 20 test deliveries a minute per project; more is rate_limited (exit 7). Read the outcome with webhooks deliveries.

webhooks deliveries#

Read a webhook's delivery log, newest event first.

webhooks deliveries <id> [--before CURSOR] [--limit N]
ParameterTypeRequiredDefaultNotes
<id>stringyesnoneThe whk_ webhook id.
--before CURSORstringnothe newest eventThe next_cursor (whc1_…) of the previous page.
--limit Nintegerno50Rows per page, 1 to 200.
screenrig webhooks deliveries whk_01EXAMPLEWEBHOOK00000 --limit 20

Each row is one event: event_id, event_type, state (pending, succeeded, or failed), attempts, last_status, and a fixed last_error class such as http_status, timeout, connection_failed, tls_failed, dns_failed, or url_rejected. Rows never include the receiver's response. next_cursor is null at the end of the log. A failed attempt is retried with backoff, 30 seconds doubling to one hour, for 24 hours, then the row is failed.

Verify a delivery

The body is the same JSON event object events list returns. The headers are ScreenRig-Webhook-Id, ScreenRig-Event-Id (stable across retries), and ScreenRig-Signature: t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 over "<t>.<raw body>" keyed with the secret string. Compute it over the raw bytes before parsing JSON, compare in constant time, reject an old t, and deduplicate on ScreenRig-Event-Id, because delivery is at least once and unordered. Answer 2xx within 10 seconds; redirects are not followed.

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

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

Playback

Playback evidence comes in two shapes. playback plays reads one row per visible start of an image or video on a screen, oldest first by received_at: proof of play for a report or an advertiser. playback list reads daily totals, one row per screen, media object, and UTC day. Both export as CSV, and the dashboard exports the same files.

Plays are kept for the plan's event retention, at least 30 days.

Problem codeExitMeaning
invalid_request8A plays range longer than 31 days, a daily range longer than 366 days, or a malformed filter. The CLI refuses most of these before sending anything (exit 2).
rate_limited7The playback export budget of 30 requests a minute per project is spent. Wait for retry_after_seconds.

Billing: each JSON page is one billed API request, so --all bills one request per page. A CSV export is one billed request for its whole range. Every plays request and every CSV export also spends the playback export budget.

playback plays#

Read per-play records, one row per visible start.

playback plays [--from TIME] [--to TIME] [--screen-id ID] [--media-id ID] [--tag TAG] [--cursor CURSOR] [--limit N] [--all]
playback plays --format csv [--output FILE] [--from TIME] [--to TIME] [--screen-id ID] [--media-id ID] [--tag TAG]
ParameterTypeRequiredDefaultNotes
--from TIMEstringno24 hours before --toInclusive bound on received_at: an RFC 3339 instant with seconds and an offset, now, or an age such as 7d, 12h, or 30m.
--to TIMEstringnonowExclusive bound, in the same forms. The range is at most 31 days.
--screen-id IDstringnoevery screenOne scr_ screen.
--media-id IDstringnoevery objectOne med_ media object.
--tag TAGstringnoevery screenA tag the screen carried when the play was received.
--cursor CURSORstringnothe start of the rangeThe next_cursor of the previous page, with the same filters.
--limit Nintegerno200Rows per JSON page, at most 1000. Not with --format csv.
--allbooleannooffFollow the cursor to the end of the range, 1000 rows per page unless --limit says otherwise, for at most 50 pages.
--format csvstringnoJSONStream the whole range as CSV.
--output FILEpathno./playback-plays.csvCSV target, or - for stdout only.
screenrig playback plays --from 7d --tag Lobby --all
screenrig playback plays --format csv --from 2026-09-01T00:00:00Z --to 2026-10-01T00:00:00Z --output september-plays.csv

The CLI normalizes both bounds to UTC and sends them on every request; data.from and data.to echo them. The newest 5 seconds are held back until they settle, so a play appears about 5 seconds after it is received, and data.to reports that effective end. JSON returns one page in data.items with data.next_cursor, and data.next.argv is the exact command for the next page. --all stops early, with data.next and a warning, at the page cap (playback_plays_truncated) or when the export budget is spent (playback_export_rate_limited), rather than spending requests into a refusal.

The CSV is RFC 4180 with the fixed header row screen_id,playlist_id,page_id,primitive_id,media_id,primitive,started_at,received_at,manifest_revision,playlist_revision, times in RFC 3339 UTC. primitive_id and started_at are empty unless the Player reported them. A cell that begins with =, +, -, @, tab, or carriage return carries a leading ' so spreadsheets do not evaluate it. The CLI writes through a private temporary file in the same directory and replaces an existing file only after the stream ends cleanly. --timeout is the no-progress limit for the stream, 60000 ms without a byte by default, not a limit on its total duration. The envelope reports path, bytes, rows, sha256, and last_received_at; the CSV never enters the envelope.

If a stream fails partway, the command exits non-zero and never writes the target file. The complete rows already received stay in FILE.partial, and error.next exports the rest of the range from the last received_at into an unused FILE-rest.csv. Rows received at that exact instant appear in both, so drop the repeats when joining them.

playback list#

Read daily playback totals for this project, newest days first.

playback list [--screen-id ID] [--media-id ID] [--day YYYY-MM-DD | --day-from YYYY-MM-DD --day-to YYYY-MM-DD] [--format csv] [--output FILE]
ParameterTypeRequiredDefaultNotes
--screen-id IDstringnoevery screenMust start with scr_.
--media-id IDstringnoevery objectMust start with med_.
--day YYYY-MM-DDstringnoevery dayOne UTC calendar day.
--day-from YYYY-MM-DDstringnononeStart of an inclusive UTC day range, with --day-to, at most 366 days.
--day-to YYYY-MM-DDstringnononeEnd of that range.
--format csvstringnoJSONExport as CSV. With no range, the 31 days up to today.
--output FILEpathno./playback-aggregates.csvCSV target, or - for stdout only.
screenrig playback list --screen-id scr_01EXAMPLESCREEN00000000 --day 2026-08-14
screenrig playback list --format csv --day-from 2026-09-01 --day-to 2026-09-30

One row per screen, media object, and UTC day. The filters select the caller's own rows. The CSV header row is screen_id,media_id,filename,primitive,day,play_count,last_page_id,last_manifest_revision,first_started_at,last_started_at. A failed daily export keeps nothing; rerun it.

Support conversations

Every plan, including Standard, includes project support chats through the CLI and the dashboard's mustard Support button at the bottom right. Conversations retain their history. The dashboard highlights unread staff replies and renders Markdown, including clickable links, lists and code. Press Enter to send or Shift+Enter for a new line. Staff replies show their first name and Slack avatar when available, with ScreenRIG staff and the screenRIG logo as the fallback; customers use a generic person avatar.

Use the documentation and your operating agent to diagnose and resolve issues. Use feedback bug or feedback feature to send reports.

support status
support submit (--body TEXT | --body-file FILE) [--conversation-id ID] [--human-requested]
support history [--before ID]
support history --conversation-id ID [--after SEQUENCE]
support follow [--conversation-id ID] [--after SEQUENCE] [--timeout MS]
support read --conversation-id ID --sequence SEQUENCE
support close --conversation-id ID

Start with support status, which reports availability and staffed hours. support submit accepts one message of 1–4000 characters. Omit --conversation-id to start a conversation; use the returned data.conversation.id for follow-ups. Keep the same idempotency key when retrying an uncertain submission.

History pages contain up to 100 rows. Pass a conversation page's data.next as --before for older conversations, or the last message sequence as --after for newer messages. support follow streams replies as SSE, resumes from the last project sequence and suppresses replay. A conversation filter selects which replies it prints while advancing that project cursor. support read acknowledges staff replies through the sequence you have read. End a resolved conversation with support close or the dashboard’s End conversation button. Closing retains its history; start a new conversation for more help.

screenrig support submit --body "How do I assign a playlist? Here are the checks I ran."
screenrig support follow --conversation-id sc_CONVERSATION --after 0 --timeout 60000
screenrig support submit --conversation-id sc_CONVERSATION --body "Please ask a human." --human-requested

Support hours are 09:00–17:00 Monday–Friday, Vancouver time, excluding B.C. statutory holidays. Luna attempts an answer outside those hours or after 30 minutes without a staff reply. Asking for a human pauses AI in that conversation. Keep credentials out of support messages.

Feedback

Bug reports and feature requests are project-scoped and immutable. The kind comes from the command, never from a body field, and a correction is a new submission rather than an edit.

feedback bug#

File a bug report against screenRIG.

feedback bug <title> (--body TEXT | --body-file FILE) [--command "GROUP ACTION"] [--no-context]
ParameterTypeRequiredDefaultNotes
<title>stringyesnoneAt most 120 characters.
--body TEXTstringone of the twononeInline body, at most 4000 characters.
--body-file FILEpathone of the twononeRead the body from a file, which suits a multi-line report.
--commandstringnononeThe command the report is about, as a command path only: up to four lowercase words such as media upload. It is never taken from the command line you ran.
--no-contextbooleannooffOmit the diagnostic envelope, which otherwise holds the CLI version, the optional command path, and the platform.
screenrig feedback bug "Playlist stalls after assignment" --body-file ./report.md --command "screen assign"

The server rejects, rather than redacts, text matching credential material, and returns field-level guidance so you can rewrite and resend. An exact retry under the same idempotency key returns the original submission for twenty-four hours.

feedback feature#

File a feature request.

feedback feature <title> (--body TEXT | --body-file FILE) [--command "GROUP ACTION"] [--no-context]
ParameterTypeRequiredDefaultNotes
<title>stringyesnoneAt most 120 characters.
--body TEXTstringone of the twononeInline body, at most 4000 characters.
--body-file FILEpathone of the twononeRead the body from a file.
--commandstringnononeThe command the request is about, as a command path only.
--no-contextbooleannooffOmit the diagnostic envelope.
screenrig feedback feature "Add a dry-run flag" --body "Preview a change first."

feedback list#

List this project's own submissions.

feedback list [--kind bug|feature]
ParameterTypeRequiredDefaultNotes
--kindstringnoboth kinds, newest firstbug or feature. Restrict the listing to one kind.
screenrig feedback list --kind bug

Advertising

Buy advertising in a network that invited you, or sell ad breaks on screens you own. Every command reads screenrig project capabilities first: buying needs the advertiser capability, selling needs owned screens. Money is reported as decimal mcr (millicredit) strings. Run screenrig ads COMMAND --help --json for every flag. The buying and selling workflow is in Start; rates and fees are on Pricing.

ads networks#

Buyer: list the networks this project was invited into, and inspect one network's permitted inventory and slots.

ads networks list
ads networks show <seller-project-id>
screenrig ads networks list

ads network#

Seller: inspect or create this project's own network and set its default rate.

ads network show
ads network create --name NAME
ads network rate --rate-mcr-per-15s MCR --expect-rev REVISION
screenrig ads network rate --rate-mcr-per-15s 1000 --expect-rev 3

The rate is per 15 seconds of completed playback. A changed effective rate pauses affected campaigns until each buyer accepts a fresh quote.

ads inventory#

Seller: opt an owned screen in or out of advertising and describe it to invited buyers.

ads inventory list
ads inventory update <screen-id> [--enabled | --disabled] [--site-name NAME]
    [--city CITY] [--region REGION] [--venue-type TYPE] [--audience-tags TOKENS]
    [--placement TEXT] [--public-description TEXT]
    [--rate-mcr-per-15s MCR | --clear-rate] [--expect-rev REVISION]
screenrig ads inventory update scr_LOBBY --enabled --venue-type cafe

Omitted flags keep their current values. A new inventory row needs --enabled or --disabled.

ads slots#

Seller: define reusable ad slots. A slot changes nothing on the glass until its adslot page is placed in a playlist.

ads slots list
ads slots create --name NAME --accepted-media image,video
    [--max-image-duration-ms MS] [--max-video-duration-ms MS]
    [--rate-mcr-per-15s MCR] [--enabled | --disabled]
ads slots update <slot-id> --expect-rev REVISION [same fields]
screenrig ads slots create --name "Lobby break" --accepted-media image,video

Advertising invitations#

Use the shared invitations commands above:

screenrig invitations create --kind ad-buyer --email owner@bakery.example --screen-id scr_LOBBY --policy review_required
screenrig invitations list --kind ad-buyer
screenrig invitations revoke ID

The invited person accepts in the dashboard. A signed-in person selects their buying project and confirms with a fresh passkey proof. A person without a login creates one and an advertiser-only project during acceptance.

ads memberships#

Seller: change or revoke an admitted buyer's review policy and inventory scope.

ads memberships list
ads memberships update <membership-id> --expect-rev REVISION
    [--policy trusted|review_required] [--screen-id IDS] [--slot-id IDS]
ads memberships revoke <membership-id>
screenrig ads memberships list

Revocation stops new ad plays and keeps financial history.

ads creatives#

Buyer: bind ready owned media to an immutable creative with approved copy.

ads creatives list
ads creatives create --media-id ID --copy TEXT
ads creatives show <creative-id>
screenrig ads creatives create --media-id med_123 --copy "Fresh bread daily"

ads campaigns#

Buyer: draft a campaign, price it, and activate it by accepting the quote.

ads campaigns list | show <campaign-id>
ads campaigns create <file>
ads campaigns update <campaign-id> <file> --expect-rev REVISION
ads campaigns preview <campaign-id> [--expect-rev REVISION]
ads campaigns activate <campaign-id> --quote-id ID --expect-rev REVISION
ads campaigns pause | resume <campaign-id> --expect-rev REVISION
ads campaigns accept-rates <campaign-id> --quote-id ID --expect-rev REVISION
screenrig ads campaigns preview cmp_123

A draft reserves nothing. preview returns a time-limited quote and spends nothing; activate requires that exact quote. After a seller rate change, only accept-rates with a fresh quote resumes the campaign.

ads reviews#

Seller: approve or reject exact submitted creative versions from buyers on review_required.

ads reviews list
ads reviews show <review-id>
ads reviews approve <review-id>
ads reviews reject <review-id> --reason TEXT
screenrig ads reviews list

ads reports#

Read spend as a buyer or delivery as a seller.

ads reports spend --campaign-id ID
ads reports delivery --from TIMESTAMP --to TIMESTAMP
screenrig ads reports delivery --from 2026-09-01T00:00:00Z --to 2026-10-01T00:00:00Z

Delivery reports gross, serving fee and net for the period. A buyer report never shows a seller's other income.

Prepare, publish, and edit playlists

For ready images and videos, prepare a full-screen playlist in playback order:

screenrig playlist init med_POSTER med_VIDEO --name "Lobby loop" --screen-id scr_LOBBY --output lobby.json
screenrig playlist preview lobby.json --output preview --contact-sheet
screenrig screen publish scr_LOBBY lobby.json

Inspect the document and preview before publishing. playlist init accepts local image/video files, ready med_ IDs, pinned rel_ application releases, and HTTPS iframe URLs in playback order, including mixed inputs:

screenrig playlist init ./poster.png med_VIDEO rel_APP https://example.com --name Lobby --screen-id scr_LOBBY --output lobby.json

Local files upload through the normal media path and wait for readiness. This requires ffmpeg and ffprobe unless --no-transcode is supplied. Preparation writes the local document and may upload media; it neither creates a remote playlist nor assigns a screen. Existing media must be ready. HTTPS URLs must contain no username or password and are not fetched during preparation.

For retryable file preparation, choose --idempotency-key on the first attempt and reuse it with identical files and input order within the server replay window. Each file occurrence gets its own upload keys. Without an explicit key, a new invocation gets new keys. Uploaded media survives a later preparation failure; inspect media list and reuse those IDs when reconciling a partial result.

Each input becomes one full-screen page with a black background and a 200 ms crossfade. Images and videos use --fit contain|cover|fill (default contain). Videos are muted, do not loop, and advance on completion. Other pages use --duration-ms (default 8000). Applications and iframes use fill; application releases are pinned with timed advancement and no controller privileges. Edit the document for application-controlled advancement. The static preview represents applications and iframes with placeholders; verify release readiness and actual playback through the server and intended Player. Framing is a property of the target page, not a publish-time check.

Supply --screen-id to read the target dimensions and revision. The result includes data.screen_id, data.screen_revision, data.preview.argv, and data.publish.argv. Pass these argument arrays to the bundled screenrig launcher without shell splitting: they preserve paths, config, and API origin. Inspect the preview before publishing. Add --expect-rev explicitly to guard against later screen changes. Target metadata stays outside the authored document.

To override the canvas, supply both --target-width and --target-height. These also work without a screen, in which case no publish arguments are returned. Unknown or multiple reported surfaces require explicit dimensions. Output files are exclusive by default; use --overwrite to replace an existing authoring file atomically. Parent directories are created automatically. URL and release-ID inputs with explicit dimensions work without login.

screen publish <screen-id> <file> creates a new playlist, assigns it with an optional screen revision guard, and reads back the assignment. It does not update an existing playlist by name. Display names may repeat; use the playlist ID for an explicit playlist update. Its JSON result reports playlist_id, playlist_revision, screen_id, screen_revision, assignment_verified, and playback_verified. Assignment verification does not prove playback; request and inspect a screenshot and relevant playback evidence separately.

Publishing saves a private local journal automatically. After an ambiguous failure, repeat the identical command and input with the same config to resume. If playlist creation succeeded before assignment failed, the error identifies the created playlist. Do not delete it or start another create to recover. A revision conflict requires inspecting the screen and reconciling the intended assignment; an already created playlist can be assigned explicitly with screen assign. Publishing never silently refreshes the expected revision. Unfinished recovery stops after the 24-hour server idempotency window; inspect and reconcile before making more writes.

To edit an existing playlist:

screenrig playlist show pl_EXISTING --output lobby.json
screenrig playlist update pl_EXISTING lobby.json

playlist show --output writes the editable {name, pages} document and returns its path, playlist_id, and source revision on stdout. Optionally pass that revision with --expect-rev to guard the update. It preserves dynamic selectors, schedules, motion, and pinned application releases, removing server-derived media and timing fields. Comments remain separate. Plain playlist show is an inspection response; --editable without --output returns data.document, data.playlist_id, and data.revision. Updating a playlist affects every screen assigned to it.

K/V and comment writes accept --file - for stdin. Use playlist show ID --editable --human for the editable document on stdout. Application and playlist display names may repeat; use resource IDs for updates.

Replace one application release

After a successful app update, take data.application.release_id and identify the exact playlist, page, and application primitive to change. Preview the replacement:

screenrig playlist replace-release pl_EXISTING --page board-page --primitive board --release-id rel_NEW

This is a read-only impact review, not a rendered preview. Inspect data.previous_release_id, data.release_id, and data.affected_screens, including archived assignments. Apply directly with --apply. To opt into this impact check, also supply --expect-impact with the preview’s data.impact:

screenrig playlist replace-release pl_EXISTING --page board-page --primitive board --release-id rel_NEW --apply

Other primitives and settings are preserved. The update affects every screen assigned to this shared playlist; archived screens retain the new pin for later use. When --expect-impact is supplied, a changed replacement, revision, or observed screen impact requires a fresh preview. Direct apply skips the screen survey; affected_screens is omitted. An already pinned release succeeds without writing. Assignments may change after the snapshot: the server checks the playlist revision atomically only when --expect-rev is supplied. The server validates release availability and ownership on apply. Existing pins remain unchanged until apply; afterward, verify manifest revisions and playback on the affected screens.

Playlist validate, create, update, preview, and screen publish accept - as their input file to read stdin. JSON envelopes are never written into authored files. --expect-rev is the preferred revision spelling; --if-match remains a compatible alias. Supply only one. Omit the flag to write the current resource without a revision precondition. A supplied stale revision still returns revision_conflict.

Generation accepts either --prompt TEXT or --prompt-file FILE, including --prompt-file - for stdin. The file is the complete prompt, with no trimming; the existing 4000-character limit applies. Prompts are excluded from diagnostics.

You should pay per byte, not per screen

Storage and transfers are metered across the project. Each device downloads its own copy and can replay cached media. See pricing for included usage, rates, and a worked fleet example. See pricing →

Keep the path short

The bundled CLI uses api.screenrig.ai; Players use play.screenrig.ai; the reporting plane uses screenrig.ai/dashboard; releases use isolated origins under apps.screenrig.ai. Native Player packages stay private and server-mediated. The internal transport reference remains available for transport tooling.

Tag filters and playback batches

Media carries up to 32 unique tags, each 1–32 letters or digits. Replace the whole set with screenrig media update med_ID --tags Summer,Drinks. --tag Summer replaces it with one tag; --clear-tag clears every tag. media list --tag Summer matches membership anywhere in the set. Tags are case-sensitive and are never authorization.

A dynamic selector can use one tag or a tags filter object:

{
  "by": "tag",
  "tags": { "all": ["Summer", "Drinks"], "any": ["Lobby", "Patio"], "none": ["Expired"] },
  "order": "random",
  "one_at_a_time": false,
  "batch_size": 3
}

all requires every listed tag, any requires at least one, and none excludes any listed tag. Present groups combine with AND; omitted groups impose no condition. Supply at least one nonempty group. Each group accepts up to 32 unique tags. tag and tags are mutually exclusive. A none-only filter selects all ready media of that primitive except the excluded tags. Image selectors select images and video selectors select videos.

The candidate pool contains up to 1,024 ready media per selector, with 1,024 media references across the complete loop snapshot. A larger match returns an error; narrow the filters. batch_size is a separate limit of 1–1,024 items per page visit. On a media_end page, one_at_a_time: false plays the next batch in sequence; omitting batch_size plays the remaining pass. one_at_a_time: true plays one item per appearance and allows only an omitted batch size or batch_size: 1. Duration and application pages require one_at_a_time: true. Images on media_end also require dwell_ms; videos cannot loop on that page.

The Player resolves every dynamic media primitive in one request at playlist startup and at each loop boundary. Membership is frozen during the loop. Uploads, removals, and tagging changes do not regenerate the manifest. At the next refresh, removed or newly excluded items leave the pending queue; new matches join when the current pass finishes. A final batch may contain fewer than the requested number; it does not refill from the next pass. Filename order sorts by filename, then media ID. Random order shuffles once per complete pass, preserving the remaining order across loop refreshes. The cursor is local to the screen, playlist, page, and primitive, persists across restart, and resets when the filter, order, or batch policy changes. Offline playback uses the last successful snapshot. Empty matches skip the primitive and any resulting empty page.

Save the selector object above as selector.json, then inspect its current candidate catalog before publishing:

screenrig media selector-preview selector.json --primitive video

Preview returns matched_count, filename-ordered candidates with media IDs and tags, and the normalized selector policy. With random order it shows the pool and shuffle policy; it does not choose the Player's shuffle, advance a cursor, create a playback grant, or change a manifest. The API is POST /api/selectors/preview with { "primitive": "video", "selector": ... }. It uses the authenticated project and the same matching rules as playback. Catalog changes after preview are resolved at playback's next loop boundary.

Static bundle export freezes dynamic matches as exact IDs. It preserves one-at-a-time or complete-pass playback; a bounded runtime sequence using batch_size with one_at_a_time: false requires removing that field or choosing explicit IDs before export. This prevents silently losing a runtime batch cursor in a static bundle.