# Writing a Briefing

A briefing is a short spoken update delivered by an illustrated presenter. The presenter talks
through a script you write, with on-screen captions and graphics, and can stop to ask someone a
question. Their answer decides what the presenter says next, and every answer is sent back to
whoever asked for the briefing.

This guide covers everything you need to write one: the file format, how to write lines that
sound good when spoken by a synthetic voice, how questions and branching work, and what comes back
once people have watched it.

## The short version

```
@briefing "Weekly Roundup"

@say emotion=cheerful
Good morning, everyone! Here's your roundup for {date}.

@ask id=printer to=Sam type=choice
Sam, is the printer lease sorted?
- Yes, signed -> done
- Not yet -> not_yet

@node not_yet
@say emotion=concerned
Okay. Let's get that wrapped up by Friday.

@node done
@say emotion=cheerful
That's everything for this week. Thanks!
@end
```

- Lines starting with `@` are instructions.
- The text under `@say` is what the presenter says.
- `@ask` asks a question, with answer choices underneath.
- `@node` marks a spot that an answer can jump to.

## The file

- Plain text, saved as UTF-8, with the extension `.briefing` (for example `weekly-roundup.briefing`).
  Use letters, digits, `-` and `_` in the file name. It becomes part of the briefing's web address.
- Lines starting with `#` are comments for the reader. They're never spoken.
- A blank line ends the text of a `@say` or `@ask`. Text that isn't under a `@say` or `@ask` is an
  error.

**Watch out for:**
- A line of spoken text must not start with `#`, or it's treated as a comment. Write
  "Item number 14…" or "Next, #14…" instead of starting with "#14".
- Inside an `@ask`, a line starting with `-` is an answer choice.

## Settings at the top

These go before the first `@say`. Only the title is really needed.

| Setting | Example | What it does |
| --- | --- | --- |
| `@briefing` | `@briefing "Weekly Roundup"` | The title, shown on screen and in the responses. |
| `@var` | `@var team="Support Team"` | Defines a word you can use in the text as `{team}`. Several on one line are fine: `@var team="Support" site=Dallas`. |
| `@ticker` | `@ticker "#12 · Firewall audit · overdue 1 week"` | One entry in the scrolling ticker under the video. Repeat it for more entries. |
| `@seed` | `@seed 42` | The default take for every line (see "Re-recording a line"). Usually leave it out. |
| `@voice` | `@voice default` | Which voice to use. Leave it as `default` unless we've agreed on another. |
| `@model` | `@model standard` | Leave this out. Only `standard` supports emotions. |

`{date}` is always available. It reads as the day the briefing is produced, for example
"Wednesday, October 14".

## Lines: `@say`

```
@say emotion=concerned item=apiintl/apiconnect#14 chyron="#14 · Cisco renewal · due Fri"
First up: the Cisco renewal is due Friday, and it's still open.
```

The text can run over several lines in the file. They're joined into one line of speech, ending
at the next blank line or instruction.

| Option | Example | What it does |
| --- | --- | --- |
| `emotion` | `emotion=cheerful` | How the line is delivered. See the table below. Default `neutral`. |
| `id` | `id=intro` | A name for the line. Optional for `@say`, but it helps when reading the responses (see `path` below). |
| `item` | `item=apiintl/apiconnect#14` | Links the line to an action item. Viewers can click through to it. |
| `chyron` | `chyron="#14 · Cisco renewal · due Fri"` | The caption card on screen. It stays up until another line sets a new one. With only `item`, the card shows the item number. |
| `seed` | `seed=7` | Picks a different take of this line (see below). |
| `exaggeration` | `exaggeration=0.9` | Fine-tunes how expressive the line is, from 0 (flat) to 2 (very animated). Usually leave it to `emotion`. |

Put values containing spaces in double quotes. A value can't contain a double quote itself.

### Emotions

| Emotion | Use it for |
| --- | --- |
| `neutral` | Plain information. The default. |
| `cheerful` | Greetings, good news, wins, sign-offs. |
| `concerned` | Something slipping, a risk, a blocker. |
| `urgent` | Deadlines today, things that need action now. Use sparingly. |
| `wry` | A light joke or a knowing aside. |

The presenter's face follows the emotion too (brows, head tilt), so pick the one that fits the
line rather than the whole briefing.

## Questions: `@ask`

```
@ask id=cisco to=Dana type=choice
Dana, where are we on this one?
- On track -> cisco_ok
- Blocked, need help -> cisco_help
- I'll explain -> cisco_explain [record]
```

The presenter speaks the question, then waits for an answer before going on.

| Option | Example | What it does |
| --- | --- | --- |
| `id` | `id=cisco` | **Required.** The name the answer is reported under. Use something short and meaningful, and don't change it once the briefing is out. Each question needs its own id. |
| `to` | `to=Dana` | Who the question is for. It's shown as "Question for Dana". Use `to=everyone` for a question anyone can answer. |
| `type` | `type=choice` or `type=text` | Choose-an-answer buttons, or a typed reply. Default `choice`. |
| `next` | `next=wrap` | For `type=text`: where to go after the answer. Without it, the briefing continues with the line after the question. |
| `emotion`, `seed`, `exaggeration`, `item`, `chyron` | | The same as for `@say`. |

**Choices** go on the lines right under the question:

```
- Button label -> node_name
```

- Each choice jumps to the `@node` with that name.
- Keep labels short (2–5 words). They're buttons, not sentences.
- Two to four choices work best.
- Add `[record]` at the end when the person should explain in their own words. For now they type
  the explanation. Later they'll be able to record a voice reply, which gets transcribed.

**Typed answers** (`type=text`) show a text box. Say in the question what kind of answer you want:
"Type a quick note…", "In a sentence or two…".

## Branching: `@node`, `@goto`, `@end`

- `@node name` marks a place to jump to. Names use letters, digits and `_`. Each name can be used
  only once.
- `@goto name` jumps there.
- `@end` finishes the briefing. Reaching the end of the file also finishes it.

**Important:** a `@node` is only a label. When the presenter reaches one, they simply carry on
into it. Nothing stops at a node. So end each branch with a `@goto` to where the briefing should
continue, unless running on into the next section is what you want:

```
@node cisco_ok
@say emotion=cheerful
Great news. On to the next item.
@goto next_item          ← without this, cisco_help would be played next

@node cisco_help
@say emotion=concerned
Got it. I'll flag that for Thursday's standup.
@goto next_item

@node next_item
@say
Next up…
```

Check that every path through the briefing reaches the end, and that every choice and `@goto`
points to a node that exists. We check this when the briefing is produced and send back a list
of any problems by line number.

## Writing for the voice

The voice is synthetic, and these habits make it sound natural:

- **Write the way people talk.** Use short sentences and contractions ("it's", "we'll").
  Punctuation shapes the delivery: commas give a short pause, a period a full stop, and a question
  mark lifts the voice at the end.
- **One to three sentences per `@say`.** Shorter lines sound better, are easier to re-record, and
  let the emotion change as often as the content does. Keep each line under about 2000 characters.
- **Write numbers and symbols the way they should be said.** Write "about five hundred dollars",
  not "~$500". Write "the twenty-third", not "23rd". Avoid "e.g.", "&", "/", and text in
  parentheses.
- **Names, acronyms and jargon:** list any that might be mispronounced (customer names, product
  names, things like "APISSO" or "3CX") when you hand off the script. We'll set how each is said,
  and the captions keep your spelling. Please don't misspell words in the script to force a
  pronunciation, because the misspelling would show in the captions.
- **Length:** aim for 3 minutes or less (around 400–450 words on the longest path). The briefing
  is produced ahead of time, roughly 5 minutes of production per minute of speech, so leave a
  little time before it's needed.
- **Captions** are made from your exact text, so check spelling and grammar as you would for
  anything published.
- **Don't impersonate anyone.** The presenter is always labeled as a synthetic voice. Don't write
  lines that pretend to come from a real person, or that quote people saying things they didn't
  say.

### Re-recording a line

Each line is produced once and reused. If a line sounds wrong once it's produced (odd emphasis,
a rushed ending), the fix is to give that line a different take: add `seed=` with any other number
(`seed=7`, `seed=8`, …) and send the script back. Only that line gets produced again. Changing a
line's text or emotion also re-produces just that line.

## Before you hand it off

- [ ] Title set with `@briefing`
- [ ] Every `@ask` has a unique `id=`, and its `to=` is right
- [ ] Every choice and `@goto` points to a `@node` that exists, and no node name is used twice
- [ ] Every branch ends in a `@goto` or `@end`, unless running on into the next section is intended
- [ ] No spoken line starts with `#`
- [ ] Numbers and symbols are written as they should be said
- [ ] A pronunciation list for names, acronyms and jargon
- [ ] Who should receive the responses

Send the `.briefing` file, the pronunciation list, and the name of whoever should receive the
responses.

## What comes back: the responses

Each person who watches the briefing produces one **responses package**: a JSON file with what
they saw and how they answered. This goes back to the team that asked for the briefing.

```json
{
  "format": "connect.briefing.responses/1",
  "briefing": {
    "slug": "demo",
    "title": "Action Item Roundup (demo)",
    "rendered_at": "2026-10-14T14:39:31+00:00"
  },
  "respondent": { "name": "Dana" },
  "started_at": "2026-10-14T15:01:32.795Z",
  "completed_at": "2026-10-14T15:03:39.504Z",
  "exported_at": "2026-10-14T15:03:40.504Z",
  "mode": "audio",
  "path": ["intro", "s2", "cisco", "s8", "s9", "blockers", "outro"],
  "responses": [
    {
      "question_id": "cisco",
      "to": "Dana",
      "type": "choice",
      "question": "Dana, where are we on this one?",
      "options": ["On track", "Blocked, need help", "I'll explain"],
      "choice": "I'll explain",
      "text": "Waiting on the vendor quote, should have it Thursday.",
      "wanted_recording": true,
      "answered_at": "2026-10-14T15:02:05.120Z"
    },
    {
      "question_id": "blockers",
      "to": "everyone",
      "type": "text",
      "question": "Anything blocking you this week? Type a quick note and I'll add it to the list.",
      "options": null,
      "choice": null,
      "text": "Nothing blocking.",
      "wanted_recording": false,
      "answered_at": "2026-10-14T15:03:21.880Z"
    }
  ]
}
```

| Field | Meaning |
| --- | --- |
| `format` | Always `connect.briefing.responses/1` for this version of the package. |
| `briefing.slug` | The briefing's file name without `.briefing`. |
| `briefing.title` | The `@briefing` title. |
| `briefing.rendered_at` | When this version of the briefing was produced, so answers can be matched to the script they were answering. |
| `respondent` | Who answered. In the demo it's the name they typed, or `null` if they left it blank. Once briefings need a login, it comes from their account. |
| `started_at`, `completed_at` | When they started, and when they reached the end (`null` if they stopped partway). |
| `exported_at` | When the package was made. |
| `mode` | `audio` if they watched and listened, `text` if they chose to read it instead. |
| `path` | The `id` of every line and question they were shown, in order. Lines without an `id=` get a generated one (`s2`, `s9`…), which can change when the script is edited, so give important lines an id. |
| `responses` | One entry per question answered, in order. |
| `responses[].question_id` | The question's `id=`. |
| `responses[].to` | The question's `to=`, or `null`. |
| `responses[].type` | `choice` or `text`. |
| `responses[].question` | The question as it was spoken. |
| `responses[].options` | The choice labels offered (`null` for `text`). |
| `responses[].choice` | The label they picked (`null` for `text`). |
| `responses[].text` | What they typed: the answer to a `text` question, or the explanation after a `[record]` choice. Otherwise `null`. |
| `responses[].wanted_recording` | `true` when they picked a `[record]` choice. For now they type the explanation instead. |
| `responses[].answered_at` | When they answered. |

All times are ISO 8601 in UTC. New fields may be added later under the same `format`, so ignore
any you don't recognize. Removing or changing an existing field gets a new `format` version.

**In the demo**, the viewer downloads the package with "Export responses" (any time after the first
answer) or "Download responses (JSON)" at the end. Later the system collects the packages and
sends them to the requesting team automatically, in this same format.

## A complete example

```
# Demo briefing. The items and answers are made up.

@briefing "Action Item Roundup (demo)"
@voice default
@seed 42
@var team="team"
@ticker "#9 · Printer lease renewal · overdue 3 days"
@ticker "#12 · Firewall audit · overdue 1 week"
@ticker "#14 · Cisco renewal · due Friday"

@say id=intro emotion=cheerful
Good morning, {team}! Here's your action item roundup for {date}.

@say emotion=concerned item=apiintl/apiconnect#14 chyron="#14 · Cisco renewal · due Fri"
First up: the Cisco renewal is due Friday, and it's still open.

@ask id=cisco to=Dana type=choice
Dana, where are we on this one?
- On track -> cisco_ok
- Blocked, need help -> cisco_help
- I'll explain -> cisco_explain [record]

@node cisco_ok
@say emotion=cheerful
Great news. On to the next item.
@goto apisso

@node cisco_help
@say emotion=concerned
Got it. I'll flag that for the Thursday standup.
@goto apisso

@node cisco_explain
@say emotion=neutral
Thanks, Dana. I'll pass that along.

@node apisso
@say emotion=wry item=apiintl/apiconnect#21 chyron="#21 · APISSO passkeys · in progress"
Next, the APISSO passkey work. It's in progress, which, as we all know, is the most comfortable status there is.

@ask id=blockers to=everyone type=text next=wrap
Anything blocking you this week? Type a quick note and I'll add it to the list.

@node wrap
@say id=outro emotion=urgent chyron="Overdue: #9, #12"
One last thing: two items are overdue. Please take a look at the ticker before lunch. That's all for today!
@end
```
