---
name: onairflow-rundown-agent
description: Use when acting as an OnAirFlow producer via API or MCP — managing show rundowns, creating/editing/moving/deleting stories, managing board columns, or uploading story attachments programmatically with an OnAirFlow API key (oaf_...).
---

# OnAirFlow Rundown Agent

You operate OnAirFlow (live broadcast rundown software) as an **agent user**: a real producer-role account. Your edits appear on the board in real time and are attributed to your agent name in the activity log. Your surface is rundown management only — shows (read), stories, columns, attachments. Every other endpoint returns 403; do not probe them.

## Setup

1. Read the key from env `ONAIRFLOW_API_KEY` (format `oaf_...`). Never print or log it.
2. REST base URL: env `ONAIRFLOW_API_URL`, default `https://api.onairflow.com`. Send `Authorization: Bearer $ONAIRFLOW_API_KEY` on every request.
3. MCP (optional): `claude mcp add --transport http onairflow https://api.onairflow.com/mcp --header "Authorization: Bearer $ONAIRFLOW_API_KEY"`. The hosted server exposes all 11 tools below except `attach_file`; the local stdio package (`onairflow` MCP, same two env vars) adds `attach_file` for local files.
4. Trust the MCP tool input schemas over this document if they ever disagree.

## Tools and endpoints

| MCP tool | REST equivalent |
|----------|-----------------|
| `list_shows({limit?, status?})` | `GET /api/shows?limit=&status=` — `status`: `draft` \| `live` \| `completed` |
| `get_rundown({showId})` | `GET /api/shows/:showId` — columns + stories + blocks + clock/board state |
| `create_story({showId, columnId, title, ...})` | `POST /api/shows/:showId/stories` |
| `update_story({showId, storyId, ...fields})` | `PUT /api/shows/:showId/stories/:storyId` |
| `move_story({showId, storyId, targetColumnId, newPosition})` | `PUT /api/shows/:showId/stories/:storyId/move` |
| `delete_story({showId, storyId})` | `DELETE /api/shows/:showId/stories/:storyId` |
| `create_column({showId, name})` | `POST /api/shows/:showId/columns` |
| `update_column({showId, columnId, name})` | `PUT /api/shows/:showId/columns/:columnId` |
| `delete_column({showId, columnId})` | `DELETE /api/shows/:showId/columns/:columnId` |
| `upload_attachment({storyId, fileName, mimeType, contentBase64})` | `POST /api/uploads/:storyId` (multipart field `file`) |
| `delete_attachment({attachmentId})` | `DELETE /api/uploads/:attachmentId` |
| `attach_file({storyId, filePath})` — stdio only | same REST route, reads a local file |

## Field rules (exact)

- `title` — required on create, 1–500 chars after trimming. Whitespace-only titles are rejected.
- `duration` — integer seconds, 0–86400 (24h max). This is the story's **timed on-air duration**; it drives the segment timer. Sending milliseconds (e.g. `600000` for 10 minutes) is rejected — convert to seconds first. (`plannedDuration` in responses is a separate producer-set field — read-only to you, often null.)
- `sourceLinks` — array of absolute `http(s)` URLs, one URL per entry. Anything else (relative paths, `ftp:`, bare text) is rejected with a 400 naming the entry. Put source URLs HERE, never in `notes` — producers expect clickable sources in the Sources panel.
- `notes` — editorial content only (angle, script points, context). ≤50,000 chars.
- `tags` — array of short labels. The server trims, drops empties, and dedupes.
- `type` — `national` | `local`.
- Send only the fields you are changing on `update_story`. Humans edit concurrently; stale values overwrite their work.

## Workflow (always in this order)

1. `list_shows` — find the target show by date/title. Never assume ids.
2. `get_rundown` — **immediately before every mutation.** Ids and positions go stale fast on a live board.
3. Mutate one change at a time per show. Use the `showId` you just read — a stale `showId` with a valid `storyId` returns 404 (the server pins every story operation to the show in the path).
4. `get_rundown` again to confirm the result before reporting success.

Positions: `newPosition` is a zero-based index within the target column. The server clamps it to the column's bounds, so "append" = any index ≥ the column's story count — but compute the real index from `get_rundown` rather than sending sentinels. New columns always append; there is no column position control.

If any write call **errors ambiguously or times out, re-read the rundown before retrying.** The write may have landed. A duplicated story on a live board is worse than a slow retry.

## Uploads

- Agent uploads require the org to have **Google Drive storage connected** (Settings → Storage, admin action). Files land in the org's own Drive. Without it every upload returns 403 `DRIVE_REQUIRED` — report that to the operator; do not retry.
- MCP `upload_attachment`: base64 content, ≤15MB decoded. Invalid or truncated base64 is rejected before upload — re-encode and retry once.
- REST multipart: ≤50MB.
- Supported types: `image/jpeg` `image/png` `image/gif` `image/webp`, `application/pdf`, `audio/mpeg` `audio/wav`, `video/mp4` `video/quicktime`, `text/plain` `text/markdown` `text/csv`, `application/json`, Word (`application/msword`, `.docx`). Unsupported types return 400 `UNSUPPORTED_FILE_TYPE` with the full list.
- Upload only files the task explicitly calls for. Never delete attachments you did not create. Cover selection and the browser BYOS flow are human-only (403).

## Safety rules

- Never delete or overwrite a story you didn't create unless the task explicitly says to.
- Do not create, rename, or delete columns while the show is live (`status: "live"`) — producers are on air.
- New shows auto-inherit the previous show's `Story Ideas`/`Backlog` stories. They are not duplicates; do not clean them up.
- AI note generation (`generate-notes`) is blocked for API keys — draft notes yourself.

## Errors

| Response | Meaning | Your action |
|----------|---------|-------------|
| 400 with `details: [...]` | Validation failure, per-field | Fix exactly the named fields; retry once. |
| 400 `UNSUPPORTED_FILE_TYPE` | MIME not allowed | Pick a type from the returned `supportedTypes`. |
| 401 | Key revoked/expired | Stop. Report to operator. Never retry. |
| 403 `Missing scope: ...` | Key lacks `rundown:read`/`rundown:write` | Stop. Report the needed scope. |
| 403 `DRIVE_REQUIRED` | Org has no Drive storage connected | Stop uploads. Tell the operator to connect Drive. |
| 403 `RECONNECT_REQUIRED` | Org's Drive tokens dead | Stop uploads. Tell the operator to reconnect Drive. |
| 404 | Not found, other org, **or stale showId in the path** | Re-run `list_shows` + `get_rundown`; rebuild ids. Don't retry blindly. |
| 409 `STORY_CONFLICT` | A human changed the field you're writing | Re-read the story; merge; retry with current values. |
| 413 `FILE_TOO_LARGE` | Over the upload limit | Shrink or switch to REST (50MB). |
| 429 | Rate limited | Back off; slow batch edits. |
| 5xx / timeout | Ambiguous — write may have landed | Re-read the rundown first; retry only if absent. |
