---
title: "Video Production Runbook"
slug: video-production-runbook
section: Video Library
status: stable
description: "How a finished screen recording becomes a live, searchable clip in the Nexus Video Library, and how it is kept honest afterwards."
last-updated: 2026-10-07
---

# Video Production Runbook

How a finished screen recording becomes a live, searchable clip in the Nexus Video Library, and how it is kept honest afterwards. Follow it end to end and the publish command will not bounce the folder back.

## The hand-off folder

Every clip is delivered as one folder with exactly these five files. The publish command refuses a folder that is missing any of them and names the missing file.

| File | What it is |
|---|---|
| `clip.mp4` | The final narrated recording: H.264 High, AAC audio, at most 1080p at 30 fps, at most 60 MB and at most 3 minutes |
| `captions.vtt` | Corrected WebVTT captions (never raw auto-captions) |
| `transcript.md` | The reviewed transcript as plain text, in the order it is spoken |
| `poster.jpg` | A still frame shown before playback |
| `video.toml` | The metadata record (below) |

The folder name does not matter. The slug inside `video.toml` does: it becomes the page address, `/docs/video-` followed by the topic.

## video.toml

Chapter times are `m:ss` offsets from the start of the clip. The duration is measured from the file, so it is not written here.

```toml
slug = "video-approve-passport"
title = "Approve a passport scan"
answers = "Where do I approve a passport scan"
audience = "staff"
program_scope = "all programs"
owner = "platform-team"
last_verified = "2026-11-12"
visibility = "staff"
pii_reviewed = true
related_docs = ["glossary"]
applies_to = ["passport queue"]

[[chapters]]
at = "0:00"
title = "Open the queue"

[[chapters]]
at = "0:42"
title = "Check the passport"
```

- **slug**: `video-<topic>`, lowercase letters, digits and hyphens.
- **answers**: the question a person would type, in their own words. Search matches this first.
- **audience** and **program_scope**: who the clip is for and which programs it covers (`all programs` when it applies to every program).
- **visibility**: `public`, `staff` or `internal`. Leave it out and it becomes `staff`. `internal` clips are only playable by the people on the internal viewers list.
- **last_verified**: the date a person last checked the clip against the live product.
- **related_docs**: slugs of written doc pages that cover the same task. Each must exist.
- **applies_to**: optional free-text tags for the screens or features the clip covers (for example `passport queue`), so a reviewer can see what a product change might outdate.
- **title**, **answers** and chapter titles must not contain double quotes, semicolons or the pipe character.

## From recording to a folder

1. Record with the `demo-video-recording` or `qa-demo-recording` skill, using synthetic QA data only.
2. Normalise the encoding (skip if the recording already meets the budget):

```bash
ffmpeg -i recording.mp4 -c:v libx264 -profile:v high -pix_fmt yuv420p \
  -vf "scale=-2:1080,fps=30" -c:a aac -b:a 192k -movflags +faststart clip.mp4
```

3. Make the poster. Pick a second that shows the screen clearly:

```bash
ffmpeg -ss 00:00:03 -i clip.mp4 -frames:v 1 -q:v 2 poster.jpg
```

4. Make the captions. This command produces a first draft only; read it against the audio and correct every line before it goes in the folder:

```bash
whisper clip.mp4 --model small --language en --output_format vtt --output_dir .
mv clip.vtt captions.vtt
```

5. Write `transcript.md` from the corrected captions: the spoken words only, no timings.
6. Write `video.toml`, then work through the PII checklist.

## PII checklist

Set `pii_reviewed = true` only when every line below is true. The publish command refuses to upload anything while it is false.

- No real applicant, host or staff name, email address or phone number appears on screen or in the audio.
- No passport, visa, SEVIS or other identity document image is visible, even blurred or in the background.
- No real record number, address, date of birth or payment detail is visible.
- No browser tab, notification, bookmark or address bar shows anything outside the demo.
- The captions and the transcript contain none of the above either.

If anything fails, re-record that segment with synthetic QA data. Do not blur and ship.

## Publishing

Set `NEXUS_MEDIA_BUCKET` (the private media bucket) and `GH_TOKEN` (needed only to open the pull request), then from the repository root:

```bash
npm run video:publish -- --dir ./my-clip-folder --dry-run   # validate and show the plan, change nothing
npm run video:publish -- --dir ./my-clip-folder             # upload, write the page, open a draft pull request
```

What it does: checks the folder and the encoding budget, uploads the three assets to `videos/<slug>/<version>/` in the private bucket, writes the page, the sidebar entry and the asset ledger on a `video/<slug>` branch, and opens a draft pull request. Merging that pull request deploys Nexus, so review the page first.

- **Running it again** with the same folder changes nothing and opens no second pull request.
- **Re-recording a clip** (same slug, new files) keeps the page address, uploads a new asset version, refreshes `last_verified`, adds a changelog line, and leaves the old version in the bucket.
- **Changing only the transcript or metadata** updates the page without uploading again.
- `--no-pr` commits locally and stops before opening a pull request.

## Engagement dashboard (PostHog)

The player sends `video_play`, `video_25`, `video_50`, `video_75`, `video_100`, `video_chapter_click` and `video_helpful`, each with `videoId`, `visibility`, `durationSec` and `source` (`nexus`, `help-panel` from the Globus Watch button, `gateway-link` from an assistant answer), plus `chapterIndex` or `helpful` where they apply. No personal data is sent. PostHog only loads on a build that has `VITE_POSTHOG_KEY`, so nothing is collected until the owners enable it. Once it is on, save these four HogQL queries as insights on a dashboard called "Nexus video engagement":

```sql
-- Plays per video (last 30 days)
SELECT properties.videoId AS video, count() AS plays FROM events WHERE event = 'video_play' AND timestamp > now() - INTERVAL 30 DAY GROUP BY video ORDER BY plays DESC

-- Completion rate per video
SELECT properties.videoId AS video, countIf(event = 'video_100') / countIf(event = 'video_play') AS completion FROM events WHERE event IN ('video_play', 'video_100') AND timestamp > now() - INTERVAL 30 DAY GROUP BY video

-- Chapter clicks
SELECT properties.videoId AS video, properties.chapterIndex AS chapter, count() AS clicks FROM events WHERE event = 'video_chapter_click' AND timestamp > now() - INTERVAL 30 DAY GROUP BY video, chapter ORDER BY video, chapter

-- Helpful rate
SELECT properties.videoId AS video, countIf(properties.helpful = true) / count() AS helpful_rate FROM events WHERE event = 'video_helpful' AND timestamp > now() - INTERVAL 30 DAY GROUP BY video
```

## Ownership and freshness

The owner named in `video.toml` owns the clip and its `last_verified` date. Clips are reviewed at least every 90 days. A weekly report lists every clip that is older than 90 days, or whose related code has changed since `last_verified`, and files one ticket per clip on the platform board. Closing that ticket means the owner watched the clip against the product and either re-recorded it or confirmed it is still right and refreshed `last_verified`.

## When it fails

| Message | Fix |
|---|---|
| `missing artefact: <file>` | Add the file named. The captions and poster commands are above |
| `pii_reviewed is not true` | Finish the PII checklist, then set it to `true` |
| `clip.mp4 must be H.264 High` or `must have AAC audio` | Re-encode with the command above |
| `exceeds 1080p`, `longer than 3 minutes` or `larger than 60 MB` | Re-encode or trim; the message shows the measured values |
| `chapter ... starts at ..., after the clip ends` | Fix the chapter time in `video.toml` |
| `relatedDocs entry ... is not a doc page` | Use the slug of an existing doc page |
| `NEXUS_MEDIA_BUCKET is not set` | Set it to the private media bucket |
