Newsroom Reference
This document is the complete working reference for the Newsroom (NRCS): every data model field, the timing math, the readiness rules, the AI generation contract, feed source configuration, and the CG and rundown template schemas.
It is the technical companion to Newsroom, which explains the concepts and workflows. Read that first if you are new to the module; come here when you need the exact behavior of a field, a formula, or a schema.
Table of Contents
- Newsroom Reference
- Table of Contents
- 1. Overview
- 2. The Story Model
- 3. Script Blocks
- 4. Platform Angles
- 5. Rundowns & Segments
- 6. The Timing Engine
- 7. Readiness & Approval Flags
- 8. Live Mode & Keyboard Shortcuts
- 9. AI Story Generation
- 10. The Feed & Ingestion
- 11. CG Template Schema
- 12. Rundown Template Schema
- 13. Assignments, Contacts & Publish Targets
- 14. Storage & Key Map
1. Overview
The Newsroom is composed of six modules sharing one project workspace:
| Module | Surface | Role |
|---|---|---|
| Feed | /newsroom/feed | Inbound wire, RSS, and Google News items |
| Editor | /newsroom/editor | Story list and the three-angle story editor |
| Rundown | /newsroom/rundown | Show building, timing, live mode (the primary surface) |
| Assignments | /newsroom/assignments | Kanban assignment desk, contacts, resources |
| Publishing | /newsroom/publishing | SEO metadata and CMS/social targets |
| Settings | /newsroom/settings | AI prompts, feed sources, rundown templates, CG templates |
Every newsroom object — story, rundown, feed item, assignment, contact, publish target, and every settings entry — is a JSON document in the project's key/value store, under a newsroom: key namespace. The workspace is project-scoped: each project has its own independent feed, stories, rundowns, and settings. Object ids are server-assigned on creation, and an object's id always matches the suffix of its storage key. The full key map is in §14.
2. The Story Model
A story is the central object: every module either produces stories (feed, assignments), edits them (editor), sequences them (rundown), or distributes them (publishing).
| Field | Type | Notes |
|---|---|---|
id | string | Server-assigned |
headline | string | Direction (LTR/RTL) auto-detected from content |
status | enum | Editorial workflow state — see below |
blocks | ScriptBlock[] | The TV script (§3) |
angles | PlatformAngle[] | Web article and social post (§4) |
assignedTo | string[] | Assigned team member ids |
duration | number (seconds) | Computed on save — sum of block durations, excluding non-timed blocks (CG, production cue) |
blockDurations | map: block type → seconds | Computed on save; per-type totals shown in the editor list and rundown |
mediaItems | MediaItem[] | Attached media (video / audio / image, with url, thumbnail, duration, title) |
metadata | object | priority, tags[], sources[] |
sourceIngestIds | string[] | Linked feed items (§10) |
notes | string | Producer/editorial notes |
onHold | boolean | Rundown hold flag (§6) |
createdAt / updatedAt | ISO timestamp |
Statuses
| Status | Meaning |
|---|---|
draft | Created, not yet in active writing |
in-progress | Being written |
review | Awaiting editorial review |
approved | Editorially approved — rundown-ready |
scheduled | Slotted for air |
on-air | Currently on air |
published | Pushed to publishing targets |
archived | Retired, kept for the record |
killed | Removed — killed stories drop out of all lists |
The set approved / scheduled / on-air / published is treated as "approved or beyond" — it drives the rundown's approval flagging (§7) and gates publishing (§13).
Metadata
priority—breaking|urgent|normal|feature.tags[]— free-form labels; publishing appendspublished:{targetId}tags.sources[]— agency names, accumulated automatically as feed items are linked.
The editor also derives live read metrics — word count, estimated read time at the broadcast rate of 2.5 words per second (150 wpm), and block count — from the script as it is written.
3. Script Blocks
The TV script is an ordered list of typed blocks. Blocks are reordered by drag and drop, collapsed individually or all at once, and each carries its own timing.
| Field | Type | Notes |
|---|---|---|
id | string | Unique within the story |
type | enum | One of the seven types below |
content | string | Script text (or the instruction, for production cues) |
duration | number (seconds) | See timing rules below |
notes | string | Internal note for the gallery/director |
mediaRef | string or media reference | A media reference object { mode: online | offline, value, title?, thumbnail?, duration? }. Offline refs on VO/SOT/package blocks are picked from the station's local media catalog (the mapped station-visible path is stored); live-shot uses plain text (location/feed line) |
autoTiming | boolean | Block duration derived from the text |
cgTemplateKey / cgTemplateName / cgFieldValues | CG blocks only | Selected template and its filled field values (§11) |
guidance | Package blocks only | Free-text editing instructions for the video editor |
referenceVideos | Package blocks only | List of { id, url, status: validated | unvalidated, label?, service?, filename?, addedAt } — external video links for the editor, checked as they're added (see Reference-video validation) |
Per-type behavior
| Type | Script text | Media | Auto-timing | New-block default | Counts toward story duration |
|---|---|---|---|---|---|
anchor-read | required | — | yes | 30 s | yes |
vo | required | clip | yes (until clip-locked) | 30 s | yes |
sot | optional | clip | — | 30 s | yes |
package | optional intro | clip (final cut) | — | 30 s | yes |
live-shot | required | location / feed reference | yes | 30 s | yes |
production-cue | instruction | — | — | 5 s | no |
cg | — (template fields instead) | — | — | 5 s | no |
Offline media references
The station-side agent publishes a catalog of the media files on the station's disk to KVS, and the editor reads its own project's catalog. In offline mode, VO, SOT and package blocks pick a file from that searchable catalog instead of typing a path, and the block stores the mapped station-visible path. While no catalog has been published (or it is being refreshed), the picker explains that inline; live-shot blocks keep a free-text location/feed field.
A stored path that is missing from the current catalog — the file was renamed or removed, or the catalog is momentarily stale — is shown as not available on this station and is never cleared automatically: the writer decides whether to reselect.
Timing rules
Three timing modes, in order of precedence:
- Media-linked. When a VO, SOT, or package block references an online clip with a known length, the block's duration locks to the clip's real length (rounded to the second). The duration field becomes read-only while the link exists. An offline reference never locks the duration.
- Auto-timing. Readable blocks (anchor read, VO, live shot) can time themselves from the text:
duration = ceil(words / 2.5)— the standard 150 wpm broadcast read rate. - Manual. Any block's duration can be typed directly when neither of the above applies.
CG and production-cue blocks carry a duration for the gallery but are excluded from the story's total run time and from all rundown timing. The single source of truth for the timing exclusion is NON_TIMED_BLOCK_TYPES / countsTowardTiming in lib/newsroom/storyReadiness.ts.
Reference-video validation
Package blocks carry guidance (free-text editing instructions) plus a list of referenceVideos — external links the writer/producer hands to the video editor, who later attaches the final cut to the same block. When a link is added it is probed against a self-hosted cobalt instance to confirm it's downloadable/supported. Nothing is downloaded at this stage (a separate worker fetches the files later); the probe only reads cobalt's POST / status.
Three outcomes:
- supported → the link is added with
status: validated(origin service and a suggested filename are captured when cobalt returns them). - unsupported (cobalt returned
status: error) → the link is not added; the writer sees an inline reason. - unavailable (no instance configured, unreachable, timed out, rate-limited, or a bad response) → the link is still added with
status: unvalidatedso the writer is never blocked; a re-check can happen later.
Configuration (server/consumer runtime):
| Env var | Required | Purpose |
|---|---|---|
NRCS_COBALT_API_URL | for validation | Base URL of the self-hosted cobalt instance. Unset ⇒ every link is added as unvalidated. |
NEWSROOM_COBALT_API_KEY | optional | Sent as Authorization: Api-Key <key> when the instance requires it. |
Hosted cobalt instances (e.g.
api.cobalt.tools) block third-party use — you must self-host. Endpoint:POST /dsb/v1/newsroom/validateReferenceVideo→services/newsroom.ts:validateReferenceVideo→services/cobalt.ts:probeMedia.
4. Platform Angles
Each story carries up to three angles of the same editorial content:
| Angle | Content | Notes |
|---|---|---|
tv-script | The block list of §3 | The source of truth for AI regeneration |
web-article | Rich text (HTML) + its own headline | Edited in a rich-text editor |
social-post | Plain text | Hard cap of 280 characters, with a live counter |
The web article keeps a separate headline from the broadcast headline — broadcast slugs and SEO headlines serve different masters. Both derivative angles can be written by hand or generated/regenerated from the TV script (§9).
5. Rundowns & Segments
| Field | Type | Notes |
|---|---|---|
id | string | Server-assigned |
name | string | e.g. "News Bulletin 20h00" |
airDate | date | |
airTime | HH:MM:SS | |
plannedDuration | number (seconds) | The show's planned length |
segments | Segment[] | Ordered show structure |
isLive | boolean | Live mode flag (§8) |
currentStoryId | string | The on-air story while live |
Each segment:
| Field | Type | Notes |
|---|---|---|
id | string | |
name | string | e.g. "Opening", "Sports" |
stories | ordered story list | Stored as story ids; rows always render the live story objects |
plannedDuration | number (seconds) | Segment budget for over/under |
hitTime | number (seconds, optional) | Target start offset from the top of the show |
color | enum | none | blue | green | amber | red | purple | teal | pink | orange |
isFloat | boolean | Marks the float section |
Behavior notes:
- Page numbers are auto-assigned: segment letter (
A,B,C, … by position) plus a two-digit row index —A01,B03. The float section always pages asZ. - Ordering: timed segments sort first, the float section last. A rundown has at most one float section.
- Planned duration input accepts
MM:SS,H:MM:SS, or a bare number (interpreted as minutes), with presets from 15 minutes to 2 hours. - Because segments store story references, a story edited in the split editor updates its row — duration, status, readiness — everywhere at once, including in any other rundown that carries it.
6. The Timing Engine
All rundown timing derives from story durations and segment budgets:
front time = sum of durations of all timed stories before this one
cume = front time + story duration
back time = planned show duration − cume
segment O/U = Σ story durations in segment (holds excluded) − segment planned duration
show O/U = Σ durations across timed segments − planned show duration
hit-time delta = segment's computed start − segment hitTime (when a hit time is set)
Exclusions:
- Float segments are not part of the show clock — their stories carry zeroed timing and contribute nothing to over/under.
- Held stories (
onHold) keep their position in the running order, but the clock does not advance through them: they are excluded from segment and show totals, and their timing cells render as em-dashes. - Non-timed blocks (CG, production cue) never contribute to a story's duration in the first place (§3).
Display tones on over/under values: within ±30 seconds renders amber (close enough to ignore), under runs green, over runs red.
7. Readiness & Approval Flags
The rundown grid doubles as a readiness board. Every story row renders its blocks as green/red chips, with a tooltip listing exactly what is missing.
Per-block readiness
| Block type | Ready when |
|---|---|
anchor-read | script text present, duration > 0 |
vo | script text + media attached, duration > 0 |
sot | media attached, duration > 0 |
package | media attached, duration > 0 |
live-shot | script text + location/feed reference, duration > 0 |
production-cue | instruction text present |
cg | template selected + field values filled |
Story-level readiness
Four aggregate signals derive from the blocks:
- script — every spoken block has text;
- media — every VO/SOT/package/live-shot has a media reference;
- cg — every CG block has a template and filled fields;
- timing — the story has a total duration and every spoken block is timed.
Row flags
- Amber left border — story has not yet reached
approved(the approved-or-beyond set of §2). Stories are never blocked from a rundown; the flag keeps the approval state visible while producers build ahead of sign-off. - Muted row + HOLD badge — story is held.
- Red ring + LIVE badge — the current on-air story while live.
8. Live Mode & Keyboard Shortcuts
Going live sets the rundown's isLive flag and points currentStoryId at the first timed story. Next/Back walk the timed running order, the elapsed clock ticks against the planned duration, and the current story is highlighted in the grid and named in the "On Air" pill. Ending live mode clears both fields.
Shortcuts are scoped to the rundown grid (they never fire while typing in an input):
| Key | Action |
|---|---|
Space | Go live / end live |
N / P | Next / previous story (live mode) |
Esc | Exit live mode |
↓ / J, ↑ / K | Move row focus |
Enter | Open the focused story in the split editor |
A | Add a story to the focused segment |
F | Toggle float on the focused story |
H | Toggle hold on the focused story |
S | Jump to the next segment |
? | Show the shortcuts help dialog |
9. AI Story Generation
Generation drafts the full story — TV script blocks, web article, and social post — in one pass, from the material already attached to the story.
Inputs
| Input | Source |
|---|---|
| Editorial direction prompt | Settings → AI Prompts — the newsroom's voice and standards |
| Anchor-read style prompt | Settings → AI Prompts |
| Voice-over style prompt | Settings → AI Prompts |
| Story context | Headline, priority, tags, sources |
| Source material | The full bodies of all linked feed items, concatenated as numbered source sections |
| CG templates | Names and field summaries of every AI-enabled CG template (§11) |
| Mode + instructions | Chosen per generation in the dialog |
Modes
| Mode | TV script | Web article | Social post |
|---|---|---|---|
full | 5–8 blocks: anchor lead, 1–2 VO, at least one SOT (placeholder attribution), optional package, CG blocks where useful, production cues | 500–800 words, headline 60–100 chars | ≤ 280 chars |
medium | 3–5 blocks | 300–500 words | ≤ 280 chars |
simple | 1–2 blocks | 150–300 words | ≤ 280 chars |
Output contract
Generation returns a single JSON object, validated before anything is written to the story:
{
"blocks": [
{
"type": "anchor-read",
"content": "…",
"duration": 18,
"notes": "…",
"cgTemplateKey": "…",
"cgTemplateName": "…",
"cgFieldValues": []
}
],
"webArticleHeadline": "60–100 character SEO headline",
"webArticle": "<p>HTML article body…</p>",
"socialPost": "≤ 280 characters"
}
Duration rules the model must follow (and the validator enforces a minimum of 1 s): spoken blocks at ceil(words / 2.5); defaults of 15 s for a SOT, 90 s for a package, 45 s for a live shot, 3 s for a production cue, 5 s for a CG. Blocks with unknown types are dropped; the social post is truncated to 280 characters.
Behavior
Generation runs as a background task attached to the story — progress is visible in the editor, and reopening the story (from any machine) re-attaches to a generation already in flight. On completion the blocks, both derivative angles, and the recomputed total duration are written to the story.
Angle regeneration
The web article and social post can be regenerated independently at any time, treating the current TV script as the source of truth:
- Input: the serialized script blocks, the editorial direction prompt, optional user instructions, and — when "include current copy" is chosen — the existing angle text to rework rather than replace.
- Web article output:
{ headline, body }— a 60–100 character SEO headline and a 300–800 word semantic-HTML body. - Social post output: plain text ≤ 280 characters, hashtags included.
Angle regeneration runs immediately and returns into the editor (no background task).
10. The Feed & Ingestion
Feed items
| Field | Type | Notes |
|---|---|---|
id | string | |
source | enum | wire | nml2 | rss | gnews | social | press-release |
sourceId | string | Which configured feed source produced the item |
headline / body | string | Body is Markdown |
receivedAt | ISO timestamp | |
agency | string | e.g. "AFP", "Reuters", or the feed's reference name |
priority | enum | flash | urgent | normal |
category | string | Editorial category |
type | enum | article | photo | video |
lang | string | Item language |
tags[] / locations[] | string[] | Topics and geography |
attachments | FeedAttachment[] | See below |
revisions | FeedRevision[] | Update trail for versioned wire items |
version | number | Wire version (Reuters) |
Attachments: { type: url | picture | video | other, displayTitle, sourceUrl, externalUrl, dusFileId? }. When media has been copied into the project's media library, the library file is used for display and the original URL is preserved alongside.
Source kinds
| Kind | Delivery | Behavior |
|---|---|---|
| AFP | Real-time push | Items arrive as the agency publishes; priorities map from agency flags (flash/alert/urgency level); video previews and thumbnails copied into the library |
| Reuters Connect | Polled, versioned | Items keyed by their NewsML id — a version bump updates the existing item in place and appends a revisions[] entry (shown as an "Updated" badge); best video rendition and thumbnail selected and copied |
| RSS | Polled | Self-service in Settings; article link and enclosures become attachments |
| Google News | Polled | Self-service in Settings; search-driven |
Polling checkpoints
Each polled source tracks which item field marks recency and only imports items strictly newer than its checkpoint:
| Field | Set by | Meaning |
|---|---|---|
reference | newsroom | Display/agency label for imported items |
type | newsroom | rss or gnews — fixed at creation |
source | newsroom | Feed URL |
lastItemField | newsroom | Item field used for recency (e.g. pubDate, guid, publishedAt) |
lastItemType | newsroom | How to compare it — date or number |
_lastCheck | system | Timestamp of the last poll |
_lastItemParsed | system | Highest item value seen — the import checkpoint |
Wire sources (AFP, Reuters) are provisioned per project rather than created in Settings; Reuters source records additionally hold credentials, an auto-refreshed access token, and an item-details URL template with a ${{id}} placeholder.
AI pre-import analysis
Every RSS and Google News item passes through an AI classification before it is written to the feed (wire copy skips this — agencies pre-classify their own output). The analysis returns three things:
- priority —
flashstrictly reserved for programming-interrupting events,urgentfor significant developing news,normalotherwise. - category — exactly one of nine: Politics · Economy & Business · Sports · Technology · Entertainment & Culture · Health & Science · Military & Security · Environment & Weather · Society & Human Interest.
- formattedBody — the body re-formatted into clean Markdown word for word: paragraph breaks, bold first mentions of key names, bullet lists for enumerations, markup stripped — never paraphrased, never given invented headings.
11. CG Template Schema
CG templates define the graphics vocabulary available to CG blocks and to AI generation. They are JSON documents, edited in Settings with live validation. Two structural models are accepted: flat (a single fields array) and nested (fields organized into layout_groups).
Top level
| Field | Required | Notes |
|---|---|---|
cg_template_version | yes | Schema version string |
template_name | yes | Shown in the CG block picker and to the AI |
ai_enabled | no | Defaults to available. false hides the template from both AI generation and the CG block picker |
fields | flat model | Array of field objects, at least one |
layout_groups | nested model | Array of groups, at least one |
cg_template_id | no (nested) | Optional external template identifier |
Each layout group: { group_id, group_title, elements: [field, …] } with at least one element.
Field object
| Field | Required | Notes |
|---|---|---|
id | yes | Stable field identifier |
type | yes | text | number | select | color_palette | boolean |
display_name | yes | Label shown in the block editor |
value | yes | Default value (string, number, or boolean) |
validation | no | { required?, character_limit (≥ 1)?, min_value?, max_value?, regex? } |
options | no | [{ label, value }] — for select and color_palette |
ui_hint | no | Free-form rendering hint |
In the block editor, field types render as: text → text input (with character limit), number → numeric input, select → dropdown of options, color_palette → color-dot picker, boolean → switch.
Example (flat model)
{
"cg_template_version": "1.0",
"template_name": "Lower Third — Name & Title",
"ai_enabled": true,
"fields": [
{
"id": "name",
"type": "text",
"display_name": "Name",
"value": "",
"validation": { "required": true, "character_limit": 40 }
},
{
"id": "title",
"type": "text",
"display_name": "Title",
"value": "",
"validation": { "character_limit": 60 }
},
{
"id": "accent",
"type": "color_palette",
"display_name": "Accent",
"value": "#C8102E",
"options": [
{ "label": "Red", "value": "#C8102E" },
{ "label": "Blue", "value": "#003087" }
]
}
]
}
The nested model wraps the same field objects in groups:
{
"cg_template_version": "1.0",
"template_name": "Election Board",
"layout_groups": [
{ "group_id": "header", "group_title": "Header", "elements": [ … ] },
{ "group_id": "results", "group_title": "Results", "elements": [ … ] }
]
}
When a CG block is filled in, the chosen template's key and name plus the entered field values (including group information for nested templates) are stored on the block itself — so a script survives later template edits intact.
12. Rundown Template Schema
Rundown templates capture a standing show structure. They are built in a structured form editor in Settings (no JSON required) and instantiated from the rundown page's "New" dialog.
| Field | Type | Notes |
|---|---|---|
templateName | string | |
airTime | HH:MM:SS (optional) | Default 20:00:00 |
plannedDuration | seconds (optional) | Default 1800 (30 minutes) |
segments | array | Ordered show structure |
Each segment: { name, plannedDuration (seconds), color?, isFloat?, rows[] }.
Each row: { slug, title?, type?, defaultDuration? } — type is a free-text production label (e.g. LVO, Read, Break, Clip/vt), and defaultDuration is in seconds.
Instantiation. Creating a rundown from a template builds the segments in order and creates one draft story per row: the headline comes from the row's title (falling back to its slug), the duration from defaultDuration, and the production label is kept in the story's notes. The result is a complete show skeleton — every slot is a real story, ready to be written, timed, and approved.
13. Assignments, Contacts & Publish Targets
Assignments
| Field | Type | Notes |
|---|---|---|
id | string | |
storyId | string | The linked story — every assignment created on the desk starts with a draft story attached |
title / description | string | The pitch |
stage | enum | pitch → assign → in-progress → review → approved → scheduled → on-air → published (+ archived, hidden from the board) |
assignees | string[] | Team member ids |
resources | array | { type: personnel | camera | vehicle | edit-suite | transmission-unit, name, available } |
dueDate | ISO timestamp | Drives derived urgency |
createdAt | ISO timestamp |
Status sync. When a linked story's status changes, the assignment's stage follows automatically:
| Story status | → Assignment stage |
|---|---|
draft | assign |
in-progress | in-progress |
review | review |
approved | approved |
scheduled | scheduled |
on-air | on-air |
published | published |
archived | archived |
Card priority is derived rather than set: the linked story's priority wins; pitches default to feature; otherwise due-date proximity escalates it (within ~3 hours → breaking, within ~8 hours → urgent).
Contacts
{ id, name, role, organization, phone?, email?, notes? } — a shared per-project contacts book, searchable from the assignment desk.
Publish targets & SEO
Publish targets: { id, type, name, enabled } with type one of wordpress | arc-xp | custom-cms | youtube | facebook | instagram | x-twitter.
Publishing requires the story to be approved or beyond (§2). Publishing to a target marks the story published and appends a published:{targetId} tag. The SEO editor validates: title ≤ 60 characters, description ≤ 160, up to 12 keywords, and well-formed Open Graph image / canonical URLs — with live search-snippet and social-card previews.
14. Storage & Key Map
Every newsroom object is a JSON value in the project's key/value store. Keys are namespaced and ids server-assigned; an object's id always equals its key suffix.
| Key | Value |
|---|---|
newsroom:story:{id} | Story (§2) — rundowns reference these by id |
newsroom:rundown:{id} | Rundown with segments holding ordered story ids (§5) |
newsroom:ingest:{id} | Feed item (§10) |
newsroom:ingest:nml2:{newsmlId} | Reuters feed item — stable key per NewsML id, so versions revise in place |
newsroom:assignment:{id} | Assignment (§13) |
newsroom:contact:{id} | Contact |
newsroom:target:{id} | Publish target |
newsroom:settings:aiEditorialDirectionPrompt | { prompt } — editorial direction (§9) |
newsroom:settings:aiAnchorPrompt | { prompt } — anchor-read style |
newsroom:settings:aiVoiceOverPrompt | { prompt } — voice-over style |
newsroom:settings:cgtemplate:{id} | CG template (§11) |
newsroom:settings:rundownTemplate:{id} | Rundown template (§12) |
newsroom:settings:sourceRss:{id} | RSS feed source (§10) |
newsroom:settings:sourceGNews:{id} | Google News feed source |
newsroom:settings:source:{id} | Provisioned wire source (AFP, Reuters) |
Deletion semantics differ by object: deleting a story sets its status to killed — it leaves every list but its record survives; deleting a settings entry (feed source, CG template, rundown template) archives the entry.
Per-user interface preferences — the rundown split size, the Blocks column display mode (icons vs. text), feed filters, editor panel state, and collapsed blocks per story — are stored per user and follow the user across devices.
