# Schedule screens from a spreadsheet

> Canonical HTML: https://screenrig.ai/docs/schedule-import

A venue keeps its schedule in a spreadsheet: a screen code, a file name, a start
and an end on every line. Import the sheet and screenRIG checks every row
against your screens and media, names each problem by row and column, previews
the calendar your screens would follow, and applies all of it or none of it.

## From sheet to screens

1. **Start from the template.** Save the template as CSV or XLSX, or use your own
   sheet with the same columns.
2. **Fill one row per schedule line.** Name the screen (or a folder or a tag), the
   file, the start and the end.
3. **Import it.** Drop the file on the Import dialog of the dashboard's Schedule
   tab, or run `screenrig schedule import FILE`.
4. **Correct the rows with errors.** Each error names the row, the column and a
   code. Edit the cell in the dialog, or edit the sheet and import it again.
5. **Check the preview.** It lists the levels the sheet makes, the ones it
   changes or removes, and the calendar of the screens it touches.
6. **Publish.** All rows apply together or none does.

Steps 1 to 5 write nothing. A screen needs a time zone (`screen set-timezone`)
and a default playlist (`screen assign`) before a row can schedule it, and a
file must be an approved image or video.

## The sheet

The first row names the columns. Each row after it is one schedule line. Blank
rows are skipped. A sheet is a CSV file (UTF-8, comma or semicolon separated) or
an XLSX workbook, and an XLSX workbook is read from its first sheet.

| Column | Also read as | What it holds |
| --- | --- | --- |
| `target` | `screen`, `screen code`, `code`, `room`, `room id`, `screen name`, `screen id` | The screens the row schedules. Required |
| `content` | `file`, `file name`, `media`, `asset`, `playlist` | What plays. Required |
| `start` | `start date`, `from`, `begin` | When the row begins |
| `end` | `end date`, `until`, `stop` | When the row ends |
| `timezone` | `time zone`, `tz` | The zone the row's times are written in |
| `days` | `weekdays`, `day` | The days of the week the row plays |
| `daily start` | `window start`, `time from` | The time of day each play window opens |
| `daily end` | `window end`, `time to` | The time of day each play window closes |
| `priority` | | 0 to 100. Higher plays over lower. Default 0 |
| `name` | `level`, `label` | A name for the level the row makes |
| `display` | `displays`, `output` | The video wall outputs the row plays on, such as `1` or `1,2` |

Headers match ignoring case, spaces and punctuation. A column nothing reads is
ignored, and the preview lists it. A row needs a `target`, a `content` and at
least one of `start`, `end`, `days` or a daily window.

```csv
Screen,File,Start,End,Timezone,Days,Daily start,Daily end,Priority,Name
LOBBY-01,welcome-spring.jpg,2026-11-05 18:00,2026-11-08 02:00,America/Los_Angeles,,,,0,
folder:Casino Floor,happy-hour.mp4,2026-11-05,2026-11-30,,Mon-Fri,16:00,19:00,5,Happy hour
tag:elevators,playlist:Holiday loop,2026-12-01 09:00,2026-12-26,,,,,0,
```

- Row 2 plays `welcome-spring.jpg` on the screen with the code `LOBBY-01` from
  6 PM on 5 November until 2 AM on 8 November, Pacific time.
- Row 3 plays `happy-hour.mp4` on every screen in the folder `Casino Floor` and
  in the folders inside it, from 4 PM to 7 PM on weekdays, from 5 November to the
  end of 30 November.
- Row 4 plays the playlist `Holiday loop` on every screen with the tag
  `elevators`, from 9 AM on 1 December to the end of 25 December.

## Targets

A target is one of these:

- **A screen.** A screen id, the value of any field of the screen (a room code,
  a store number), or the screen's name. `--code-field` or `code_field` limits
  the match to one field, such as `code` or `room_id`.
- **A folder.** `folder:NAME` or `folder:Parent/Child`. It reaches the folder and
  every folder inside it.
- **A tag.** `tag:NAME` reaches every screen with the tag.

Several targets in one cell are separated by `;` or `|`, as in
`LOBBY-01;LOBBY-02|tag:elevators`.

## Content

- **A file.** The name of a media file in the project, or the original file name
  of a converted upload. The file plays full screen. The first commit that uses
  a file makes the one-file playlist `Scheduled: FILE`, and later imports reuse
  it.
- **A playlist.** `playlist:NAME` or a playlist id. The playlist has an approved
  version and is not a template.

## Times

- **Dates and times.** `2026-11-05 18:00`, `2026-11-05T18:00`,
  `11/5/2026 6:00 PM` and spreadsheet date cells are read. A time with an offset
  such as `Z` or `+02:00` is refused.
- **Each screen's own clock.** Times are read on the clock of the screens the
  row reaches, so `18:00` is 6 PM wherever the screen is.
- **Another zone.** A `timezone` cell (or the request's `timezone`) says the row
  is written in that zone. The times convert to the screens' zone when all the
  row's screens share one zone. A row that reaches screens in several zones
  leaves `timezone` empty and reads each screen's own clock.
- **A date alone as the end** closes that day: `2026-11-30` ends at the end of 30
  November. A date alone as the start begins at midnight.
- **No end** keeps the row playing until a later import removes it. The preview
  warns with `open_end`.
- **Days.** `Mon-Fri`, `Mon,Wed,Fri`, `Sat,Sun`, `weekdays`, `weekends` or
  `daily`. A row with `days` and no daily window plays all day on those days.
- **A daily window.** `daily start` and `daily end` give the hours of each day,
  such as `16:00` and `19:00` or `4:00 PM` and `7:00 PM`. Give both. An end at
  or before the start crosses midnight and belongs to the day it starts. Without
  `days`, the window applies every day.

## Preview and errors

The preview checks every row and every cell before anything is written. A row
with an error is shown with its cells and each error names the column, a code
and what to change.

| Code | Column | Meaning and fix |
| --- | --- | --- |
| `required` | `target`, `content`, `daily start`, `daily end` | The cell is empty, or only one of the daily window cells is filled. Fill it |
| `unknown_screen` | `target` | No screen has that id, field value or name. Check the code, or the field `code_field` names |
| `ambiguous_screen` | `target` | The value matches several screens. Use the screen id or a code that is unique |
| `unknown_folder` | `target` | No screen folder has that name. Check the spelling |
| `ambiguous_folder` | `target` | Several folders have that name. Write the path, such as `folder:Hotel/Casino Floor` |
| `unknown_tag` | `target` | No screen has the tag. Check the spelling |
| `unknown_file` | `content` | No file or playlist has that name. Upload the file, or correct the name |
| `ambiguous_file` | `content` | Several files have that name. Rename one, or write `playlist:NAME` |
| `not_playable` | `content` | The file is not an image or a video, the media waits for review, or the playlist is a template or has no approved version. Approve it, or choose another |
| `invalid_time` | `start`, `end`, `daily start`, `daily end` | The cell is not a date and time, or it carries a zone offset. Write `2026-11-05 18:00` |
| `reversed_times` | `end` | The end is not after the start. Correct either |
| `past_end` | `end` | The end has passed. Move it later, or remove the row |
| `no_timing` | `start` | The row says nothing about when. Give a start, an end, days or a daily window |
| `invalid_value` | `days`, `priority`, `display` | The cell is not a list of days such as `Mon-Fri`, a whole number from 0 to 100, or display numbers such as `1,2` |
| `timezone` | `timezone` | The zone is unknown, or the target holds screens in several zones. Use a zone such as `America/Los_Angeles`, or leave it empty |
| `screen_not_ready` | `target` | A screen has no time zone or no default playlist. Run `screen set-timezone` or `screen assign` |
| `overlap` | `priority` | Two rows hold at the same time on a screen at the same priority. Give one a different priority, or move a time |
| `limit` | `target` | A screen would hold more than 32 levels of its own, or the project more than 100 shared levels. Merge rows, or target a folder or a tag |

Two warnings do not stop an import. `open_end` marks a row with no end.
`outranked` marks a row that a shared level (a row for several screens, a folder
or a tag) plays over on a screen.

A sheet with no rows, a file that is not UTF-8, more than 5,000 rows, a file over
8 MiB, or a commit that would write more than 400 screens' own schedules is
refused with the reason.

## Levels

Rows that agree on content, times, priority, name and display become one level.
The target is not part of the comparison, so ten rows that play the same file
at the same times on ten screens make one level.

- **One screen** makes an entry in that screen's own schedule.
- **Several screens, a folder or a tag** make one shared level, a campaign over
  that audience. A shared level outranks the own levels of a screen.

Grouping turns a venue's 200 screens into one campaign and not 200 entries, and
a correction in the sheet changes one level. The `name` column names a shared
level. Without one, it is called after the import, the content and the target,
as in `Spring promos: happy-hour.mp4 on Casino Floor`.

## Import again

The `name` of an import identifies it, and the default is `import`. Importing
under the same name again:

- updates the levels it made, even when a row's dates changed;
- adds the levels of new rows;
- removes the levels whose rows are gone from the sheet;
- changes nothing when the sheet is the same.

Levels made by an import carry it in their id (a screen's entries start with
`imp_`) and campaigns carry an `import` mark. Levels you made by hand are never
touched. Use one name for one sheet, and a different name for another.

## Run an import

### Dashboard

Open a project's **Schedule** tab and choose **Import**. Drop a CSV or XLSX
file. The dialog shows a table of the rows with each error beside its cell, and
you edit the cell in place. It then shows the composed calendar of the screens
the import changes. **Publish** commits it.

### Command line

```sh
screenrig schedule template --output schedule.csv
screenrig schedule import schedule.csv --name "Spring promos" --timezone America/Los_Angeles
screenrig schedule import schedule.csv --name "Spring promos" --timezone America/Los_Angeles --commit
```

`schedule import` previews and writes nothing. `--commit` applies the sheet when
every row is valid.

| Option | Meaning |
| --- | --- |
| `--name NAME` | The import's name, 1 to 80 characters. The default is the file name without its extension |
| `--timezone ZONE` | The zone the times are written in when a row has no `timezone` |
| `--code-field FIELD` | Read a screen code only from this screen field |
| `--commit` | Apply the sheet |
| `--format csv\|xlsx` | `schedule template`: the template's format |
| `--output FILE` | `schedule template`: where to save it |

The command exits 0 for a valid sheet, 8 when rows have errors (nothing is
written) and 5 when the project changed since the preview (run it again).
`FILE` is a `.csv`, `.xlsx` or `.json` file, or `-` for standard input. A JSON
file holds an array of row objects, or `{"rows": [...]}`, keyed by column name.

### API

Send the sheet as JSON rows, or as the CSV or XLSX body with the settings as
query parameters. Preview needs read access. Commit needs a credential with
write access, which a person who can approve in the dashboard grants.

```sh
curl -s https://api.screenrig.ai/api/schedule/imports/preview \
  --header "Authorization: Bearer $SCREENRIG_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{
    "name": "Spring promos",
    "timezone": "America/Los_Angeles",
    "rows": [
      {"target": "LOBBY-01", "content": "welcome-spring.jpg", "start": "2026-11-05 18:00", "end": "2026-11-08 02:00"},
      {"target": "folder:Casino Floor", "content": "happy-hour.mp4", "start": "2026-11-05", "end": "2026-11-30",
       "days": "Mon-Fri", "daily_start": "16:00", "daily_end": "19:00", "priority": 5, "name": "Happy hour"}
    ]
  }'
```

The same sheet as a file:

```sh
curl -s "https://api.screenrig.ai/api/schedule/imports/preview?name=Spring%20promos&timezone=America/Los_Angeles" \
  --header "Authorization: Bearer $SCREENRIG_TOKEN" \
  --header "Content-Type: text/csv" \
  --data-binary @schedule.csv
```

The answer, abridged:

```json
{
  "name": "Spring promos",
  "valid": false,
  "fingerprint": "FINGERPRINT",
  "columns": ["target", "content", "start", "end", "days", "daily_start", "daily_end", "priority", "name"],
  "summary": {"rows": 2, "rows_with_errors": 1, "levels": 1, "creates": 1, "updates": 0,
              "unchanged": 0, "removes": 0, "screens": 1, "playlists": 1},
  "rows": [
    {"row": 2, "status": "ok", "level": "LEVEL_KEY"},
    {"row": 3, "status": "error",
     "errors": [{"column": "content", "code": "unknown_file",
                 "message": "No file or playlist is named \"happy-hour.mp4\"."}]}
  ],
  "levels": [{"key": "LEVEL_KEY", "action": "create", "home": "screen", "screen_id": "scr_LOBBY",
              "content": {"kind": "file", "id": "med_WELCOME", "name": "welcome-spring.jpg"},
              "screens": 1, "priority": 0, "from": "2026-11-05T18:00", "until": "2026-11-08T02:00", "rows": [2]}],
  "removals": []
}
```

`rows[].cells` returns each row's cells as the sheet gave them, so a client
corrects them and sends the rows again. `valid` is true when no row has an
error. `calendar` has the shape of `GET /api/schedule/calendar` and shows the screens
the import changes as they would be after a commit.

Commit sends the same sheet and the `fingerprint` of its preview, as a member of
the JSON body or a query parameter with a file. Send an `Idempotency-Key` to
retry safely.

```sh
curl -s https://api.screenrig.ai/api/schedule/imports \
  --header "Authorization: Bearer $SCREENRIG_TOKEN" \
  --header "Idempotency-Key: spring-promos-1" \
  --header "Content-Type: application/json" \
  --data @commit.json
```

The answer holds the `preview` and `applied`, the counts of what was created,
updated, left unchanged and removed, with the screens written and the playlists
made. A commit is refused, and writes nothing, as:

| Problem | Status | Meaning and fix |
| --- | --- | --- |
| [`schedule_import_invalid`](https://screenrig.ai/problems/schedule-import-invalid.md) | 422 | Rows have errors. Correct them and preview again |
| [`schedule_import_stale`](https://screenrig.ai/problems/schedule-import-stale.md) | 409 | Screens, media, playlists or levels changed since the preview, or the rows read differently. Preview again and commit with the new fingerprint |

`GET /api/schedule/imports/template?format=csv` and `format=xlsx` answer the
template.

## Limits

- 5,000 rows and 8 MiB per sheet.
- 32 own levels per screen and 100 shared levels per project.
- 400 screens' own schedules written per commit. A folder or tag row reaches any
  number of screens.
- An import name is 1 to 80 characters.

## Related

- [Signage CLI](https://screenrig.ai/docs/cli.md#schedules): schedules, takeover and campaigns for one screen or a fleet.
- [API reference](https://screenrig.ai/docs/api.md): the `/api/schedule/imports` routes and their problems.
- [Authentication](https://screenrig.ai/docs/authentication.md): Read only, Propose and Manage access.
