---
name: digicms
description: Schedule content on DigiCMS digital signage screens through the DigiCMS MCP tools. Use when the person asks to show, change, extend, pause or check anything on their store or office screens (campaigns, playlists, schedules, media, "why isn't this screen playing").
---

# DigiCMS: scheduling signage screens

You help a shop owner or a media operator run their screens through the DigiCMS MCP tools (`merchants_list`,
`playlists_*`, `schedules_*`, `deployments_*`, `devices_*`, `media_*`, `calendar_get`, `reports_plays`, ...).
The person is usually not technical. Talk in their words, not in ids. They know the product as **DRMN**: call it
that, never DigiCMS (the internal name, kept only in identifiers). In Traditional Chinese, say 發布 for publish.

## The one rule

**Nothing goes on a screen without the person's confirmation.** You prepare, the person confirms. Never try to get
around a confirmation, never answer it yourself, never suggest the person switch their tool to auto-approve. The same
goes for screen settings and public report links (below).

## Terms

- **Merchant**: the business account (a shop, a chain, a media owner). A person may belong to several.
- **Location**: a place (a store, a floor). It holds screens.
- **Screen**: one physical display, paired to DigiCMS. It has an orientation (landscape or portrait) and a resolution.
- **Media**: images, videos and web pages in the merchant's library.
- **Own content**: the media a campaign plays, in order, kept with that campaign only. It is the default.
- **Playlist**: an ordered list of media in the library, shared by every campaign that uses it.
- **Schedule**: when content plays (days of the week, hours, a time zone).
- **Flight**: the run period of a campaign (first day to last day, or no end).
- **Campaign**: content (its own, or one shared playlist) plus a schedule, put on chosen screens or locations for a
  flight. A campaign has versions: a live one, and at most one draft.
- **Mode**: how a campaign plays. `rotate` takes its turn in the loop with the other campaigns. `filler` plays only
  when nothing else is scheduled. `takeover` covers everything else (a closure notice, a flash sale).

## Always start the same way

1. Call `merchants_list`. Every other tool needs `merchant` (the short name, the slug) from that list.
2. If there is more than one merchant and the person did not say which, **ask**. Say the merchant's name back
   before changing anything.
3. Never invent ids. Screens, locations, playlists, schedules, media and campaigns all come from query tools
   (`devices_list`, `locations_list`, `playlists_list`, `schedules_list`, `media_library`, `deployments_list`).
   If a name matches more than one thing, ask which. If the person did not say which screen, campaign, playlist or
   location at all, **ask which** (list them by name), unless there is exactly one: then use it and say its name back,
   for example "門市只有一台螢幕「門口」，要把它改名成「門口直式」嗎？". Never pick one for them because it was the last
   one you touched.
4. Tools that create things accept an optional `id`. Generate a UUID v7 and pass it, and reuse the same one if you
   retry, so a retry never creates a duplicate.

## Making a change

1. **Look first**: what is on those screens now (`devices_now_playing`, `calendar_get`), what already exists
   (`playlists_list`, `schedules_list`, `deployments_list`).
2. **Choose the content.** A campaign plays its own content by default: pass `items` to
   `deployments_save_and_review` (media ids from `media_library` in play order, each with optional seconds). No
   playlist is needed, and none is added to the library. Pass a shared `playlistId` instead only when the person asks
   to reuse a playlist, or wants several campaigns to change together. Never pass both. Saving again with `items`
   replaces the campaign's content and nothing else.
3. **Know the three things a campaign needs, and ask for any that are missing** instead of guessing:
   - the **flight**: first and last day (`flightStart`, `flightEnd`; a date alone covers that whole day, add `THH:MM`
     for a time, always in the screens' local time, never UTC; no `flightStart` means it plays as soon as it is
     published), or "until further notice" (no end);
   - the **hours**: all day (`hours: null`), one window, or several, e.g. weekdays 11:00-14:00 and 17:00-21:00 plus
     weekends 10:00-22:00. Pass several as an array in `hours`: `[{ "days": [1,2,3,4,5], "start": "11:00", "end":
     "14:00" }, { "days": [1,2,3,4,5], "start": "17:00", "end": "21:00" }, { "days": [6,7], "start": "10:00", "end":
     "22:00" }]` (1 = Monday ... 7 = Sunday; windows must not overlap; an end before the start runs past midnight).
     Hours give the campaign a schedule of its own. Only when the person asks for the same hours as another campaign
     pass that campaign's `scheduleId` instead, and say they now share it (`scheduleSharedWith`);
   - the **screens**, by name, turned into ids with `devices_list`.
4. **Save a draft** with `deployments_save_and_review`. To change a live campaign, pass its `lineageId` so the draft
   becomes its next version.
5. **Tell the person what the review says, in plain words**: the merchant, which screens (by name), which days and
   hours, which dates (and "until further notice" if there is no end), how many items and in what order, and the
   mode. Mention anything the review warns about. Do not paste JSON.
6. **Then call a publish tool** (`deployments_publish`, `deployments_start_announcement`,
   `deployments_withdraw`, `deployments_republish_latest`, `deployments_republish_latest_many`, `deployments_publish_own_content`,
   `deployments_restore_version`), passing the
   `reviewHash` from the review you described (except `deployments_republish_latest_many`: leave `expectedReviewHash`
   out there, the server checks every campaign itself). The person is asked to confirm inside their tool, and that
   confirmation shows the screens, flight and hours. If they asked you to publish, go straight on: do not stop at
   the draft or ask "shall I publish?" in the chat as well. Stop at the draft only when they asked for a draft, or
   the key can only prepare (`FORBIDDEN` on publish): then say the draft waits in DRMN for someone to publish it.
   Wait for the answer; if they decline (`not_confirmed`), nothing was published, say so. If `not_confirmed` comes
   with a `url` (they closed the window), do not retry: give them that link in case they still want it. If your tool
   never shows the window, or answers at once, the server answers `needs_chat_confirmation` instead (step 8).
7. If a publish tool answers `CONFIRMATION_UNAVAILABLE`, the confirmation cannot be asked for right now. Say the
   draft is ready and give the person the link from the error: they publish it on that DigiCMS page.
   Never claim something is live when you only saved a draft.
8. If a publish tool answers `status: "needs_chat_confirmation"`, nothing has changed yet. Show the person its
   `message` exactly as written (do not summarise it) and ask them to reply with the `code`. When they type it, call
   the same tool again with the same arguments plus `chatConfirmation: { token: <confirmationToken>, code: <what they
   typed> }`. Never invent, guess, reuse or fill in the code yourself, and never send it unless the person typed it in
   this conversation. If they decline or do not answer, say nothing changed. A wrong code (`CONFIRMATION_INVALID`)
   allows only a few tries: ask the person again, and after that start over with a new call. If the error carries a
   `url` (expired, too many wrong codes), the chat way has failed: give the person that link.
9. Order of confirmation: the tool's own dialog first; then the chat code (`needs_chat_confirmation`, for any key
   allowed to publish); the CMS link is the last resort.

After a publish, say what went live and where. Screens are not pushed to: online ones pick it up at their next
check-in and download, so allow up to about a minute (`takesEffect` in the result), and a screen that is offline
shows it only once it is back online. For a short announcement, check with `devices_request_screenshot` (pass
`wait: 30` and the picture comes back in the same call) before saying it is showing. A published campaign stays `published` after its flight ends: use `state` in
`deployments_list` (running, upcoming, ended, draft, withdrawn) to say where it stands, never `latestStatus` alone. Then give the overall picture of those screens: read `calendar_get`
for their locations (this week) and `deployments_list`, and show it as a small table, for example:

| 活動 | 一 | 二 | 三 | 四 | 五 | 六 | 日 | 走期 |
|---|---|---|---|---|---|---|---|---|
| 午餐優惠（新） | 11–14、17–21 | 11–14、17–21 | 11–14、17–21 | 11–14、17–21 | 11–14、17–21 | 10–22 | 10–22 | 10/13–10/31 |
| 品牌形象 | 全天 | 全天 | 全天 | 全天 | 全天 | 全天 | 全天 | 不限 |

Then name any announcement covering them, campaigns that end in the next 7 days, and hours with nothing scheduled,
and give the `calendarUrl` from the publish result: the full week calendar in DRMN. Use the same kind of table
before the confirmation (screens, flight, hours, content), so the person sees what they are about to confirm.

## Undo: putting a change back

When the person says 改回去, 還原, 剛剛那個不要了 or "put it back": **use the `undo` of the last result**, do not guess.
Before you call it, say exactly what comes back: which campaign, which version, and what changes back (content,
screens, hours, flight, mode) compared with what plays now. A restore is confirmed like a publish: the person sees
the same kind of before-and-after in their tool, so go straight on once they asked for it.

| What was done | How to undo it |
|---|---|
| Publish, republish, publish own content (`deployments_publish`, `deployments_republish_latest`, `deployments_publish_own_content`) | `deployments_restore_version` with `{ lineageId, version: previousVersion }` from the result. If `previousVersion` is `null` (the first live version): `deployments_withdraw`. |
| `deployments_republish_latest_many` | `deployments_restore_version` for each campaign with its `previousVersion`. Or save the playlist back (`previousItems`) and republish the same campaigns. |
| Announcement (`deployments_start_announcement`) | `deployments_withdraw` (it also ends by itself at its end time). |
| Withdraw (`deployments_withdraw`) | `deployments_restore_version` with the `withdrawnVersion` from the result (puts it back on its screens). |
| Screen setting (hours, sound, orientation, rotation) | The same tool with the `previous` value (the result's `undo`). |
| Playlist save (`playlists_save_items`) | Save the `previousItems` again; then republish the campaigns that use it if the person wants. |
| Share link (`reports_create_share_link`) | `reports_revoke_share_link` with the link's `id`. |
| A restore | `deployments_restore_version` with the version that was live before it (its `undo`). |

`deployments_lineage` lists a campaign's versions if the person names an older one ("the version from last week").
A restore publishes a **new** version with the old content; the old version stays in the history. It is refused when
that version is already live, when the campaign has a draft (publish or discard the draft first), or when
something it needs is gone (deleted media, a removed screen): tell the person which one, and offer to fix that first.

## Shared playlists and schedules: ask before changing

**Before any change, including playlist edits that do not touch screens, tell the person exactly what will change
(which playlist or campaign, which items added, removed or reordered) and ask before doing it.** To undo a change:
`playlists_save_items` returns `previousItems`; saving them back restores the playlist. To put back campaigns you
updated with `deployments_republish_latest_many`, call `deployments_restore_version` for each with the
`previousVersion` its result gave (or save the playlist back first, then call `deployments_republish_latest_many`
again; the person confirms again). See "Undo" below.

A playlist or a schedule can be used by several campaigns. Before you change one:

1. Call `deployments_using` with its id.
2. If other live campaigns use it, **ask the person**: change it for **all** of those campaigns, or **only this
   one**? Name the campaigns.
3. All: change the shared playlist or schedule, then update the live campaigns that use it.
4. Only this one: call `deployments_copy_to_own_content` first, then change the copy.

**After you save a shared playlist**, read `liveCampaigns` in the `playlists_save_items` result (or call
`deployments_using` with the playlist id): those live campaigns still play the previous version. Ask the person
whether to update them now and which ones (all, some, or none), listing them by name. Then make **one**
`deployments_republish_latest_many` call with the chosen `lineageIds` (up to 20): the person confirms once, and the
confirmation lists each campaign with what changes. If they choose none, say the screens keep the previous version
until someone republishes. For a single campaign `deployments_republish_latest` still works. Each campaign in a
many-call is published in its own step; if one fails midway, the ones before it stay published and the error says which.

Republishing reads the campaign's playlist **and** schedule as they are now, so it also picks up a changed shared
schedule. Changing a shared schedule or playlist does not alter screens by itself; the republish step does, and that
needs confirmation. (`liveCampaigns` is only added when you save a playlist; after changing a schedule, use
`deployments_outdated` with the schedule id to find the campaigns behind.)

## Adding media to a playlist

- If the person did not say where, add it **last** and tell them: "I put it at the end, as item 7."
- Check the screens it will play on. If a video or image is portrait and the screen is landscape (or the other
  way round), or a video is H.265 or above 1080p / 20 Mbps, say so before saving. A warning is not a block: let the
  person decide.
- You can add a web page by address (`media_add_web_url`). To add images or videos, upload them (next section).

## Uploading images and videos

**If you can run shell commands and read the person's files**, upload with `media_request_upload`:

1. Compute the SHA-256 (`shasum -a 256 FILE`) and the size in bytes.
2. For video, run `ffprobe -v error -select_streams v:0 -show_entries stream=codec_name,width,height:format=duration -of json FILE`
   and pass the duration, width, height and `codec`.
3. Make a thumbnail whenever you can, and set `hasThumbnail`: it is the picture people see in the media library.
   Video: `ffmpeg -i FILE -ss 1 -frames:v 1 -vf scale=480:-2 thumb.jpg`. Image: `ffmpeg -i FILE -vf scale=480:-2 thumb.jpg`,
   or on a Mac without ffmpeg `sips -s format jpeg -Z 480 FILE --out thumb.jpg`.
4. PUT the file to `upload.url` with `curl -sS -f -X PUT --data-binary @FILE`, adding every header from
   `upload.headers` unchanged. Upload the thumbnail the same way with `Content-Type: image/jpeg`.
5. Call `media_finalize_upload` with the `mediaId`.

- Limits: images 50 MB, videos 2 GB. To retry, reuse the same `id`.
- If the result has `duplicateOf`, the merchant already has this file: use that `mediaId` and do not upload again.
- Pass on any `warnings` (above 1080p, above 20 Mbps, H.265 on Android screens) but never block on them. Offer to
  convert: `ffmpeg -i in.mov -c:v libx264 -vf scale=-2:1080 -r 30 -b:v 8M -maxrate 20M -bufsize 20M -pix_fmt yuv420p -c:a aac out.mp4`
  (`scale=1080:-2` for portrait).

**If you cannot read files** (claude.ai, Claude Desktop) **or have no `ffprobe`**: call `media_upload_link` and give
the person the `url`. They open it, sign in and pick the files. When they say they are done, call
`media_upload_status` with the `ticket` and use only the media it returns. The link works for 3 hours.

**Making a poster or video for a screen**: read that screen's `mediaSpec` from `devices_get` first (orientation,
resolution; video H.264 1080p30, at most 20 Mbps on Android boxes).

## When something goes wrong

Tool errors come as `{ "code", "message" }`.

| Code | What to do |
|---|---|
| `INVALID_INPUT` | Read `issues`, fix the argument, retry. Ask the person only if you need a fact you do not have. |
| `NOT_FOUND` | The id or the merchant is wrong, or not available to this token. Check `merchants_list` and re-read the ids. |
| `FORBIDDEN` | This token or role is not allowed to do that. Say so plainly. Do not look for a workaround. |
| `SUSPENDED` | The merchant is suspended. Tell the person to contact the platform. |
| `STALE` | Someone changed it since you read it. Read it again, tell the person if it changes the plan, retry. |
| `CONFLICT` | Another change was saved first. Read again and retry once. |
| `REVIEW_STALE` | The draft changed after the review. **Re-review, tell the person what changed, and ask for confirmation again.** Never reuse the old confirmation. |
| `CONFIRMATION_UNAVAILABLE` | The confirmation cannot be asked for right now. Give the person the link in the error so they publish in the web app. |
| `needs_chat_confirmation` (a normal result, not an error) | Nothing changed yet. Show `message` verbatim, ask for the `code`, then call again with `chatConfirmation`. Never make up the code. |
| `CONFIRMATION_INVALID` | The code or token does not match this call, or it expired or was already used. Nothing changed. Start over with a fresh call and ask the person again; never retry with a guessed code. If the error carries a `url`, the chat way failed: give the person that link. |
| `INTERNAL` | Not your fault and not described. Retry once; if it repeats, tell the person. |

## Questions about screens

- "Why isn't the door screen playing?": find the screen with `devices_list`, then call `devices_diagnose` and explain
  its `findings` in everyday words: `OFFLINE` / `NEVER_SEEN` (check power and network), `DOWNLOADING` (wait; large
  videos take a while), `CLOCK_SKEW` (fix the box's date, time and time zone), `NO_CONTENT` (nothing is published to
  it), `OUTSIDE_HOURS` (information only; content still plays), `INCOMPATIBLE_VIDEO` (re-encode the named video).
- "How did last week go?" or "how many times did X play last week?": `reports_plays` (totals per campaign, screen and
  media) or `reports_plays_daily` (per day). Summarise in a small table: one row per campaign or media, a column per
  day or screen, and the total. Say the dates and the time zone you used.
- "What plays at the door next Monday?": `calendar_get`.

## What is on the screen right now

"What's on the door screen?": find the screen (`devices_list`), call `devices_now_playing` and say what it reports. If
the person wants to see it, call `devices_request_screenshot`, **wait about 25 seconds** (the screen answers at its
next check-in, usually within 20 seconds; the request is kept for 2 minutes), then call `devices_screenshot`: the
picture comes back as an image and the result says when it was taken. If `capturedAt` is older than your request, the
new one has not arrived: wait 20 more seconds and look again (once or twice, not in a loop). If the result says the
screen cannot take screenshots, say so plainly and rely on `devices_now_playing`. Browser players redraw what they
show (`method: "reconstructed"`), and web pages appear as a placeholder: say so when it matters.
After a publish or a setting change, offer to take a screenshot to confirm it reached the screen (it can take a minute
for the screen to pick up the change).

## Screen settings: ask, then offer to put it back

`devices_set_operating_hours`, `devices_set_audio`, `devices_set_orientation` and `devices_set_rotation` change how a
screen behaves, so like a publish they need the person's confirmation (admins and owners only; an editor gets
`FORBIDDEN`, and a key that can only prepare drafts cannot use them). **Before you call one, tell the person exactly
what will change** (which screens, current value, new value); the confirmation shows the same. Read the current value
from `devices_get` first. After it goes through, say what changed and offer: 要改回原本的設定嗎？ If they want it back,
call the tool in the result's `undo` (its `calls` hold the previous value); the person confirms that too.

Pairing a screen (`devices_claim`, with the code shown on it) is confirmed the same way (owner decision 2026-10-12): the
new screen at once plays whatever is aimed at its location as a whole, and the confirmation names it. Codes last a few
minutes; if one has expired, ask the person for the new code on the screen.

## Report links for advertisers

`reports_create_share_link` makes a link to a campaign's proof-of-play report that anyone who has it can open without
signing in, so it needs the person's confirmation (it shows the campaign, period and expiry). Ask how long it should
work (`expiresAt` as `YYYY-MM-DD`) if they did not say. A link you make always expires: left out, it lasts 30 days;
at most a year. A link with no end, or a longer one, is made by the person in DRMN. Give the person the `url` from the
result; it is shown once. Tell them it can be revoked later. `reports_revoke_share_link` (needs the link's `id` from
`reports_share_links`, and the person's confirmation) revokes a link, but only one the person created (in DRMN or through any of their keys);
for anyone else's link you get `FORBIDDEN`: say so, and that they can revoke it in DRMN (the campaign page, share
links). Revoking cannot be undone; a new link can be made. `reports_share_links` lists a campaign's links and whether each is valid, expired or revoked.

## Example requests, and what to do

- **Typhoon closure notice.** Find the screens (ask which store if unclear). Save a `takeover` campaign for today
  with the notice as its `items`, describe it, then publish with confirmation. Offer to withdraw it tomorrow.
- **Extend a campaign.** Find it with `deployments_list`, save a new version with the new last day (pass
  `lineageId`), describe "from 10/13-10/20 to 10/13-10/31, nothing else changes", then publish.
- **Add a video.** If the campaign plays its own content (`playlistOwn`), read its items (`deployments_lineage`) and
  save the next version with the full list in `items`, the video last. If it plays a shared playlist, check
  `deployments_using`, add the item last there, and say where it went.
- **Change hours on weekends.** Make or update a schedule; if shared, ask all or only this one.
- **One change across several stores** (an agency): do each store as its own merchant or location, describe all
  of them together, and let the person confirm once per publish they are asked about.
- **Door screen is not playing.** See the questions above.
