Every change on screenjson-server is published on a websocket channel, using the Centrifuge protocol. The server’s own web app uses it to re-render scripts as agents edit them; your code can listen to the same stream.
Channels
| Channel | Carries |
|---|---|
doc:{id} | Every change to one script. |
library | Scripts added, changed and removed. |
pass:{label} | A pass’s progress. |
GET /status names the websocket (events.client_url, normally
/connection/websocket).
A listener in Node
npm install centrifuge
// watch.mjs
import { Centrifuge } from 'centrifuge';
const server = process.env.S ?? 'http://127.0.0.1:8080';
const doc = process.argv[2];
const client = new Centrifuge(server.replace(/^http/, 'ws') + '/connection/websocket');
const sub = client.newSubscription(`doc:${doc}`);
sub.on('publication', ({ data }) => console.log(JSON.stringify(data)));
sub.subscribe();
client.connect();
node watch.mjs 3f64b20e-40b6-4a84-a6f2-da9ae05c11f3
The same code runs in the browser.
What an event looks like
Edit a line (by hand, over the API, or through an agent) and the listener prints:
{
"seq": 3,
"op": "node.updated",
"kind": "element",
"type": "action",
"id": "8cd1ad2a-…",
"path": "/documents/3f64…/scenes/0fea…/elements/action/8cd1ad2a-…",
"rev": 2,
"node": { "type": "action", "text": { "en": "The door opens." }, "…": "…" },
"actor": "dialogue-polish",
"ts": "2026-10-01T17:54:06Z"
}
opisnode.inserted,node.updated,node.removed,node.moved,document.created,document.replaced,document.deletedorcheckout.changed(someone took or released a checkout).nodeis the changed part, as ScreenJSON.actoris the token name that made the change, so you can tell agents and people apart.seqcounts the script’s changes. If you see a gap, you missed something: read the script again.
Uses
- A dashboard of what a team of agents is doing, line by line.
- Re-embedding a line the moment its text changes (or use a webhook, which includes a URL to send the vector back to).
- Keeping a search index or cache in step with the library.