openapi: 3.1.0
info:
  title: Rundown Studio API v1
  version: 1.0.0
  license:
    name: Proprietary — © Rundown Studio. All rights reserved.
    identifier: LicenseRef-Proprietary
  description: |-
    The public API for **Rundown Studio** — programmatic access to the
    rundowns that drive live shows and broadcast productions. It is built to
    be driven by scripts, automations, and AI agents as readily as by people:
    predictable JSON, `snake_case` everywhere, and errors that tell you how to
    fix them.

    ## Two surfaces

    - **Data plane** — REST/JSON CRUD over rundowns, cues, columns, cells,
      mentions, and text variables. This is where you build and edit a show.
    - **Rundown Control** — live show control: read `status`, `countdown`, and
      `output-message`, then drive the show with `action:start`, `action:pause`,
      `action:next`, and friends. Reads are `GET`; actions are `POST`, so a
      speculative request can never start a show by accident. The live reads
      (`status`/`countdown`/`events`) return the current snapshots and the SSE
      push stream directly with a `200`, and do zero per-poll database reads.
      Subscribe to `events` to *react* to changes instead of polling.

    ## Conventions

    - **Success envelope** — every success is `{ ok: true, message, data, meta }`.
      `meta.server_time` (epoch ms) rides on every response; trust it over the
      client clock for show timing. See `PingResponse` for the full shape.
    - **Errors** — RFC 9457 `application/problem+json`. The `detail` field says
      what went wrong *and* the corrective action; `code` is a stable,
      dot-namespaced identifier you can branch on.
    - **Naming & time** — `snake_case` on the wire. Timing fields are epoch
      milliseconds; audit fields (`created_at`, `updated_at`, …) are ISO 8601.
    - **Pagination** — cursor-based (`?cursor=&limit=`), `id`-ascending; large
      lists return `{ items, next_cursor, has_more }`.
    - **Stability** — the wire shape is a contract: fields are only ever added,
      never removed or renamed. Breaking changes ship as a new major version.

    ## Authentication

    Pass a team-scoped API token in the `Authorization: Bearer <token>` header.
    The control plane additionally accepts `?token=<token>` in the query string
    for production hardware (stream decks, cue triggers) that can set a URL but
    not a header. Call `GET /auth` to introspect a token's scope, permissions,
    and expiry.

    CORS is permissive on every endpoint (`Access-Control-Allow-Origin: *`, no
    credentials), so browser callers work cross-origin out of the box.

    ## Rate limits

    Limits are per team and depend on your plan. Plans **with** API access get
    higher limits (the `unrestricted` tier); every other plan gets the default
    `restricted` tier. Upgrade to lift the limits — nothing in the request
    changes. Any `429` echoes which tier applied to you in its `details.bucket`
    field (`restricted` or `unrestricted`).

    **Request rate** (`/status`, `/countdown`, and every CRUD endpoint):
    `restricted` allows 20 requests/min,
    `unrestricted` 120/min. `RateLimit-*`
    headers count down on every response; exceeding the rate returns `429`
    `rate_limit.exceeded` with an integer `Retry-After`.

    **Concurrent SSE connections** (`/events`): the long-lived stream is bounded
    by how many you hold open at once, not by request rate — `restricted` allows
    2 concurrent streams, `unrestricted` 6. Opening one more returns `429`
    `rate_limit.too_many_connections`; close an existing stream to free a slot.

    See the **[guides & getting started](https://rundownstudio.app/docs/api-v1/getting-started/)**
    for the conceptual overview, auth model, and worked examples.
externalDocs:
  description: Guides & getting started
  url: https://rundownstudio.app/docs/api-v1/getting-started/
servers:
  - url: https://api-v1.rundownstudio.app
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Misc
    description: Liveness, version, and other non-resource endpoints.
  - name: Auth
    description: Token introspection and pre-flight access checks.
  - name: Rundown Control
    description: Live show control. Reads are GET (`status`/`countdown`/`output-message`); runner actions are POST `action:<verb>` (start/pause/next/…, one verb per endpoint); the output-message is written with PATCH (`visible` carries show/hide). Every endpoint also accepts `?token=` query auth for production hardware that can't set a header.
  - name: Rundowns
    description: Rundown CRUD and export.
  - name: Cues
    description: Polymorphic cue CRUD — `cue` / `heading` / `group`.
  - name: Columns
    description: Column CRUD and reorder. v1 supports `text` columns only.
  - name: Cells
    description: Sparse, coordinate-addressed cells. v1 writes to `text` columns only.
  - name: Mentions
    description: Per-rundown mentions, addressed by `mention_id`.
  - name: Text Variables
    description: Per-rundown text variables, addressed by `key`.
paths:
  /ping:
    get:
      operationId: get_ping
      summary: Ping API
      description: |-
        Returns 200 with the current build version when the API is reachable.

        Use this to verify connectivity, deploy version, and that your token-less
        requests can reach the v1 surface. Auth is not required.

        Example:
        ```
        curl https://api-v1.rundownstudio.app/ping
        ```
      tags:
        - Misc
      x-mcp: false
      responses:
        "200":
          description: Service is alive.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    description: Always `true` on success.
                    example: true
                  message:
                    type: string
                    description: Human-readable status. For /ping this is always "pong".
                    example: pong
                  data:
                    type: object
                    properties:
                      version:
                        type: string
                        description: Build version of the API function. Useful for confirming which deploy is live.
                        example: 2.14.2
                    required:
                      - version
                    additionalProperties: false
                    description: Body of a /ping response.
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for GET /ping.
        "429": &a1
          description: Rate limit exceeded. Back off and retry after the seconds named in the `Retry-After` header; the `RateLimit-*` headers carry the limit, current usage, and reset.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem429"
      security: []
  /auth:
    get:
      operationId: get_auth
      summary: Introspect API token
      description: |-
        Returns the token's full picture: scope, team, rundown allow-list, role,
        and expiry. Callable by any role (even VIEW) — agents self-check before
        they write.

        Use this as the first call from any new integration to confirm what the
        token can do, and to discover the team / rundown ids it should pass to
        data-plane endpoints.

        Example:
        ```
        curl -H "Authorization: Bearer $TOKEN" https://api-v1.rundownstudio.app/auth
        ```
      tags:
        - Auth
      x-mcp: true
      responses:
        "200":
          description: Token introspection succeeded.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    description: Always `true` on success.
                    example: true
                  message:
                    type: string
                    description: Human-readable status.
                    example: OK
                  data:
                    type: object
                    properties:
                      token_id:
                        type: string
                        description: Stable token identifier (`jti`).
                        example: tok_abc123
                      scope:
                        type: string
                        enum:
                          - team
                          - rundown
                        description: Scope of the token. `team` covers every rundown owned by the team; `rundown` is pinned to one.
                        example: team
                      team_id:
                        type: string
                        description: Team that owns the token.
                        example: eeqyGE2qUuicAaIW8y0o
                      rundown_ids:
                        anyOf:
                          - type: array
                            items:
                              type: string
                          - type: string
                            const: "*"
                        description: '`"*"` for team-scoped tokens; a one-element array for rundown-scoped tokens.'
                        example: "*"
                      role:
                        type: string
                        enum:
                          - VIEW
                          - SHOWCALL
                          - EDIT
                          - OWNER
                        description: Public role waterfall. OWNER includes EDIT includes SHOWCALL includes VIEW.
                        example: EDIT
                      expires_at:
                        anyOf:
                          - type: string
                          - type: "null"
                        description: ISO-8601 UTC timestamp when the token expires. `null` if the token has no expiry.
                        example: 2026-06-01T00:00:00.000Z
                    required:
                      - token_id
                      - scope
                      - team_id
                      - rundown_ids
                      - role
                      - expires_at
                    additionalProperties: false
                    description: Full picture of the authenticated token.
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for GET /auth.
        "401":
          description: Token missing, malformed, expired, or revoked.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem401"
        "429": *a1
  /auth/probe:
    get:
      operationId: get_auth_probe
      summary: Check rundown access
      description: |-
        Returns `data.allowed: true|false` for the given `rundown_id`. The probe
        itself never fails — `ok: true` always; the answer is in `data.allowed`.

        For team-scoped tokens, "allowed" means the rundown exists and belongs
        to this team. For rundown-scoped tokens, "allowed" means `rundown_id`
        matches the token's pinned rundown.

        Useful as a pre-flight before a write — cheaper than attempting the
        mutation and parsing the 403.

        Example:
        ```
        curl -H "Authorization: Bearer $TOKEN" \
          "https://api-v1.rundownstudio.app/auth/probe?rundown_id=0WgMEKaosk6S8y4C2YsK"
        ```
      tags:
        - Auth
      x-mcp: true
      parameters:
        - name: rundown_id
          in: query
          required: true
          description: Rundown to probe.
          schema:
            type: string
            example: 0WgMEKaosk6S8y4C2YsK
      responses:
        "200":
          description: Probe completed (result is in `data.allowed`).
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    description: Always `true` on success.
                    example: true
                  message:
                    type: string
                    description: Human-readable status.
                    example: OK
                  data:
                    anyOf:
                      - type: object
                        properties:
                          allowed:
                            type: boolean
                            const: true
                            description: Token can act on this rundown.
                            example: true
                          rundown_id:
                            type: string
                            description: The rundown that was probed.
                            example: 0WgMEKaosk6S8y4C2YsK
                          role:
                            type: string
                            enum:
                              - VIEW
                              - SHOWCALL
                              - EDIT
                              - OWNER
                            description: Public role waterfall. OWNER includes EDIT includes SHOWCALL includes VIEW.
                            example: EDIT
                        required:
                          - allowed
                          - rundown_id
                          - role
                        additionalProperties: false
                        description: Probe result when the token has access to the rundown.
                      - type: object
                        properties:
                          allowed:
                            type: boolean
                            const: false
                            description: Token cannot act on this rundown.
                            example: false
                          rundown_id:
                            type: string
                            description: The rundown that was probed.
                            example: 0WgMEKaosk6S8y4C2YsK
                        required:
                          - allowed
                          - rundown_id
                        additionalProperties: false
                        description: Probe result when the token does not have access.
                    description: "Probe payload. `allowed: true` adds the effective role."
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for GET /auth/probe.
        "400":
          description: "`rundown_id` query parameter missing."
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem400"
        "401":
          description: Token missing, malformed, expired, or revoked.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem401"
        "429": *a1
  /rundowns/{id}/events:
    get:
      operationId: control_events
      summary: Subscribe to live events (SSE)
      description: |-
        Opens a **Server-Sent Events** stream of live rundown changes — a cue
        advances, the show pauses, a title is edited — so a consumer can react
        without polling. This is the push channel.

        Connect and the **200** documented below is the stream you get, served
        directly on this host. Pass `?token=` and `?events=` on the URL.

        The stream opens with a `: connected …` comment, then an `event: ready`
        hello, then the full `status` snapshot as the first data event — so a
        reconnect resyncs to current truth (no replay buffer or cursor to manage).
        Frame shapes:
        - `ready` — the hello, sent once on connect: `{disconnect_at,
          heartbeat_interval_ms}`. `disconnect_at` is the epoch-ms deadline at which
          this connection's forced cycle (see below) closes the stream — read it to
          reconnect slightly early for a gap-free handover.
        - `status` — the bare status snapshot, the same shape `/status` returns.
          Always present — the spine of the channel; it can't be disabled.
        - `rundown` / `cue` / `column` / `cell` — a change envelope, `change` ∈
          `added|modified|removed`. At `:fat` the full resource rides under its own
          key — `{change, id, rundown: {…}}` / `{change, id, cue: {…}}` / `{change,
          id, column: {…}}` / `{change, cue_id, column_id, cell: {…}}`, the same
          shape the matching `GET` returns — so you react without a follow-up read.
          At `:thin` it's the envelope alone (refetch on the id / coordinate). A
          `removed` event is always the bare envelope — a delete has no body even at
          `:fat`. `cell` is keyed by its `(cue_id, column_id)` coordinate (it has no
          public id); cells on heading/group cues and in private columns are never
          emitted. (`column`/`cell` are opt-in — add them to `?events=`.)
        - `heartbeat` — a keep-alive every 15s, `{at}` carrying the epoch-ms send
          time (matches `ready`'s `heartbeat_interval_ms`). Anchor an inactivity
          watchdog on it.
        - `disconnect` — sent just before the server closes the stream, naming why
          in `reason`: `lifetime` (the periodic forced cycle — just reconnect) or
          `auth_expired` / `auth_revoked` (your token stopped validating — refresh
          it, then reconnect). The server is closing the stream; what you do next is
          yours to decide from `reason` (a blind reconnect won't fix a bad token).

        Connect with the built-in `EventSource` (browsers, Node ≥22) or
        `curl -N -H "Authorization: Bearer <token>"`. Select resources with the
        `?events=` param. Auth is re-checked in-process while you stay connected, so
        a valid token holds the stream open; the server force-cycles each connection
        on a long lifetime (~12h) for hygiene, emitting `event: disconnect` first. On
        a `lifetime` close, reconnect — the first `status` frame brings you current
        again. Note: native `EventSource` does not auto-reconnect after a server
        close, so a robust client re-creates it when a `disconnect` event arrives.

        Requires a valid API token with VIEW access on the rundown.
      tags:
        - Rundown Control
      x-mcp: false
      parameters:
        - &a2
          name: id
          in: path
          required: true
          description: Rundown id.
          schema:
            type: string
            example: YOmm4UG2SGSomuOci44c
        - name: events
          in: query
          required: false
          description: "Comma-separated subscription selecting which **optional** resources to stream and at what fidelity: `resource` (fat — full payload) or `resource:thin` (the `{change, id}` envelope only). Configurable resources: `rundown`, `cue`, `column`, `cell`. A `status` frame is **always** streamed — it's the spine of the channel and can't be removed. `column` and `cell` are opt-in (absent from the default). `cell` is addressed by its `(cue_id, column_id)` coordinate rather than an `id`, but takes fidelity like the rest: bare `cell` (fat) pushes the full body, `cell:thin` the coordinate envelope only. The param **replaces** the optional set; there is no add/remove syntax. Omit it for the default `rundown:fat,cue:thin`. An unknown resource or fidelity is rejected with `400` before the stream opens."
          schema:
            type: string
            example: rundown:fat,cue:thin,column:fat,cell
        - &a3
          name: token
          in: query
          required: false
          description: "API token in the query string — an alternative to `Authorization: Bearer …` for production hardware that can set a URL but not always a header. Accepted on every control endpoint (reads and actions); ignored when the `Authorization` header is present. Rejected on the data-plane surfaces."
          schema:
            type: string
            example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
      responses:
        "200":
          description: An open SSE stream (`text/event-stream`). The response never completes until the server closes it on its lifetime cap or the client disconnects.
          headers:
            Content-Type:
              description: Always `text/event-stream`.
              schema:
                type: string
                example: text/event-stream
          content:
            text/event-stream:
              example: |
                : connected rundown=YOmm4UG2SGSomuOci44c events=rundown:fat,cue:thin,column:fat,cell:fat,status:fat

                event: ready
                data: {"disconnect_at":1747043200000,"heartbeat_interval_ms":15000}

                event: status
                data: {"server_time":1747000000000,"state":"running","active_cue":{"id":"Ii6WeSEQmEqyKY6YAu8c","title":"Opening monologue","started_at":1747000000000,"paused_at":null,"duration_ms":60000},"next_cue":{"id":"Ec0mmeOaGq68oiASGoSG","title":"Guest intro"}}

                event: heartbeat
                data: {"at":1747000005000}

                event: cue
                data: {"change":"modified","id":"Ii6WeSEQmEqyKY6YAu8c"}

                event: cell
                data: {"change":"modified","cue_id":"Ii6WeSEQmEqyKY6YAu8c","column_id":"EoeiemGEmeMOY8GwUK6s","cell":{"cue_id":"Ii6WeSEQmEqyKY6YAu8c","column_id":"EoeiemGEmeMOY8GwUK6s","content":"Mic check","content_html":"<p>Mic check</p>","created_at":"2024-05-11T18:00:00.000Z","updated_at":"2024-05-12T01:46:40.000Z"}}

                event: status
                data: {"server_time":1747000012000,"state":"running","active_cue":{"id":"Ec0mmeOaGq68oiASGoSG","title":"Guest intro","started_at":1747000012000,"paused_at":null,"duration_ms":120000},"next_cue":{"id":"ca064OOCWK2wGIQ6KOGq","title":"Closing"}}

                event: cue
                data: {"change":"added","id":"Az0neWCueOaGq68oiASGo"}

                event: column
                data: {"change":"modified","id":"EoeiemGEmeMOY8GwUK6s","column":{"id":"EoeiemGEmeMOY8GwUK6s","name":"AUDIO","created_at":"2024-05-11T18:00:00.000Z","updated_at":"2024-05-12T01:46:40.000Z"}}

                event: rundown
                data: {"change":"modified","id":"YOmm4UG2SGSomuOci44c","rundown":{"id":"YOmm4UG2SGSomuOci44c","title":"Morning Show","timezone":"America/New_York","start_time":1747000000000,"end_time":null,"status":"draft","settings":{"cue_numbering":{"start_from":1,"prefix":"","padding":1},"tod_display_format":null},"cue_order":[{"id":"Ii6WeSEQmEqyKY6YAu8c"},{"id":"Ec0mmeOaGq68oiASGoSG"},{"id":"Az0neWCueOaGq68oiASGo"}],"column_order":["Notes","Camera"],"created_at":"2024-05-11T18:00:00.000Z","updated_at":"2024-05-12T01:46:40.000Z"}}

                event: heartbeat
                data: {"at":1747000020000}

                event: cue
                data: {"change":"removed","id":"Bd0olDCueOaGq68oiASGo"}

                event: disconnect
                data: {"reason":"lifetime"}
              schema:
                type: string
        "400":
          description: Invalid `?events=` subscription — unknown resource or fidelity. The message lists the valid resources.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem400"
        "401": &a4
          description: Token missing, malformed, expired, or revoked.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem401"
        "404": &a5
          description: Rundown (or target cue, for `jump`) does not exist or is invisible to this token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem404"
        "429":
          description: Too many concurrent SSE streams open for your team (`rate_limit.too_many_connections`). Unlike the poll endpoints this stream is capped by concurrent connection count, not request rate — `restricted` allows 2 open streams, `unrestricted` 6 (`details.bucket` names yours). Close an existing stream before opening another.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem429"
      security:
        - bearerAuth: []
        - tokenQuery: []
  /rundowns/{id}/status:
    get:
      operationId: control_get_status
      summary: Read show status
      description: |-
        `GET /rundowns/{id}/status` returns the live status snapshot — the same
        shape every control verb returns as `data`. It does zero database work and
        is safe to poll at high frequency.

        Compute the active cue's remaining time locally from the snapshot:
        - running: `remaining = active_cue.started_at + active_cue.duration_ms - server_time`
        - paused:  `remaining = active_cue.started_at + active_cue.duration_ms - active_cue.paused_at`

        To **react** to changes instead of polling, subscribe to the SSE stream
        (`GET /rundowns/{id}/events`).

        Requires a valid API token with VIEW access on the rundown.
      tags:
        - Rundown Control
      x-mcp: true
      parameters:
        - *a2
        - *a3
      responses:
        "200":
          description: Live status snapshot. Safe to poll at high frequency.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: OK
                  data:
                    $ref: "#/components/schemas/ApiV1Status"
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Status snapshot envelope, served by the read-live surface. `data` is the show status; `meta.side_effects` is always `[]` (reads never emit events). Safe to poll at high frequency.
        "401": *a4
        "404": *a5
        "429": *a1
      security:
        - bearerAuth: []
        - tokenQuery: []
  /rundowns/{id}/countdown:
    get:
      operationId: control_get_countdown
      summary: Read show countdown
      description: |-
        `GET /rundowns/{id}/countdown` returns the live countdown snapshot — like
        `/status` it does zero database work and is safe to poll at high frequency.

        The countdown is the active cue's `remaining_ms` plus ceil-rounded
        `{days, hours, minutes, seconds}` components and a `formatted` clock-style
        string ready for display — pick it over `/status` when you want a display
        string straight from the server. Rounding ticks at integer-second
        boundaries (a 999ms remaining shows as "1s", not "0s"), the
        counterintuitive part most clients get wrong.

        Overtime is surfaced — `remaining_ms` goes negative, `is_overtime` flips
        to `true`, and `formatted` prepends `+`. Consumers can clamp at zero to
        hide it. When the show is `stopped` (pre-show or post-show), `active_cue`
        is `null`.

        To **track** state continuously, subscribe to the SSE stream
        (`GET /rundowns/{id}/events`) instead of polling.

        Requires a valid API token with VIEW access on the rundown.
      tags:
        - Rundown Control
      x-mcp: true
      parameters:
        - *a2
        - *a3
      responses:
        "200":
          description: Live countdown snapshot. Safe to poll at high frequency.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: OK
                  data:
                    type: object
                    properties:
                      state:
                        type: string
                        enum:
                          - running
                          - paused
                          - stopped
                        description: Same three-state machine as `/status`. `stopped` covers both pre-show and post-show.
                        example: running
                      server_time:
                        type: integer
                        minimum: -9007199254740991
                        maximum: 9007199254740991
                        description: Server wall-clock in epoch ms when the snapshot was assembled. Anchor for the running countdown.
                        example: 1747000000000
                      active_cue:
                        anyOf:
                          - type: object
                            properties:
                              cue_id:
                                type: string
                                example: Ii6WeSEQmEqyKY6YAu8c
                              title:
                                type: string
                                example: Opening monologue
                              duration_ms:
                                type: integer
                                minimum: -9007199254740991
                                maximum: 9007199254740991
                                description: Active cue's duration in milliseconds (mirrors the cue document).
                                example: 60000
                              remaining_ms:
                                type: integer
                                minimum: -9007199254740991
                                maximum: 9007199254740991
                                description: "Raw signed milliseconds remaining. Negative values are overtime — consumers can clamp at 0 if they want to hide it. Running: `started_at + duration_ms - server_time`; paused: `… - paused_at`."
                                example: 42000
                              remaining:
                                type: object
                                properties:
                                  is_overtime:
                                    type: boolean
                                    description: True when `remaining_ms < 0`. At exactly zero the countdown is NOT yet in overtime.
                                    example: false
                                  prefix:
                                    type: string
                                    description: Display prefix — empty when not overtime, `+` when overtime. The `formatted` string already includes this; surfaced separately so clients can swap in their own (e.g. `OVER `, `-`).
                                    example: ""
                                  days:
                                    type: integer
                                    minimum: 0
                                    maximum: 9007199254740991
                                    example: 0
                                  hours:
                                    type: integer
                                    minimum: 0
                                    maximum: 9007199254740991
                                    example: 0
                                  minutes:
                                    type: integer
                                    minimum: 0
                                    maximum: 9007199254740991
                                    example: 0
                                  seconds:
                                    type: integer
                                    minimum: 0
                                    maximum: 9007199254740991
                                    description: "Components are absolute values, ceil-rounded to the whole second (a 999ms countdown shows `seconds: 1`). Sub-second precision is dropped — read `remaining_ms` for it."
                                    example: 42
                                  formatted:
                                    type: string
                                    description: "Clock-style string: `mm:ss` < 1h, `h:mm:ss` < 1 day, `d:hh:mm:ss` ≥ 1 day. Overtime prepends `+` (the `prefix`)."
                                    example: 00:42
                                required:
                                  - is_overtime
                                  - prefix
                                  - days
                                  - hours
                                  - minutes
                                  - seconds
                                  - formatted
                                additionalProperties: false
                                description: Pre-formatted countdown components. Countdown rounding uses `Math.ceil` (so a 999ms remaining shows as "1s" not "0s"); durations would floor. Use this when you want a display string straight from the server.
                            required:
                              - cue_id
                              - title
                              - duration_ms
                              - remaining_ms
                              - remaining
                            additionalProperties: false
                            description: Active-cue countdown payload. Populated when state is `running` or `paused`; null when `stopped`.
                          - type: "null"
                    required:
                      - state
                      - server_time
                      - active_cue
                    additionalProperties: false
                    description: Public countdown snapshot — the server-computed cousin of `/status` for clients that don't want to do clock math.
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Countdown snapshot envelope, served by the read-live surface. `data` carries the active-cue countdown (or null when stopped); `meta.side_effects` is always `[]` (reads never emit events). Safe to poll at high frequency.
        "401": *a4
        "404": *a5
        "429": *a1
      security:
        - bearerAuth: []
        - tokenQuery: []
  /rundowns/{id}/action:start:
    post:
      operationId: control_start
      summary: Start show
      description: |-
        Starts the show on the first playable cue (when pre-show) or resumes
        the active cue (when paused). Idempotent when already running — returns
        200 with empty `side_effects`.

        Hard edge: a rundown that has run to completion (`state: stopped` with no
        remaining cues) returns 409 `runner.ended_cannot_start` — reset from the
        dashboard before starting again.
      tags:
        - Rundown Control
      x-mcp: true
      parameters:
        - *a2
        - *a3
      responses:
        "200":
          description: Action applied (or already in target state — see `meta.side_effects`).
          content:
            application/json:
              example:
                ok: true
                message: Show started
                data:
                  server_time: 1747000050000
                  state: running
                  active_cue: &a7
                    id: Ii6WeSEQmEqyKY6YAu8c
                    title: Opening monologue
                    started_at: 1747000000000
                    paused_at: null
                    duration_ms: 60000
                  next_cue: &a6
                    id: Ec0mmeOaGq68oiASGoSG
                    title: Guest intro
                meta:
                  request_id: req_8f4c2b6d9e1a7c3b5d8e2f01
                  server_time: 1747000050000
                  side_effects:
                    - type: runner.active_cue_changed
                      active_cue: &a13
                        id: Ii6WeSEQmEqyKY6YAu8c
                        title: Opening monologue
                    - type: runner.next_cue_changed
                      next_cue: *a6
                    - type: runner.timesnap
                      state: running
                      active_cue: *a7
                      next_cue: *a6
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: OK
                  data:
                    $ref: "#/components/schemas/ApiV1Status"
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for every control verb. `data` is the post-verb status snapshot. `meta.side_effects` is empty when the verb was a no-op (e.g. pausing an already-paused rundown).
        "401": *a4
        "403": &a9
          description: Token role insufficient — SHOWCALL or higher required.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem403"
        "404": *a5
        "409": &a10
          description: Action conflicts with current state — see `code` for the specific case (`runner.not_running`, `runner.ended_cannot_start`, `runner.no_next_cue`, `runner.no_previous_cue`).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem409"
        "429": *a1
      security:
        - bearerAuth: []
        - tokenQuery: []
  /rundowns/{id}/action:pause:
    post:
      operationId: control_pause
      summary: Pause show
      description: |-
        Freezes the countdown at the current `lastStop`. Idempotent when already
        paused — returns 200 with empty `side_effects`.

        Returns 409 `runner.not_running` when the rundown is `stopped`
        (pre-show or ended).
      tags:
        - Rundown Control
      x-mcp: true
      parameters:
        - *a2
        - *a3
      responses:
        "200":
          description: Action applied (or already in target state — see `meta.side_effects`).
          content:
            application/json:
              example:
                ok: true
                message: Show paused
                data:
                  server_time: 1747000050000
                  state: paused
                  active_cue: &a8
                    id: Ii6WeSEQmEqyKY6YAu8c
                    title: Opening monologue
                    started_at: 1747000000000
                    paused_at: 1747000045000
                    duration_ms: 60000
                  next_cue: *a6
                meta:
                  request_id: req_8f4c2b6d9e1a7c3b5d8e2f01
                  server_time: 1747000050000
                  side_effects:
                    - type: runner.timesnap
                      state: paused
                      active_cue: *a8
                      next_cue: *a6
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: OK
                  data:
                    $ref: "#/components/schemas/ApiV1Status"
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for every control verb. `data` is the post-verb status snapshot. `meta.side_effects` is empty when the verb was a no-op (e.g. pausing an already-paused rundown).
        "401": *a4
        "403": *a9
        "404": *a5
        "409": *a10
        "429": *a1
      security:
        - bearerAuth: []
        - tokenQuery: []
  /rundowns/{id}/action:next:
    post:
      operationId: control_next
      summary: Advance to next cue
      description: |-
        Advances to the pre-computed `next_cue`. NOT idempotent — each
        call moves forward by one. At the last cue, returns 409
        `runner.no_next_cue`. When the rundown is stopped, returns 409
        `runner.not_running`.
      tags:
        - Rundown Control
      x-mcp: true
      parameters:
        - *a2
        - *a3
      responses:
        "200":
          description: Action applied (or already in target state — see `meta.side_effects`).
          content:
            application/json:
              example:
                ok: true
                message: Advanced to next cue
                data:
                  server_time: 1747000050000
                  state: running
                  active_cue: &a12
                    id: Ec0mmeOaGq68oiASGoSG
                    title: Guest intro
                    started_at: 1747000050000
                    paused_at: null
                    duration_ms: 120000
                  next_cue: &a11
                    id: ca064OOCWK2wGIQ6KOGq
                    title: Closing
                meta:
                  request_id: req_8f4c2b6d9e1a7c3b5d8e2f01
                  server_time: 1747000050000
                  side_effects:
                    - type: runner.active_cue_changed
                      active_cue: *a6
                    - type: runner.next_cue_changed
                      next_cue: *a11
                    - type: runner.timesnap
                      state: running
                      active_cue: *a12
                      next_cue: *a11
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: OK
                  data:
                    $ref: "#/components/schemas/ApiV1Status"
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for every control verb. `data` is the post-verb status snapshot. `meta.side_effects` is empty when the verb was a no-op (e.g. pausing an already-paused rundown).
        "401": *a4
        "403": *a9
        "404": *a5
        "409": *a10
        "429": *a1
      security:
        - bearerAuth: []
        - tokenQuery: []
  /rundowns/{id}/action:previous:
    post:
      operationId: control_previous
      summary: Step back to previous cue
      description: |-
        Symmetric to `/next`. At the first cue, returns 409
        `runner.no_previous_cue`. When stopped, returns 409 `runner.not_running`.
      tags:
        - Rundown Control
      x-mcp: true
      parameters:
        - *a2
        - *a3
      responses:
        "200":
          description: Action applied (or already in target state — see `meta.side_effects`).
          content:
            application/json:
              example:
                ok: true
                message: Stepped back to previous cue
                data:
                  server_time: 1747000050000
                  state: running
                  active_cue: *a7
                  next_cue: *a6
                meta:
                  request_id: req_8f4c2b6d9e1a7c3b5d8e2f01
                  server_time: 1747000050000
                  side_effects:
                    - type: runner.active_cue_changed
                      active_cue: *a13
                    - type: runner.next_cue_changed
                      next_cue: *a6
                    - type: runner.timesnap
                      state: running
                      active_cue: *a7
                      next_cue: *a6
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: OK
                  data:
                    $ref: "#/components/schemas/ApiV1Status"
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for every control verb. `data` is the post-verb status snapshot. `meta.side_effects` is empty when the verb was a no-op (e.g. pausing an already-paused rundown).
        "401": *a4
        "403": *a9
        "404": *a5
        "409": *a10
        "429": *a1
      security:
        - bearerAuth: []
        - tokenQuery: []
  /rundowns/{id}/action:jump:
    post:
      operationId: control_jump
      summary: Jump to cue
      description: |-
        Sets the active cue to the `cue_id` supplied in the body. Idempotent when
        already on that cue — returns 200 with empty `side_effects`.

        When pre-show, this starts the show on that cue (no separate
        `action:start` call needed). The target must be `type: cue` — heading/group
        targets are rejected with 422 `cues.invalid_cue_type`.
      tags:
        - Rundown Control
      x-mcp: true
      parameters:
        - *a2
        - *a3
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                cue_id:
                  type: string
                  minLength: 1
                  description: "Target cue id. Must be a cue of `type: cue` (heading/group not allowed)."
                  example: Ii6WeSEQmEqyKY6YAu8c
              required:
                - cue_id
              description: POST body for `/rundowns/:id/action:jump`.
      responses:
        "200":
          description: Action applied (or already in target state — see `meta.side_effects`).
          content:
            application/json:
              example:
                ok: true
                message: Jumped to cue
                data:
                  server_time: 1747000050000
                  state: running
                  active_cue: *a12
                  next_cue: *a11
                meta:
                  request_id: req_8f4c2b6d9e1a7c3b5d8e2f01
                  server_time: 1747000050000
                  side_effects:
                    - type: runner.active_cue_changed
                      active_cue: *a6
                    - type: runner.next_cue_changed
                      next_cue: *a11
                    - type: runner.timesnap
                      state: running
                      active_cue: *a12
                      next_cue: *a11
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: OK
                  data:
                    $ref: "#/components/schemas/ApiV1Status"
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for every control verb. `data` is the post-verb status snapshot. `meta.side_effects` is empty when the verb was a no-op (e.g. pausing an already-paused rundown).
        "401": *a4
        "403": *a9
        "404": *a5
        "409": *a10
        "422":
          description: "`cues.invalid_cue_type` — target is a heading or group, which can't be jumped to."
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem422"
        "429": *a1
      security:
        - bearerAuth: []
        - tokenQuery: []
  /rundowns/{id}/action:adjust-duration:
    post:
      operationId: control_adjust_duration
      summary: Adjust active cue duration
      description: |-
        Writes the active cue's `duration` and re-syncs the countdown deadline.
        `delta_ms` may be positive or negative; `0` is a no-op (returns 200 with
        empty `side_effects`). Returns 409 `runner.not_running` when the rundown
        is stopped.
      tags:
        - Rundown Control
      x-mcp: true
      parameters:
        - *a2
        - *a3
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                delta_ms:
                  type: integer
                  minimum: -86400000
                  maximum: 86400000
                  description: Milliseconds to add to the active cue's duration. Positive lengthens, negative shortens. `0` is a no-op (200 with empty `side_effects`). The resulting duration is clamped to `[0, 86400000]` (24 h) — the same cap create/patch enforce; a fully clamped delta is also a no-op. Writes the cue's stored duration and re-syncs the countdown deadline.
                  example: 30000
              required:
                - delta_ms
              description: POST body for `/rundowns/:id/action:adjust-duration`.
      responses:
        "200":
          description: Action applied (or already in target state — see `meta.side_effects`).
          content:
            application/json:
              example:
                ok: true
                message: Adjusted active cue by 30000 ms
                data:
                  server_time: 1747000050000
                  state: running
                  active_cue: &a14
                    id: Ii6WeSEQmEqyKY6YAu8c
                    title: Opening monologue
                    started_at: 1747000000000
                    paused_at: null
                    duration_ms: 90000
                  next_cue: *a6
                meta:
                  request_id: req_8f4c2b6d9e1a7c3b5d8e2f01
                  server_time: 1747000050000
                  side_effects:
                    - type: runner.retimed
                      delta_ms: 30000
                    - type: runner.timesnap
                      state: running
                      active_cue: *a14
                      next_cue: *a6
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: OK
                  data:
                    $ref: "#/components/schemas/ApiV1Status"
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for every control verb. `data` is the post-verb status snapshot. `meta.side_effects` is empty when the verb was a no-op (e.g. pausing an already-paused rundown).
        "401": *a4
        "403": *a9
        "404": *a5
        "409": *a10
        "429": *a1
      security:
        - bearerAuth: []
        - tokenQuery: []
  /rundowns/{id}/output-message:
    get:
      operationId: control_get_output_message
      summary: Read output message
      description: |-
        Reads the current stage-display output message — the five fields
        `text`/`visible`/`color`/`bold`/`underline`. The live message is
        intentionally absent from `GET /rundowns/:id`, so this is its read path.

        Writes go through `PATCH /rundowns/:id/output-message`.

        Requires SHOWCALL access — output-message is fully show-caller-owned.
      tags:
        - Rundown Control
      x-mcp: true
      parameters:
        - *a2
        - *a3
      responses:
        "200":
          description: Current output-message state.
          content:
            application/json:
              example:
                ok: true
                message: OK
                data: &a15
                  text: STAND BY
                  visible: false
                  color: "#ffffff"
                  bold: false
                  underline: false
                meta:
                  request_id: req_8f4c2b6d9e1a7c3b5d8e2f01
                  server_time: 1747000050000
                  side_effects:
                    - type: output_message.changed
                      message: *a15
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: OK
                  data:
                    $ref: "#/components/schemas/ApiV1OutputMessage"
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Output-message envelope. `data` is the full post-write state. `meta.side_effects` carries `output_message.changed` on writes that actually changed a field; reads and no-op writes carry `[]`.
        "401": *a4
        "403": *a9
        "404": *a5
        "429": *a1
      security:
        - bearerAuth: []
        - tokenQuery: []
    patch:
      operationId: control_patch_output_message
      summary: Update output message
      description: |-
        Writes any subset of the five output-message fields. Only fields
        present in the body are touched; omitted fields remain at their
        current values. `visible: true|false` shows/hides the message — there
        are no separate show/hide/toggle endpoints; flip it with a
        GET-then-PATCH.

        An empty body is rejected with 422 `messages.empty_patch`. A body whose
        values all match the current state is a no-op — 200 with
        `side_effects: []`.

        Requires SHOWCALL access.
      tags:
        - Rundown Control
      x-mcp: true
      parameters:
        - *a2
        - *a3
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                text:
                  type: string
                  maxLength: 1000
                visible:
                  type: boolean
                color:
                  type: string
                  maxLength: 50
                bold:
                  type: boolean
                underline:
                  type: boolean
              description: "PATCH body for `/rundowns/:id/output-message`. Any subset of the five fields; only provided fields are written, the rest stay put. `visible: true|false` shows/hides the message. An empty body is rejected with 422 `messages.empty_patch`."
      responses:
        "200":
          description: Post-write output-message state (full envelope).
          content:
            application/json:
              example:
                ok: true
                message: Output message updated
                data: &a16
                  text: STAND BY
                  visible: true
                  color: "#ffffff"
                  bold: false
                  underline: false
                meta:
                  request_id: req_8f4c2b6d9e1a7c3b5d8e2f01
                  server_time: 1747000050000
                  side_effects:
                    - type: output_message.changed
                      message: *a16
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: OK
                  data:
                    $ref: "#/components/schemas/ApiV1OutputMessage"
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Output-message envelope. `data` is the full post-write state. `meta.side_effects` carries `output_message.changed` on writes that actually changed a field; reads and no-op writes carry `[]`.
        "400":
          description: Malformed body — e.g. a field of the wrong type (`visible` not a boolean).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem400"
        "401": *a4
        "403": *a9
        "404": *a5
        "422":
          description: "`messages.empty_patch` — body is empty (no field to write)."
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem422"
        "429": *a1
      security:
        - bearerAuth: []
        - tokenQuery: []
  /rundowns:
    get:
      operationId: list_rundowns
      summary: List rundowns
      description: |-
        Returns rundowns the token is authorized for, ordered by `id` ascending.
        Cursor-based pagination — pass the previous response's `next_cursor` back
        via `?cursor=`. Default limit 25, max 100.

        Rundown-scoped tokens see a single rundown; team-scoped tokens see every
        rundown the team owns. Soft-deleted rundowns are filtered out.

        Example:
        ```
        curl -H "Authorization: Bearer $TOKEN" \
          "https://api-v1.rundownstudio.app/rundowns?limit=50"
        ```
      tags:
        - Rundowns
      x-mcp: true
      parameters:
        - name: cursor
          in: query
          required: false
          description: Opaque pagination cursor from the previous response.
          schema:
            type: string
            example: eyJ2IjoxLCJhZnRlcl9pZCI6InJuZF94In0
        - name: limit
          in: query
          required: false
          description: Items per page (default 25, max 100).
          schema:
            type: integer
            minimum: 1
            maximum: 100
            example: 25
      responses:
        "200":
          description: Page of rundowns.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: Returned 12 rundowns
                  data:
                    type: object
                    properties:
                      items:
                        type: array
                        items:
                          type: object
                          properties:
                            id:
                              type: string
                              example: YOmm4UG2SGSomuOci44c
                            title:
                              type: string
                              example: Brave Show — June 14
                            timezone:
                              anyOf:
                                - type: string
                                - type: "null"
                              example: America/New_York
                            start_time:
                              anyOf:
                                - type: integer
                                  minimum: -9007199254740991
                                  maximum: 9007199254740991
                                - type: "null"
                              description: Wall-clock start, epoch milliseconds. Pair with `timezone` to render.
                              example: 1750024800000
                            end_time:
                              anyOf:
                                - type: integer
                                  minimum: -9007199254740991
                                  maximum: 9007199254740991
                                - type: "null"
                              description: Wall-clock end, epoch milliseconds. May be null for open-ended rundowns.
                              example: 1750028400000
                            status:
                              type: string
                              enum:
                                - imported
                                - draft
                                - awaiting-data
                                - approved
                                - finalized
                                - rejected
                              description: Lifecycle state of the rundown. `draft` and `approved` are the live-show happy path.
                              example: draft
                            created_at:
                              anyOf:
                                - type: string
                                - type: "null"
                              example: 2026-05-01T10:00:00.000Z
                            updated_at:
                              anyOf:
                                - type: string
                                - type: "null"
                              example: 2026-05-17T14:30:00.000Z
                          required:
                            - id
                            - title
                            - timezone
                            - start_time
                            - end_time
                            - status
                            - created_at
                            - updated_at
                          additionalProperties: false
                          description: Trimmed rundown shape on the list endpoint. Fetch the full shallow shape via GET /rundowns/:id when needed.
                      next_cursor:
                        anyOf:
                          - type: string
                          - type: "null"
                        description: Opaque cursor for the next page; `null` when `has_more` is `false`.
                        example: null
                      has_more:
                        type: boolean
                        example: false
                    required:
                      - items
                      - next_cursor
                      - has_more
                    additionalProperties: false
                    description: List payload — pagination metadata lives inside `data`.
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: "Success envelope for GET /rundowns. Order: `id` ascending. Default limit 25, max 100."
        "400":
          description: Cursor or limit value is invalid.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem400"
        "401":
          description: Token missing, malformed, expired, or revoked.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem401"
        "429": *a1
    post:
      operationId: create_rundown
      summary: Create one rundown
      description: |-
        One endpoint, two modes: send `{ "title": "..." }` for an empty rundown,
        or send a fully nested document with `columns`, `cues`, and inline `cells`.
        No CSV input — transform CSV → JSON in the caller (an LLM is good at this;
        see the worked example in the docs).

        Rules:
        - Cells reference columns by `name`, not id (IDs don't exist yet at import).
        - Column names must be unique within `columns[]`.
        - Only `type: "cue"` may carry cells; heading/group are rejected.
        - Groups may contain cues + headings as `children`; nested groups are not supported.
        - Best-effort atomic: a validation failure rolls back the whole rundown.

        Errors point at the offending field via `details.path` (e.g. `cues[7].cells.AUDIO`)
        and `details.reason`. Fix one, retry.

        Example (empty rundown):
        ```
        curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
          -d '{"title":"Brave Show — June 14"}' \
          "https://api-v1.rundownstudio.app/rundowns"
        ```
      tags:
        - Rundowns
      x-mcp: true
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                  minLength: 1
                  maxLength: 200
                  example: Brave Show — June 14
                timezone:
                  type: string
                  minLength: 1
                  maxLength: 100
                  description: IANA timezone name. Validated against the runtime timezone database; an unknown zone is rejected.
                  example: America/New_York
                start_time:
                  description: Wall-clock start in epoch milliseconds. Defaults to "now" rounded if omitted.
                  example: 1750024800000
                  type: integer
                  minimum: -9007199254740991
                  maximum: 9007199254740991
                status:
                  type: string
                  enum:
                    - imported
                    - draft
                    - awaiting-data
                    - approved
                    - finalized
                    - rejected
                  description: Lifecycle state of the rundown. `draft` and `approved` are the live-show happy path.
                  example: draft
                settings:
                  description: Optional rundown settings. Anything omitted falls back to defaults.
                  type: object
                  properties:
                    cue_numbering:
                      description: Cue-numbering config. Any sub-key omitted is left unchanged.
                      type: object
                      properties:
                        start_from:
                          type: integer
                          minimum: 0
                          maximum: 9007199254740991
                        prefix:
                          type: string
                          maxLength: 20
                        padding:
                          type: integer
                          minimum: 1
                          maximum: 5
                    tod_display_format:
                      anyOf:
                        - type: string
                          enum:
                            - 12hNoAmPm
                            - 12h
                            - 24h
                        - type: "null"
                columns:
                  description: Column declarations. Names must be unique within the array. Omit for an empty rundown.
                  maxItems: 50
                  type: array
                  items:
                    type: object
                    properties:
                      name:
                        type: string
                        minLength: 1
                        maxLength: 200
                        description: Display name; also the key cells use to reference this column in `cues[].cells`.
                        example: AUDIO
                      type:
                        description: Only `text` columns are supported. Field is accepted but the default is fine.
                        example: text
                        type: string
                        const: text
                    required:
                      - name
                    description: Column declaration for `POST /rundowns`. Names must be unique within the request.
                cues:
                  description: Cues in insertion order. Children of groups count toward the per-rundown cue cap.
                  maxItems: 500
                  type: array
                  items:
                    oneOf:
                      - type: object
                        properties:
                          type:
                            type: string
                            const: cue
                          title:
                            type: string
                            maxLength: 500
                            example: Opening
                          subtitle:
                            example: Host walks on stage
                            type: string
                            maxLength: 500
                          duration_ms:
                            description: Duration in milliseconds. Defaults to 0 if omitted; ignored on headings/groups.
                            example: 60000
                            type: integer
                            minimum: 0
                            maximum: 86400000
                          background_color:
                            example: "#3b0764"
                            type: string
                            maxLength: 20
                          start_time:
                            description: Fixed wall-clock start time in epoch milliseconds. If set, cue is treated as hard-start (FIXED).
                            example: 1750024800000
                            anyOf:
                              - type: integer
                                minimum: -9007199254740991
                                maximum: 9007199254740991
                              - type: "null"
                          cells:
                            type: object
                            propertyNames:
                              type: string
                              minLength: 1
                              maxLength: 200
                            additionalProperties:
                              type: string
                              maxLength: 10000
                            description: Map of column-name → string value. Keys reference `columns[].name` (not id). Omit empty cells.
                            example:
                              PRESET: P1
                              AUDIO: track-01
                        required:
                          - type
                          - title
                        description: "A regular cue. Only cues of `type: cue` may carry `cells`."
                      - type: object
                        properties:
                          type:
                            type: string
                            const: heading
                          title:
                            type: string
                            maxLength: 500
                            example: Opening
                          subtitle:
                            example: Host walks on stage
                            type: string
                            maxLength: 500
                          duration_ms:
                            description: Duration in milliseconds. Defaults to 0 if omitted; ignored on headings/groups.
                            example: 60000
                            type: integer
                            minimum: 0
                            maximum: 86400000
                          background_color:
                            example: "#3b0764"
                            type: string
                            maxLength: 20
                          start_time:
                            description: Fixed wall-clock start time in epoch milliseconds. If set, cue is treated as hard-start (FIXED).
                            example: 1750024800000
                            anyOf:
                              - type: integer
                                minimum: -9007199254740991
                                maximum: 9007199254740991
                              - type: "null"
                        required:
                          - type
                          - title
                        description: A heading row — no cells, no duration semantics. Renders as a section label in the rundown.
                      - type: object
                        properties:
                          type:
                            type: string
                            const: group
                          title:
                            type: string
                            maxLength: 500
                            example: Opening
                          subtitle:
                            example: Host walks on stage
                            type: string
                            maxLength: 500
                          duration_ms:
                            description: Duration in milliseconds. Defaults to 0 if omitted; ignored on headings/groups.
                            example: 60000
                            type: integer
                            minimum: 0
                            maximum: 86400000
                          background_color:
                            example: "#3b0764"
                            type: string
                            maxLength: 20
                          start_time:
                            description: Fixed wall-clock start time in epoch milliseconds. If set, cue is treated as hard-start (FIXED).
                            example: 1750024800000
                            anyOf:
                              - type: integer
                                minimum: -9007199254740991
                                maximum: 9007199254740991
                              - type: "null"
                          children:
                            description: Child cues/headings rendered inside the group. Nested groups are not supported.
                            maxItems: 500
                            type: array
                            items:
                              oneOf:
                                - type: object
                                  properties:
                                    type:
                                      type: string
                                      const: cue
                                    title:
                                      type: string
                                      maxLength: 500
                                      example: Opening
                                    subtitle:
                                      example: Host walks on stage
                                      type: string
                                      maxLength: 500
                                    duration_ms:
                                      description: Duration in milliseconds. Defaults to 0 if omitted; ignored on headings/groups.
                                      example: 60000
                                      type: integer
                                      minimum: 0
                                      maximum: 86400000
                                    background_color:
                                      example: "#3b0764"
                                      type: string
                                      maxLength: 20
                                    start_time:
                                      description: Fixed wall-clock start time in epoch milliseconds. If set, cue is treated as hard-start (FIXED).
                                      example: 1750024800000
                                      anyOf:
                                        - type: integer
                                          minimum: -9007199254740991
                                          maximum: 9007199254740991
                                        - type: "null"
                                    cells:
                                      type: object
                                      propertyNames:
                                        type: string
                                        minLength: 1
                                        maxLength: 200
                                      additionalProperties:
                                        type: string
                                        maxLength: 10000
                                      description: Map of column-name → string value. Keys reference `columns[].name` (not id). Omit empty cells.
                                      example:
                                        PRESET: P1
                                        AUDIO: track-01
                                  required:
                                    - type
                                    - title
                                  description: "A regular cue. Only cues of `type: cue` may carry `cells`."
                                - type: object
                                  properties:
                                    type:
                                      type: string
                                      const: heading
                                    title:
                                      type: string
                                      maxLength: 500
                                      example: Opening
                                    subtitle:
                                      example: Host walks on stage
                                      type: string
                                      maxLength: 500
                                    duration_ms:
                                      description: Duration in milliseconds. Defaults to 0 if omitted; ignored on headings/groups.
                                      example: 60000
                                      type: integer
                                      minimum: 0
                                      maximum: 86400000
                                    background_color:
                                      example: "#3b0764"
                                      type: string
                                      maxLength: 20
                                    start_time:
                                      description: Fixed wall-clock start time in epoch milliseconds. If set, cue is treated as hard-start (FIXED).
                                      example: 1750024800000
                                      anyOf:
                                        - type: integer
                                          minimum: -9007199254740991
                                          maximum: 9007199254740991
                                        - type: "null"
                                  required:
                                    - type
                                    - title
                                  description: A heading row — no cells, no duration semantics. Renders as a section label in the rundown.
                              type: object
                        required:
                          - type
                          - title
                        description: A group cue. `children` may contain cues and headings; nested groups are rejected.
                    description: Polymorphic cue input — `type` picks `cue`, `heading`, or `group`.
                    type: object
              required:
                - title
              description: 'Body for `POST /rundowns`. Minimum: `{ "title": "..." }`. Maximum: a full nested rundown with columns + cues + cells. Cells reference columns by `name`.'
      responses:
        "201":
          description: Rundown created. Body is the full nested rundown with assigned IDs.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: Rundown created
                  data:
                    $ref: "#/components/schemas/Rundown"
                  included:
                    type: object
                    properties:
                      cues:
                        type: array
                        items:
                          $ref: "#/components/schemas/Cue"
                      columns:
                        type: array
                        items:
                          $ref: "#/components/schemas/Column"
                      cells:
                        type: array
                        items:
                          $ref: "#/components/schemas/Cell"
                    required:
                      - cues
                      - columns
                      - cells
                    additionalProperties: false
                    description: The newly created rundown's cues, columns, and cells — every entity carries its server-assigned id. Always populated on create (unlike GET, where `included` is gated on `?include=`).
                  meta:
                    $ref: "#/components/schemas/RundownResponseMeta"
                required:
                  - ok
                  - message
                  - data
                  - included
                  - meta
                additionalProperties: false
                description: Success envelope for POST /rundowns. Status code is 201. `data` is the shallow rundown; `included` (a sibling of `data`) mirrors `GET /rundowns/:id?include=cues,columns,cells` but is always populated.
        "400":
          description: Body failed validation. `details.issues[]` carries one entry per offending field with `path`, `message`, and `code`.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem400"
        "401":
          description: Token missing, malformed, expired, or revoked.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem401"
        "403":
          description: Token role/scope insufficient (team-scope EDIT required).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem403"
        "422":
          description: Semantic rejection (e.g. duplicate column name, cells on heading/group).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem422"
        "429": *a1
  /rundowns/{id}:
    get:
      operationId: get_rundown
      summary: Fetch one rundown
      description: |-
        Default response is shallow: `data` holds top-level metadata plus the
        cue-order tree as IDs only. Pass `?include=cues,columns,cells` for nested
        content, which arrives in `included` (a sibling of `data`).

        Rules for `?include=`:
        - Comma-separated subset of `cues`, `columns`, `cells`.
        - `cells` implies `cues` and `columns` (auto-added; reflected in `meta.applied_includes`).
        - Sideloaded resources land in the `included` sibling; `included` is omitted entirely when nothing is requested.
        - Unknown values → 400 `bad_request` with `details.unknown_includes`.
        - 1,000-cell soft cap → 413 `payload.too_large` with a hint to use list endpoints.

        Example:
        ```
        curl -H "Authorization: Bearer $TOKEN" \
          "https://api-v1.rundownstudio.app/rundowns/YOmm4UG2SGSomuOci44c?include=cues,columns"
        ```
      tags:
        - Rundowns
      x-mcp: true
      parameters:
        - name: id
          in: path
          required: true
          description: Rundown id.
          schema:
            type: string
            example: YOmm4UG2SGSomuOci44c
        - name: include
          in: query
          required: false
          description: "Comma-separated list of nested resources to include: `cues`, `columns`, `cells`."
          schema:
            type: string
            example: cues,columns
      responses:
        "200":
          description: Rundown payload (shallow or with includes).
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: OK
                  data:
                    $ref: "#/components/schemas/Rundown"
                  included:
                    type: object
                    properties:
                      cues:
                        description: Full cue objects. Present when `cues` is requested via `?include=`.
                        type: array
                        items:
                          $ref: "#/components/schemas/Cue"
                      columns:
                        description: Full column objects. Present when `columns` is requested via `?include=`.
                        type: array
                        items:
                          $ref: "#/components/schemas/Column"
                      cells:
                        description: Cell objects. Present when `cells` is requested via `?include=` (implies `cues` and `columns`).
                        type: array
                        items:
                          $ref: "#/components/schemas/Cell"
                    additionalProperties: false
                    description: Sideloaded resources requested via `?include=`, a sibling of `data` (compound-document pattern). Omitted entirely when no includes are requested; the applied-include manifest is in `meta.applied_includes`.
                  meta:
                    $ref: "#/components/schemas/RundownResponseMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for GET /rundowns/:id. `data` is the rundown alone; `included` is a sibling present only when `?include=` requested resources; `meta.applied_includes` always reports the applied set.
              examples:
                shallow:
                  summary: Default — no `?include=`
                  value:
                    ok: true
                    message: OK
                    data: &a17
                      id: YOmm4UG2SGSomuOci44c
                      title: Brave Show — June 14
                      timezone: America/New_York
                      start_time: 1750024800000
                      end_time: 1750028400000
                      status: draft
                      settings:
                        cue_numbering:
                          start_from: 1
                          prefix: A
                          padding: 2
                        tod_display_format: 24h
                      cue_order:
                        - id: Ii6WeSEQmEqyKY6YAu8c
                        - id: G0M48GugUIem6YOSIOyO
                          children:
                            - id: UEieqCc0q4wKsME0QSSm
                      column_order:
                        - EoeiemGEmeMOY8GwUK6s
                        - Sw0kqCqSuM8gKeICwsIa
                      created_at: 2026-05-01T10:00:00.000Z
                      updated_at: 2026-05-17T14:30:00.000Z
                    meta:
                      request_id: req_8f4c2b6d9e1a7c3b5d8e2f01
                      server_time: 1747000000000
                      side_effects: []
                      applied_includes: []
                with_includes:
                  summary: With `?include=cues,columns`
                  value:
                    ok: true
                    message: OK
                    data: *a17
                    included:
                      cues:
                        - id: Ii6WeSEQmEqyKY6YAu8c
                          type: cue
                          title: Opening monologue
                          subtitle: Host walks on stage
                          duration_ms: 60000
                          background_color: "#3b0764"
                          prevent_edits: false
                          skipped_by: null
                          start_time: 1750024800000
                          created_at: 2026-05-01T10:00:00.000Z
                          updated_at: 2026-05-17T14:30:00.000Z
                      columns:
                        - id: EoeiemGEmeMOY8GwUK6s
                          name: AUDIO
                          created_at: 2026-05-01T10:00:00.000Z
                          updated_at: 2026-05-12T11:20:00.000Z
                    meta:
                      request_id: req_8f4c2b6d9e1a7c3b5d8e2f01
                      server_time: 1747000000000
                      side_effects: []
                      applied_includes:
                        - cues
                        - columns
        "400":
          description: Unknown `include` value.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem400"
        "401":
          description: Token missing, malformed, expired, or revoked.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem401"
        "403":
          description: Token cannot access this rundown.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem403"
        "404":
          description: Rundown does not exist or has been deleted.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem404"
        "413":
          description: Included content exceeds the 1,000-cell soft cap.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem413"
        "429": *a1
    patch:
      operationId: update_rundown
      summary: Update one rundown
      description: |-
        Updates `title`, `timezone`, `start_time`, `status`, and partial `settings`.
        Omitted fields are left alone; `settings` keys are merged (not replaced).
        An empty body is rejected with 422 `rundowns.empty_patch`.

        Returns the rundown's new shallow state. For nested edits use the dedicated
        cue / column / cell endpoints.
      tags:
        - Rundowns
      x-mcp: true
      parameters:
        - name: id
          in: path
          required: true
          description: Rundown id.
          schema:
            type: string
            example: YOmm4UG2SGSomuOci44c
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  example: Brave Show — June 15
                  type: string
                  minLength: 1
                  maxLength: 200
                timezone:
                  type: string
                  minLength: 1
                  maxLength: 100
                  description: IANA timezone name. Validated against the runtime timezone database; an unknown zone is rejected.
                  example: America/New_York
                start_time:
                  description: New wall-clock start in epoch milliseconds. Combine with `timezone` to set a date in a specific zone.
                  example: 1750024800000
                  type: integer
                  minimum: -9007199254740991
                  maximum: 9007199254740991
                status:
                  type: string
                  enum:
                    - imported
                    - draft
                    - awaiting-data
                    - approved
                    - finalized
                    - rejected
                  description: Lifecycle state of the rundown. `draft` and `approved` are the live-show happy path.
                  example: draft
                settings:
                  description: Partial settings merge. Provided keys are written; omitted keys are left alone.
                  type: object
                  properties:
                    cue_numbering:
                      description: Cue-numbering config. Any sub-key omitted is left unchanged.
                      type: object
                      properties:
                        start_from:
                          type: integer
                          minimum: 0
                          maximum: 9007199254740991
                        prefix:
                          type: string
                          maxLength: 20
                        padding:
                          type: integer
                          minimum: 1
                          maximum: 5
                    tod_display_format:
                      anyOf:
                        - type: string
                          enum:
                            - 12hNoAmPm
                            - 12h
                            - 24h
                        - type: "null"
              description: Top-level rundown update — all fields optional. Empty body is rejected.
      responses:
        "200":
          description: Rundown updated.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: Rundown updated
                  data:
                    $ref: "#/components/schemas/Rundown"
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for PATCH /rundowns/:id. Body is the rundown's new shallow state.
        "400":
          description: Validation error in the body.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem400"
        "401":
          description: Token missing, malformed, expired, or revoked.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem401"
        "403":
          description: Token role insufficient (EDIT required).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem403"
        "404":
          description: Rundown does not exist or has been deleted.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem404"
        "422":
          description: Body is empty or has no recognized fields.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem422"
        "429": *a1
    delete:
      operationId: delete_rundown
      summary: Soft-delete one rundown
      description: |-
        Soft-delete: the rundown is hidden from all reads immediately, retained
        for 30 days, then a daily cleanup purges it permanently. Recover within
        that window from the dashboard — v1 has no restore endpoint.
        Requires team-scope EDIT.

        Calling this on a rundown that's already deleted returns 404.
      tags:
        - Rundowns
      x-mcp: true
      parameters:
        - name: id
          in: path
          required: true
          description: Rundown id.
          schema:
            type: string
            example: YOmm4UG2SGSomuOci44c
      responses:
        "200":
          description: Rundown soft-deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: Rundown deleted
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                        example: YOmm4UG2SGSomuOci44c
                      deleted_at:
                        type: string
                        example: 2026-05-18T12:00:00.000Z
                    required:
                      - id
                      - deleted_at
                    additionalProperties: false
                    description: Confirmation payload — the rundown id that was soft-deleted and the timestamp.
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: "Success envelope for DELETE /rundowns/:id. Soft-delete: a future cleanup pass removes the documents."
        "401":
          description: Token missing, malformed, expired, or revoked.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem401"
        "403":
          description: Token role insufficient (team-scope EDIT required).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem403"
        "404":
          description: Rundown does not exist or has already been deleted.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem404"
        "429": *a1
  /rundowns/{id}/export:
    get:
      operationId: export_rundown
      summary: Export one rundown as CSV or PDF
      description: |-
        CSV: returns the file inline as `text/csv` with a `Content-Disposition`
        header — pipe straight to disk.

        PDF: returns a JSON envelope with `download_url`. The PDF is generated by
        an external service; the server doesn't hold the bytes synchronously, so
        we hand back the URL instead. Follow the URL within a couple of minutes.

        Need all the rundown data in one JSON call? Use
        `GET /rundowns/:id?include=cues,columns,cells`.

        Example:
        ```
        curl -H "Authorization: Bearer $TOKEN" -L -O \
          "https://api-v1.rundownstudio.app/rundowns/YOmm4UG2SGSomuOci44c/export?format=csv"
        ```
      tags:
        - Rundowns
      x-mcp: false
      parameters:
        - name: id
          in: path
          required: true
          description: Rundown id.
          schema:
            type: string
            example: YOmm4UG2SGSomuOci44c
        - name: format
          in: query
          required: true
          description: Output format. `csv` returns text/csv inline; `pdf` returns a JSON envelope with a `download_url`.
          schema:
            type: string
            enum:
              - csv
              - pdf
            example: csv
      responses:
        "200":
          description: Exported file or download descriptor (per `format`).
          content:
            text/csv:
              schema:
                type: string
                example: |
                  Cue,Start Time,End Time,Duration,Title
                  1,10:00,10:01,00:01:00,Opening
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: PDF ready
                  data:
                    type: object
                    properties:
                      format:
                        type: string
                        const: pdf
                        example: pdf
                      download_url:
                        type: string
                        description: Absolute URL of the generated PDF. URLs are short-lived (issued by the external PDF service) — follow within a few minutes.
                        example: https://app.rundownstudio.app/download-pdf/abc123/Brave-Show.pdf
                      filename:
                        type: string
                        example: Brave Show Rundown Studio-20260518-1430.pdf
                    required:
                      - format
                      - download_url
                      - filename
                    additionalProperties: false
                    description: PDF export descriptor — the bytes live behind `download_url`, served by the dashboard's proxy route.
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for PDF exports. PDF generation is asynchronous server-side; the body carries the download URL instead of the bytes.
        "400":
          description: Unknown `format` value.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem400"
        "401":
          description: Token missing, malformed, expired, or revoked.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem401"
        "403":
          description: Token cannot access this rundown.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem403"
        "404":
          description: Rundown does not exist or has been deleted.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem404"
        "429": *a1
  /rundowns/{id}/cues:
    get:
      operationId: list_cues
      summary: List cues
      description: |-
        Returns cues for the rundown, ordered by `id` ascending. Cursor-based
        pagination — pass the previous response's `next_cursor` back via
        `?cursor=`. Default limit 25, max 100.

        **Order note:** list order is `id` ascending, not show order. The
        rundown's hierarchical cue-order tree lives on `GET /rundowns/:id`
        (shallow, IDs only). Walk that tree client-side, then look up cues by
        id from this endpoint (or fetch all at once via `GET /rundowns/:id?include=cues`).

        Example:
        ```
        curl -H "Authorization: Bearer $TOKEN" \
          "https://api-v1.rundownstudio.app/rundowns/YOmm4UG2SGSomuOci44c/cues?limit=50"
        ```
      tags:
        - Cues
      x-mcp: true
      parameters:
        - name: id
          in: path
          required: true
          description: Rundown id.
          schema:
            type: string
            example: YOmm4UG2SGSomuOci44c
        - name: cursor
          in: query
          required: false
          description: Opaque pagination cursor from the previous response.
          schema:
            type: string
            example: eyJ2IjoxLCJhZnRlcl9pZCI6ImN1ZV94In0
        - name: limit
          in: query
          required: false
          description: Items per page (default 25, max 100).
          schema:
            type: integer
            minimum: 1
            maximum: 100
            example: 25
      responses:
        "200":
          description: Page of cues.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: Returned 25 cues
                  data:
                    type: object
                    properties:
                      items:
                        type: array
                        items:
                          $ref: "#/components/schemas/Cue"
                      next_cursor:
                        anyOf:
                          - type: string
                          - type: "null"
                        description: Opaque cursor for the next page; `null` when `has_more` is `false`.
                        example: null
                      has_more:
                        type: boolean
                        example: false
                    required:
                      - items
                      - next_cursor
                      - has_more
                    additionalProperties: false
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: "Success envelope for GET /rundowns/:id/cues. Order: `id` ascending — for show order, walk the cue-order tree on `GET /rundowns/:id` instead."
        "400":
          description: Cursor or limit value is invalid.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem400"
        "401":
          description: Token missing, malformed, expired, or revoked.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem401"
        "404":
          description: Rundown does not exist or has been deleted.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem404"
        "429": *a1
    post:
      operationId: create_cue
      summary: Create one cue
      description: |-
        Polymorphic create: `type` picks `cue`, `heading`, or `group`. `position`
        controls placement (defaults to `"end"`).

        Placement rules:
        - New cues of type `group` must land at root level — `after_cue_id` /
          `before_cue_id` targeting a child cue is rejected with 422
          `cues.invalid_position`. Use `:reorder` for hierarchical edits.
        - Targets that don't exist → 422 `cues.position_target_not_found`.

        `meta.side_effects` is always `[]` for cue creation.
      tags:
        - Cues
      x-mcp: true
      parameters:
        - name: id
          in: path
          required: true
          description: Rundown id.
          schema:
            type: string
            example: YOmm4UG2SGSomuOci44c
      requestBody:
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  properties:
                    type:
                      type: string
                      const: cue
                    title:
                      type: string
                      maxLength: 500
                      example: Opening
                    subtitle:
                      type: string
                      maxLength: 500
                      example: Host walks on stage
                    duration_ms:
                      type: integer
                      minimum: 0
                      maximum: 86400000
                      description: Duration in milliseconds. Ignored on `heading` and `group` (forced to 0 server-side).
                      example: 60000
                    background_color:
                      type: string
                      maxLength: 20
                      example: "#3b0764"
                    start_time:
                      anyOf:
                        - type: integer
                          minimum: -9007199254740991
                          maximum: 9007199254740991
                        - type: "null"
                      description: Fixed wall-clock start time in epoch milliseconds. Non-null marks the cue as a hard-start anchor (FIXED). `null` makes the cue flexible.
                      example: 1750024800000
                    position:
                      $ref: "#/components/schemas/CuePlacement"
                  required:
                    - type
                  description: Create a regular cue.
                - type: object
                  properties:
                    type:
                      type: string
                      const: heading
                    title:
                      type: string
                      maxLength: 500
                      example: Opening
                    subtitle:
                      type: string
                      maxLength: 500
                      example: Host walks on stage
                    background_color:
                      type: string
                      maxLength: 20
                      example: "#3b0764"
                    position:
                      $ref: "#/components/schemas/CuePlacement"
                  required:
                    - type
                  description: Create a heading row.
                - type: object
                  properties:
                    type:
                      type: string
                      const: group
                    title:
                      type: string
                      maxLength: 500
                      example: Opening
                    subtitle:
                      type: string
                      maxLength: 500
                      example: Host walks on stage
                    background_color:
                      type: string
                      maxLength: 20
                      example: "#3b0764"
                    position:
                      $ref: "#/components/schemas/CuePlacement"
                  required:
                    - type
                  description: Create a group cue. Children land via subsequent `POST /cues` calls with `position.after_cue_id` pointing at this group's id, or via `:reorder`.
              description: Body for `POST /rundowns/:id/cues`. `type` discriminates between `cue`, `heading`, and `group`. `position` defaults to `"end"`.
              type: object
      responses:
        "201":
          description: Cue created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: Cue created
                  data:
                    $ref: "#/components/schemas/Cue"
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for POST /rundowns/:id/cues. Status 201.
        "400":
          description: Body failed validation.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem400"
        "401":
          description: Token missing, malformed, expired, or revoked.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem401"
        "403":
          description: Token role insufficient (EDIT required).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem403"
        "404":
          description: Rundown does not exist or has been deleted.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem404"
        "422":
          description: Semantic rejection (invalid position, target not found, group placement).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem422"
        "429": *a1
  /rundowns/{id}/cues:bulk:
    post:
      operationId: bulk_create_cues
      summary: Create many cues
      description: |-
        Atomic bulk create. Array order is insertion order; the batch lands
        contiguously at the optional `position` (defaults to `"end"`). Groups
        in the array land empty — to populate group children in one round-trip,
        use full-rundown create (`POST /rundowns`) or follow up with
        `:reorder`.

        Caps: max 500 items per call (caps middleware also enforces structurally).

        Errors:
        - 400 `cues.invalid_payload` — body shape rejected by validation.
        - 422 `cues.invalid_position` — batch position descriptor is invalid (e.g. group placement inside another group).
        - 422 `cues.position_target_not_found` — `after_cue_id` / `before_cue_id` target does not exist.
      tags:
        - Cues
      x-mcp: true
      parameters:
        - name: id
          in: path
          required: true
          description: Rundown id.
          schema:
            type: string
            example: YOmm4UG2SGSomuOci44c
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                cues:
                  minItems: 1
                  maxItems: 500
                  type: array
                  items:
                    oneOf:
                      - type: object
                        properties:
                          type:
                            type: string
                            const: cue
                          title:
                            type: string
                            maxLength: 500
                            example: Opening
                          subtitle:
                            type: string
                            maxLength: 500
                            example: Host walks on stage
                          duration_ms:
                            type: integer
                            minimum: 0
                            maximum: 86400000
                            description: Duration in milliseconds. Ignored on `heading` and `group` (forced to 0 server-side).
                            example: 60000
                          background_color:
                            type: string
                            maxLength: 20
                            example: "#3b0764"
                          start_time:
                            anyOf:
                              - type: integer
                                minimum: -9007199254740991
                                maximum: 9007199254740991
                              - type: "null"
                            description: Fixed wall-clock start time in epoch milliseconds. Non-null marks the cue as a hard-start anchor (FIXED). `null` makes the cue flexible.
                            example: 1750024800000
                        required:
                          - type
                        description: Bulk-create entry for a regular cue.
                      - type: object
                        properties:
                          type:
                            type: string
                            const: heading
                          title:
                            type: string
                            maxLength: 500
                            example: Opening
                          subtitle:
                            type: string
                            maxLength: 500
                            example: Host walks on stage
                          background_color:
                            type: string
                            maxLength: 20
                            example: "#3b0764"
                        required:
                          - type
                        description: Bulk-create entry for a heading row.
                      - type: object
                        properties:
                          type:
                            type: string
                            const: group
                          title:
                            type: string
                            maxLength: 500
                            example: Opening
                          subtitle:
                            type: string
                            maxLength: 500
                            example: Host walks on stage
                          background_color:
                            type: string
                            maxLength: 20
                            example: "#3b0764"
                        required:
                          - type
                        description: Bulk-create entry for a group cue. Groups land empty — populate children via subsequent `POST /cues` calls or `:reorder`.
                    description: One entry in a `cues:bulk` array. Same shape as single-create minus `position` (batch-level only).
                    type: object
                  description: Array of cues to create in insertion order. Min 1, max 500 per call. Caps middleware enforces the same cap structurally.
                position:
                  description: 'Where the whole batch lands (default `"end"`). One descriptor for the entire array, not per-item — every created cue lands contiguously here, in array order. Same four shapes as single-create: `"start"` (top of the rundown), `"end"` (bottom), `{ "after_cue_id": "..." }` (right after a named cue), `{ "before_cue_id": "..." }` (right before one). Placing the batch inside a group is not supported here — create at root, then `:reorder` to nest.'
                  $ref: "#/components/schemas/CuePlacement"
              required:
                - cues
              description: Body for `POST /rundowns/:id/cues:bulk`. Creates many cues in one atomic call. Array order is insertion order; the optional batch-level `position` controls where the whole block lands (defaults to `"end"`). Groups land empty — use `:reorder` to populate children, or use full-rundown create when columns + cells need to land in the same call.
      responses:
        "201":
          description: Cues created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: Created 3 cues
                  data:
                    type: object
                    properties:
                      cues:
                        type: array
                        items:
                          $ref: "#/components/schemas/Cue"
                        description: Created cues in the same order as the request array, each carrying its newly-assigned `id`.
                      count:
                        type: integer
                        minimum: -9007199254740991
                        maximum: 9007199254740991
                        example: 3
                    required:
                      - cues
                      - count
                    additionalProperties: false
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for `POST /rundowns/:id/cues:bulk`. Status 201.
        "400":
          description: Body failed validation.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem400"
        "401":
          description: Token missing, malformed, expired, or revoked.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem401"
        "403":
          description: Token role insufficient (EDIT required).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem403"
        "404":
          description: Rundown does not exist or has been deleted.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem404"
        "422":
          description: Semantic rejection (invalid position, target not found, oversized batch).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem422"
        "429": *a1
  /rundowns/{id}/cues:reorder:
    post:
      operationId: reorder_cues
      summary: Reorder cues
      description: |-
        Replaces the whole hierarchical cue-order tree in one atomic write.
        Body items are `{ cue_id, children? }`.

        Validation:
        - The ID set in the body must equal the rundown's current cue IDs
          exactly — no adds or removes. Use `POST /cues` and `DELETE /cues/:cue_id`
          for adds/removes.
        - No duplicate IDs.
        - `children` is allowed only on cues of type `group`.
        - Children must be leaves — no nested groups.

        Errors:
        - 422 `cues.invalid_reorder` — body shape or ID-set check failed; `details` carries the specific reason.
      tags:
        - Cues
      x-mcp: true
      parameters:
        - name: id
          in: path
          required: true
          description: Rundown id.
          schema:
            type: string
            example: YOmm4UG2SGSomuOci44c
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                cue_order:
                  type: array
                  items:
                    type: object
                    properties:
                      cue_id:
                        type: string
                        minLength: 1
                        example: Ii6WeSEQmEqyKY6YAu8c
                      children:
                        description: Present only on group cues. Omit for cue/heading-typed items.
                        type: array
                        items:
                          type: object
                          properties:
                            cue_id:
                              type: string
                              minLength: 1
                              example: IMS282k2WQOcm0AOeW4C
                          required:
                            - cue_id
                          description: A child item in a group. Children are always leaves — nested groups are not supported.
                    required:
                      - cue_id
                    description: A top-level item in the cue order tree. May carry `children` only if the cue is type `group`.
                  description: The full new cue-order tree. Every cue currently in the rundown must appear exactly once; no cue may be added or removed via this endpoint (use `POST /cues` / `DELETE /cues/:cue_id` for that).
              required:
                - cue_order
              description: "Body for `POST /rundowns/:id/cues:reorder`. Sets the whole hierarchical cue-order tree atomically. Items are `{ cue_id, children? }` (snake_case wire shape). Validation: ID set must match the rundown's current cue IDs exactly, no duplicates, children only on group-typed cues, no nested groups."
      responses:
        "200":
          description: Cue order updated.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: Cues reordered
                  data:
                    type: object
                    properties:
                      cue_order:
                        type: array
                        items:
                          $ref: "#/components/schemas/CueOrderNode"
                        description: The rundown's new cue-order tree after the reorder.
                    required:
                      - cue_order
                    additionalProperties: false
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for `POST /rundowns/:id/cues:reorder`.
        "400":
          description: Body failed validation.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem400"
        "401":
          description: Token missing, malformed, expired, or revoked.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem401"
        "403":
          description: Token role insufficient (EDIT required).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem403"
        "404":
          description: Rundown does not exist or has been deleted.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem404"
        "422":
          description: ID-set mismatch, duplicate IDs, children on non-group cue, or nested groups.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem422"
        "429": *a1
  /rundowns/{id}/cues/{cue_id}:
    get:
      operationId: get_cue
      summary: Fetch one cue
      description: Returns the cue at `:cue_id`. 404 if the cue doesn't exist, has been deleted, or belongs to a rundown the token can't see.
      tags:
        - Cues
      x-mcp: true
      parameters:
        - name: id
          in: path
          required: true
          description: Rundown id.
          schema:
            type: string
            example: YOmm4UG2SGSomuOci44c
        - name: cue_id
          in: path
          required: true
          description: Cue id.
          schema:
            type: string
            example: Ii6WeSEQmEqyKY6YAu8c
      responses:
        "200":
          description: Cue payload.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: OK
                  data:
                    $ref: "#/components/schemas/Cue"
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for GET /rundowns/:id/cues/:cue_id.
        "401":
          description: Token missing, malformed, expired, or revoked.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem401"
        "404":
          description: Cue or rundown does not exist.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem404"
        "429": *a1
    patch:
      operationId: update_cue
      summary: Update one cue
      description: |-
        All body fields optional; the body must carry at least one. Three
        kinds of change can happen in a single request:

        1. **Field edits** — `title`, `subtitle`, `duration_ms`, `background_color`,
           `start_time`.
        2. **Type conversion** — set `type` to `cue` / `heading` / `group`.
           `group → cue/heading` requires an empty group (else 409
           `cues.group_not_empty` with `details.children_ids`). Converting
           `cue → heading/group` keeps the cue's cells in the DB but they are
           filtered out of cell endpoints — reverting to `cue` restores them.
        3. **Move** — set `position` to `"start"` / `"end"` / `{after_cue_id}` /
           `{before_cue_id}`. Groups can't move to a child-level target.

        `prevent_edits` guards the cue's CONTENTS. A locked cue rejects any
        PATCH (423 `cues.prevent_edits`) and any cell write (409 `cells.prevent_edits`).
        The lock is a human-only toggle — release it on the rundown screen by
        going to the cue's Settings → Prevent Edits (checkbox); API tokens
        cannot flip it. Reorder and delete are NOT blocked — the lock is about
        content immutability, not existence.
      tags:
        - Cues
      x-mcp: true
      parameters:
        - name: id
          in: path
          required: true
          description: Rundown id.
          schema:
            type: string
            example: YOmm4UG2SGSomuOci44c
        - name: cue_id
          in: path
          required: true
          description: Cue id.
          schema:
            type: string
            example: Ii6WeSEQmEqyKY6YAu8c
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                type:
                  description: "Optional type conversion. Allowed transitions: `cue ↔ heading`, `cue ↔ group`, `heading ↔ group`. `group → cue/heading` requires the group to have no children, else 409 `cues.group_not_empty`."
                  example: cue
                  type: string
                  enum:
                    - cue
                    - heading
                    - group
                title:
                  type: string
                  maxLength: 500
                  example: Opening
                subtitle:
                  type: string
                  maxLength: 500
                  example: Host walks on stage
                duration_ms:
                  type: integer
                  minimum: 0
                  maximum: 86400000
                  description: Duration in milliseconds. Ignored on `heading` and `group` (forced to 0 server-side).
                  example: 60000
                background_color:
                  type: string
                  maxLength: 20
                  example: "#3b0764"
                start_time:
                  anyOf:
                    - type: integer
                      minimum: -9007199254740991
                      maximum: 9007199254740991
                    - type: "null"
                  description: Fixed wall-clock start time in epoch milliseconds. Non-null marks the cue as a hard-start anchor (FIXED). `null` makes the cue flexible.
                  example: 1750024800000
                position:
                  description: Optional move. Applied after type conversion (if any).
                  $ref: "#/components/schemas/CuePlacement"
              description: "PATCH body for `/rundowns/:id/cues/:cue_id`. All fields optional; at least one must be present. `type` performs a type conversion; `position` performs a move. Both can be combined in one request. A cue with `prevent_edits: true` rejects every PATCH (423 `cues.prevent_edits`) — the lock is a human-only toggle: release it on the rundown screen by going to the cue's Settings → Prevent Edits (checkbox); API tokens cannot flip it."
      responses:
        "200":
          description: Cue updated. Body is the cue's new state.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: Cue updated
                  data:
                    $ref: "#/components/schemas/Cue"
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for PATCH /rundowns/:id/cues/:cue_id.
        "400":
          description: Body failed validation.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem400"
        "401":
          description: Token missing, malformed, expired, or revoked.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem401"
        "403":
          description: Token role insufficient (EDIT required).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem403"
        "404":
          description: Cue or rundown does not exist.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem404"
        "409":
          description: Group has children — convert is blocked until they are moved or deleted.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem409"
        "422":
          description: Empty body, invalid type conversion, or invalid position descriptor.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem422"
        "423":
          description: "Cue has `prevent_edits: true`; release the lock on the rundown screen (cue Settings → Prevent Edits)."
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem423"
        "429": *a1
    delete:
      operationId: delete_cue
      summary: Soft-delete one cue
      description: |-
        Removes the cue from the rundown's order tree and marks it deleted.
        A group cue cascades to its children (they soft-delete too).

        Soft-delete: hidden immediately, retained for 30 days, then a daily
        cleanup purges it permanently. Recover within that window from the
        dashboard — v1 has no restore endpoint.

        `prevent_edits` does NOT block deletion — it guards cells only.
      tags:
        - Cues
      x-mcp: true
      parameters:
        - name: id
          in: path
          required: true
          description: Rundown id.
          schema:
            type: string
            example: YOmm4UG2SGSomuOci44c
        - name: cue_id
          in: path
          required: true
          description: Cue id.
          schema:
            type: string
            example: Ii6WeSEQmEqyKY6YAu8c
      responses:
        "200":
          description: Cue soft-deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: Cue deleted
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                        example: Ii6WeSEQmEqyKY6YAu8c
                      deleted_at:
                        type: string
                        example: 2026-05-18T12:00:00.000Z
                    required:
                      - id
                      - deleted_at
                    additionalProperties: false
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: "Success envelope for DELETE /rundowns/:id/cues/:cue_id. Soft-delete: a background pass eventually hard-removes the cue + its cells."
        "401":
          description: Token missing, malformed, expired, or revoked.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem401"
        "403":
          description: Token role insufficient (EDIT required).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem403"
        "404":
          description: Cue or rundown does not exist.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem404"
        "429": *a1
  /rundowns/{id}/columns:
    get:
      operationId: list_columns
      summary: List columns
      description: |-
        Returns the rundown's public columns in display order. Private columns
        (those scoped to a single dashboard user via `privateUid`) are filtered
        out — agents acting on a team token have no clean "which user" answer,
        so the spec hides them entirely.

        No pagination: column counts are bounded (~50 per rundown).

        Example:
        ```
        curl -H "Authorization: Bearer $TOKEN" \
          "https://api-v1.rundownstudio.app/rundowns/YOmm4UG2SGSomuOci44c/columns"
        ```
      tags:
        - Columns
      x-mcp: true
      parameters:
        - name: id
          in: path
          required: true
          description: Rundown id.
          schema:
            type: string
            example: YOmm4UG2SGSomuOci44c
      responses:
        "200":
          description: Columns in display order.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: Returned 4 columns
                  data:
                    type: object
                    properties:
                      items:
                        type: array
                        items:
                          $ref: "#/components/schemas/Column"
                        description: Public columns in their display order (matches the rundown's `columns` array, with private columns filtered out).
                      count:
                        type: integer
                        minimum: -9007199254740991
                        maximum: 9007199254740991
                        example: 4
                    required:
                      - items
                      - count
                    additionalProperties: false
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for GET /rundowns/:id/columns. No pagination — column counts are bounded (~50 per rundown).
        "401":
          description: Token missing, malformed, expired, or revoked.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem401"
        "404":
          description: Rundown does not exist or has been deleted.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem404"
        "429": *a1
    post:
      operationId: create_column
      summary: Create one column
      description: |-
        Adds a new public column to the rundown. `position` controls placement
        (default `"end"`). Targets in `after_column_id` / `before_column_id`
        must reference existing public columns — private or unknown IDs return
        422 `columns.invalid_position`.

        v1 supports `type: "text"` only. The field is accepted (and defaulted)
        so future richer types land without a breaking change.

        Errors:
        - 400 `columns.invalid_payload` — body shape rejected by validation.
        - 422 `columns.invalid_position` — target column for placement does not exist or is private.
        - 422 `columns.too_many` — would exceed the 50-columns-per-rundown cap.
      tags:
        - Columns
      x-mcp: true
      parameters:
        - name: id
          in: path
          required: true
          description: Rundown id.
          schema:
            type: string
            example: YOmm4UG2SGSomuOci44c
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                  description: Display name. Used in CSV export headers and as the lookup key when authoring cells inline via `POST /rundowns`.
                  example: AUDIO
                type:
                  description: v1 supports `text` only. Field is accepted (and defaulted) so future richer types land without a breaking change.
                  example: text
                  type: string
                  const: text
                position:
                  description: Where to place the new column. Defaults to `"end"`.
                  $ref: "#/components/schemas/ColumnPlacement"
              required:
                - name
              description: Body for `POST /rundowns/:id/columns`.
      responses:
        "201":
          description: Column created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: Column created
                  data:
                    $ref: "#/components/schemas/Column"
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for POST /rundowns/:id/columns. Status 201.
        "400":
          description: Body failed validation.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem400"
        "401":
          description: Token missing, malformed, expired, or revoked.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem401"
        "403":
          description: Token role insufficient (EDIT required).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem403"
        "404":
          description: Rundown does not exist or has been deleted.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem404"
        "422":
          description: Semantic rejection (invalid position target, too many columns).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem422"
        "429": *a1
  /rundowns/{id}/columns:reorder:
    post:
      operationId: reorder_columns
      summary: Reorder columns
      description: |-
        Replaces the public-column display order in one atomic write. The body
        lists public columns only — private columns retain their relative
        positions in storage.

        Validation:
        - The ID set in the body must equal the rundown's current public
          column IDs exactly — no adds or removes. Use `POST /columns` and
          `DELETE /columns/:column_id` for that.
        - No duplicate IDs.
        - No unknown or private IDs.

        Errors:
        - 422 `columns.invalid_reorder` — body shape or ID-set check failed; `details` carries the specific reason.
      tags:
        - Columns
      x-mcp: true
      parameters:
        - name: id
          in: path
          required: true
          description: Rundown id.
          schema:
            type: string
            example: YOmm4UG2SGSomuOci44c
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                column_order:
                  type: array
                  items:
                    type: object
                    properties:
                      column_id:
                        type: string
                        minLength: 1
                        example: EoeiemGEmeMOY8GwUK6s
                    required:
                      - column_id
                  description: The full new public-column order. Every public column currently in the rundown must appear exactly once; no add or remove via this endpoint.
              required:
                - column_order
              description: Body for `POST /rundowns/:id/columns:reorder`. Sets the public-column order atomically. The ID set must match the rundown's current public columns exactly — no duplicates, no unknown IDs, no missing IDs. Private columns are not addressable here and retain their relative positions in storage.
      responses:
        "200":
          description: Column order updated.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: Columns reordered
                  data:
                    type: object
                    properties:
                      column_order:
                        type: array
                        items:
                          type: string
                        description: Public column IDs in their new display order.
                        example:
                          - EoeiemGEmeMOY8GwUK6s
                          - Sw0kqCqSuM8gKeICwsIa
                          - aAaoceeWc8OUCCGCKqEg
                    required:
                      - column_order
                    additionalProperties: false
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for POST /rundowns/:id/columns:reorder.
        "400":
          description: Body failed validation.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem400"
        "401":
          description: Token missing, malformed, expired, or revoked.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem401"
        "403":
          description: Token role insufficient (EDIT required).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem403"
        "404":
          description: Rundown does not exist or has been deleted.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem404"
        "422":
          description: ID-set mismatch, duplicate IDs, or unknown/private IDs.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem422"
        "429": *a1
  /rundowns/{id}/columns/{column_id}:
    get:
      operationId: get_column
      summary: Fetch one column
      description: Returns the column at `:column_id`. 404 if the column doesn't exist, has been soft-deleted, is private, or belongs to a rundown the token can't see.
      tags:
        - Columns
      x-mcp: true
      parameters:
        - name: id
          in: path
          required: true
          description: Rundown id.
          schema:
            type: string
            example: YOmm4UG2SGSomuOci44c
        - name: column_id
          in: path
          required: true
          description: Column id.
          schema:
            type: string
            example: EoeiemGEmeMOY8GwUK6s
      responses:
        "200":
          description: Column payload.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: OK
                  data:
                    $ref: "#/components/schemas/Column"
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for GET /rundowns/:id/columns/:column_id.
        "401":
          description: Token missing, malformed, expired, or revoked.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem401"
        "404":
          description: Column or rundown does not exist (or column is private and therefore invisible to v1).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem404"
        "429": *a1
    patch:
      operationId: update_column
      summary: Update one column
      description: |-
        Only `name` is editable; width and type are not. Empty body is rejected
        with 422 `columns.empty_patch`.

        Renaming a column does NOT touch its cells — cells reference columns by
        id, not name. (Inline cell authoring in `POST /rundowns` uses names
        because IDs don't exist yet at import time — once columns have IDs,
        cells bind to those IDs.)
      tags:
        - Columns
      x-mcp: true
      parameters:
        - name: id
          in: path
          required: true
          description: Rundown id.
          schema:
            type: string
            example: YOmm4UG2SGSomuOci44c
        - name: column_id
          in: path
          required: true
          description: Column id.
          schema:
            type: string
            example: EoeiemGEmeMOY8GwUK6s
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                  description: Display name. Used in CSV export headers and as the lookup key when authoring cells inline via `POST /rundowns`.
                  example: AUDIO
              description: PATCH body for `/rundowns/:id/columns/:column_id`. Only `name` is editable; width and type are not. Empty body is rejected with 422 `columns.empty_patch`.
      responses:
        "200":
          description: Column updated.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: Column updated
                  data:
                    $ref: "#/components/schemas/Column"
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for PATCH /rundowns/:id/columns/:column_id.
        "400":
          description: Body failed validation.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem400"
        "401":
          description: Token missing, malformed, expired, or revoked.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem401"
        "403":
          description: Token role insufficient (EDIT required).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem403"
        "404":
          description: Column or rundown does not exist (or column is private).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem404"
        "422":
          description: Empty body or no recognized fields.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem422"
        "429": *a1
    delete:
      operationId: delete_column
      summary: Soft-delete one column
      description: |-
        Removes the column from the rundown's column order and marks it
        deleted. The column's cells are also dropped from the public view.

        Soft-delete: hidden immediately, retained for 30 days, then a daily
        cleanup purges it (and its cells) permanently. Recover within that
        window from the dashboard — v1 has no restore endpoint.

        Private columns cannot be deleted via v1 — returns 404 `columns.not_found`.
      tags:
        - Columns
      x-mcp: true
      parameters:
        - name: id
          in: path
          required: true
          description: Rundown id.
          schema:
            type: string
            example: YOmm4UG2SGSomuOci44c
        - name: column_id
          in: path
          required: true
          description: Column id.
          schema:
            type: string
            example: EoeiemGEmeMOY8GwUK6s
      responses:
        "200":
          description: Column soft-deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: Column deleted
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                        example: EoeiemGEmeMOY8GwUK6s
                      deleted_at:
                        type: string
                        example: 2026-05-21T12:00:00.000Z
                    required:
                      - id
                      - deleted_at
                    additionalProperties: false
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: "Success envelope for DELETE /rundowns/:id/columns/:column_id. Soft-delete: cells in this column are also dropped from the public view."
        "401":
          description: Token missing, malformed, expired, or revoked.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem401"
        "403":
          description: Token role insufficient (EDIT required).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem403"
        "404":
          description: Column or rundown does not exist (or column is private).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem404"
        "429": *a1
  /rundowns/{id}/cells:
    get:
      operationId: list_cells
      summary: List cells
      description: |-
        Returns the **virtual cell grid** — every coordinate of `cue`-type cues
        (in `cue_order`) × public columns (in `column_order`), walked cue-major.
        Unpopulated coordinates are returned as the empty cell (content `""`, null
        timestamps), so the consumer reads a complete grid and never reconciles
        missing cells. Storage stays sparse — this is purely the read shape.
        Cells on `heading` / `group` cues and cells in private columns are not part
        of the grid.

        Optional `cue_id` / `column_id` query params narrow the scope:
        - neither → the whole grid;
        - `cue_id` only → that cue's row (one cell per public column);
        - `column_id` only → that column (one cell per `cue`-type cue);
        - both → the single cell at that coordinate (always exactly one).

        A `cue_id` / `column_id` that does not resolve to a visible cue / column
        returns 404. A `heading` / `group` `cue_id` is a valid cue that holds no
        grid row, so it returns an empty list (not a 404).

        No cursor pagination: `limit` (default 50, max 1000) caps the returned
        coordinates and anything beyond it is truncated. The response reports
        `count` of `total` (the full grid size for the scope), so a truncated grid
        is never silent — raise `limit` or narrow with the filters.
      tags:
        - Cells
      x-mcp: true
      parameters:
        - &a18
          name: id
          in: path
          required: true
          description: Rundown id.
          schema:
            type: string
            example: YOmm4UG2SGSomuOci44c
        - name: cue_id
          in: query
          required: false
          description: Optional filter — only this cue's grid row. Combine with `column_id` for the single cell at that coordinate.
          schema:
            type: string
            example: Ii6WeSEQmEqyKY6YAu8c
        - name: column_id
          in: query
          required: false
          description: Optional filter — only this column's grid cells. Combine with `cue_id` for the single cell at that coordinate.
          schema:
            type: string
            example: EoeiemGEmeMOY8GwUK6s
        - name: limit
          in: query
          required: false
          description: Maximum number of grid coordinates to return (default 50, max 1000). Coordinates past the limit are truncated — there is no cursor; `total` reports the full grid size.
          schema:
            type: integer
            minimum: 1
            maximum: 1000
            default: 50
            example: 50
      responses:
        "200":
          description: The cell grid for the scope — every coordinate of `cue`-type cues × public columns (empties included), narrowed by the requested `cue_id` / `column_id` and capped by `limit`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: Returned 12 of 700 cells
                  data:
                    type: object
                    properties:
                      items:
                        type: array
                        items:
                          $ref: "#/components/schemas/Cell"
                        description: Cells at the grid coordinates, in `cue_order` × `column_order` order. Unpopulated coordinates are returned as the empty cell (content `""`, null timestamps) — the consumer reads a complete grid and never reconciles missing cells. Cells on heading/group cues and cells in private columns are not part of the grid.
                      count:
                        type: integer
                        minimum: -9007199254740991
                        maximum: 9007199254740991
                        description: Number of coordinates returned in `items` (≤ `limit`).
                        example: 12
                      total:
                        type: integer
                        minimum: -9007199254740991
                        maximum: 9007199254740991
                        description: Full grid size for the requested scope (cues × columns; `cue_id` → columns; `column_id` → cues; both → 1). `count < total` means `limit` truncated the result — raise `limit` or narrow the scope; there is no cursor.
                        example: 700
                    required:
                      - items
                      - count
                      - total
                    additionalProperties: false
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for `GET /rundowns/:id/cells` — the virtual cell grid for the scope, optionally narrowed by the `cue_id` / `column_id` filters and capped by `limit`. Reports `count` of `total` so a truncated grid is never silent.
        "400":
          description: Query string malformed (e.g. empty `cue_id` / `column_id`).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem400"
        "401": &a19
          description: Token missing, malformed, expired, or revoked.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem401"
        "404":
          description: Rundown does not exist, or a supplied `cue_id` / `column_id` filter does not resolve to a visible cue / column.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem404"
        "429": *a1
    patch:
      operationId: patch_cell
      summary: Update one cell
      description: |-
        Sets the content at `(cue_id, column_id)`. Creates the cell if it did
        not exist, updates it if it did — always 200, no create/update
        distinction. An empty `content` / `content_html` CLEARS the cell
        (removes it); there is no separate delete. The response `data` is the
        cell's resulting state — the empty cell (content `""`, null timestamps)
        when cleared.

        Errors:
        - 404 `rundowns.not_found` / `cues.not_found` / `columns.not_found` — target is missing or invisible.
        - 409 `cells.prevent_edits` — the target cue has `prevent_edits: true`.
        - 422 `cells.invalid_cue_type` — the target cue is a `heading` or `group`.
        - 422 `cells.unsupported_column_type` — the target column is not a `text` column (v1 writes text only).
      tags:
        - Cells
      x-mcp: true
      parameters:
        - *a18
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                cue_id:
                  type: string
                  minLength: 1
                  description: Cue the cell belongs to. Must reference a non-deleted cue of type `cue` (not `heading`/`group`).
                  example: Ii6WeSEQmEqyKY6YAu8c
                column_id:
                  type: string
                  minLength: 1
                  description: Column the cell belongs to. Must reference a public, non-deleted column.
                  example: EoeiemGEmeMOY8GwUK6s
                content:
                  type: string
                  maxLength: 50000
                  description: Cell content as plain text. Escaped and stored as rich text — REPLACES any existing formatting/mentions/media in the cell. To preserve rich content, read and write `content_html` instead. An empty string CLEARS the cell (removes it) — there is no separate delete. Provide this OR `content_html`, never both.
                  example: track 7
                content_html:
                  type: string
                  maxLength: 100000
                  description: "Cell content as rich-text HTML. Sanitized on write against a tag/attribute allowlist (scripts, event handlers, unsafe URLs, and unknown tags are stripped) and echoed back in the response, so the stored result may differ cosmetically from the input. Standard formatting tags (including `<img src>` images and checkbox task lists) plus three custom nodes: `<rs-mention data-mention-id>`, `<rs-variable data-variable-key>`, `<rs-file filename url>`. The lossless, round-trippable representation. Provide this OR `content`, never both."
                  example: <p>track <strong>7</strong></p>
              required:
                - cue_id
                - column_id
              description: Body for `PATCH /rundowns/:id/cells`. Sets the content at `(cue_id, column_id)` — creates the cell if it does not exist, updates it if it does. An empty `content` / `content_html` clears the cell (removes it). Carries exactly one of `content` or `content_html`.
      responses:
        "200":
          description: Cell written (created, updated, or cleared).
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: Cell updated
                  data:
                    $ref: "#/components/schemas/Cell"
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for PATCH /rundowns/:id/cells. Always 200. `data` is the cell's resulting state — the empty cell (content `""`, null timestamps) when it was cleared.
        "400":
          description: Body failed validation.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem400"
        "401": *a19
        "403":
          description: Token role insufficient (EDIT required).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem403"
        "404":
          description: Rundown, cue, or column does not exist (or column is private).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem404"
        "409":
          description: Cue is marked prevent_edits.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem409"
        "422":
          description: Cue is heading/group, or column type is not text.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem422"
        "429": *a1
  /rundowns/{id}/cells:bulk:
    patch:
      operationId: patch_cells_bulk
      summary: Update many cells
      description: |-
        Bulk write. Items in `cells` are written (empty content clears);
        coordinates not in the array are untouched. Combines the horizontal +
        vertical slice surfaces — every item carries its own `(cue_id, column_id)`.
        Response `items` carries one entry per submitted item in request order
        (cleared coordinates render as the empty cell).

        Atomic — the whole batch lands in one write or none.

        Caps: max 500 items per call (caps middleware also enforces structurally).

        Errors:
        - 400 `cells.invalid_payload` — body shape rejected by validation.
        - 404 `cues.not_found` — one of the cue targets does not exist.
        - 404 `columns.not_found` — one of the column targets does not exist or is private.
        - 409 `cells.prevent_edits` — one of the cue targets has `prevent_edits: true`.
        - 422 `cells.duplicate_coordinate` — two items target the same `(cue_id, column_id)`. `details.duplicates[]` carries the conflicting indexes.
        - 422 `cells.invalid_cue_type` — one of the cue targets is a `heading` or `group`.
        - 422 `cells.unsupported_column_type` — one of the column targets is not a `text` column.
      tags:
        - Cells
      x-mcp: true
      parameters:
        - *a18
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                cells:
                  minItems: 1
                  maxItems: 500
                  type: array
                  items:
                    type: object
                    properties:
                      cue_id:
                        type: string
                        minLength: 1
                        example: Ii6WeSEQmEqyKY6YAu8c
                      column_id:
                        type: string
                        minLength: 1
                        example: EoeiemGEmeMOY8GwUK6s
                      content:
                        type: string
                        maxLength: 50000
                        description: Cell content as plain text. Escaped and stored as rich text — REPLACES any existing formatting/mentions/media in the cell. To preserve rich content, read and write `content_html` instead. An empty string CLEARS the cell (removes it) — there is no separate delete. Provide this OR `content_html`, never both.
                        example: track 7
                      content_html:
                        type: string
                        maxLength: 100000
                        description: "Cell content as rich-text HTML. Sanitized on write against a tag/attribute allowlist (scripts, event handlers, unsafe URLs, and unknown tags are stripped) and echoed back in the response, so the stored result may differ cosmetically from the input. Standard formatting tags (including `<img src>` images and checkbox task lists) plus three custom nodes: `<rs-mention data-mention-id>`, `<rs-variable data-variable-key>`, `<rs-file filename url>`. The lossless, round-trippable representation. Provide this OR `content`, never both."
                        example: <p>track <strong>7</strong></p>
                    required:
                      - cue_id
                      - column_id
                  description: Cells to write, each carrying its own `(cue_id, column_id)`. Items missing from the array are left alone; an item with empty content clears that cell.
              required:
                - cells
              description: Body for `PATCH /rundowns/:id/cells:bulk`. Writes across both axes — items in `cells` are written (empty content clears); items NOT in the array are untouched. Duplicate `(cue_id, column_id)` coordinates within one body are rejected with `cells.duplicate_coordinate`.
      responses:
        "200":
          description: Cells written (created, updated, or cleared).
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: Patched 70 cells
                  data:
                    type: object
                    properties:
                      items:
                        type: array
                        items:
                          $ref: "#/components/schemas/Cell"
                        description: One entry per submitted item, in request order. Cleared coordinates render as the empty cell (content `""`, null timestamps).
                      count:
                        type: integer
                        minimum: -9007199254740991
                        maximum: 9007199254740991
                        example: 70
                    required:
                      - items
                      - count
                    additionalProperties: false
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for PATCH /rundowns/:id/cells:bulk.
        "400":
          description: Body failed validation.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem400"
        "401": *a19
        "403":
          description: Token role insufficient (EDIT required).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem403"
        "404":
          description: Rundown, or one of the referenced cues/columns, does not exist.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem404"
        "409":
          description: One of the cue targets is marked prevent_edits.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem409"
        "422":
          description: Duplicate coordinates, invalid cue type, or unsupported column type.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem422"
        "429": *a1
  /rundowns/{id}/mentions:
    get:
      operationId: list_mentions
      summary: List mentions
      description: |-
        Returns every mention in the rundown in creation order.
        No pagination — mentions are bounded (~100 per rundown).

        Example:
        ```
        curl -H "Authorization: Bearer $TOKEN" \
          "https://api-v1.rundownstudio.app/rundowns/YOmm4UG2SGSomuOci44c/mentions"
        ```
      tags:
        - Mentions
      x-mcp: true
      parameters:
        - &a20
          name: id
          in: path
          required: true
          description: Rundown id.
          schema:
            type: string
            example: YOmm4UG2SGSomuOci44c
      responses:
        "200":
          description: Mentions in creation order.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: Returned 3 mentions
                  data:
                    type: object
                    properties:
                      items:
                        type: array
                        items:
                          $ref: "#/components/schemas/Mention"
                        description: "Mentions in creation order. Soft cap: 100 mentions per rundown."
                      count:
                        type: integer
                        minimum: -9007199254740991
                        maximum: 9007199254740991
                        example: 3
                    required:
                      - items
                      - count
                    additionalProperties: false
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for GET /rundowns/:id/mentions. No pagination — counts are bounded (~100 per rundown).
        "401": &a21
          description: Token missing, malformed, expired, or revoked.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem401"
        "404": &a22
          description: Rundown does not exist or has been deleted.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem404"
        "429": *a1
    post:
      operationId: create_mention
      summary: Create one mention
      description: |-
        Creates a new mention with a server-assigned `id`. All body fields are
        optional — missing fields default (placeholder name, deterministic color
        from the name, empty description).

        Errors:
        - 400 `mentions.invalid_payload` — body shape rejected by validation.
        - 422 `mentions.name_conflict` — a mention with that name already exists in this rundown.
        - 422 `mentions.too_many` — would exceed the 100-mentions-per-rundown cap.
      tags:
        - Mentions
      x-mcp: true
      parameters:
        - *a20
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 50
                  pattern: ^[a-zA-Z0-9\s\-_]+$
                  description: Display name. Letters/numbers/spaces/hyphens/underscores only. Server picks a placeholder (`Mention N`) when omitted on create.
                  example: Alice Cooper
                color:
                  type: string
                  pattern: ^hsla?\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)%\s*,\s*(\d+(?:\.\d+)?)%\s*(?:,\s*(0(?:\.\d+)?|1(?:\.0+)?)\s*)?\)$
                  description: "`hsl(...)` or `hsla(...)` color. Server picks a deterministic preset color from the name when omitted."
                  example: hsla(220, 70%, 50%, 1)
                description:
                  type: string
                  maxLength: 5000
                  description: Free-form description. Empty string by default.
                  example: Guest in segment 3
              description: Body for `POST /rundowns/:id/mentions`. All fields optional; the server assigns the `id` and fills any missing field with a sensible default (placeholder name, deterministic color, empty description).
      responses:
        "201":
          description: Mention created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: Mention created
                  data:
                    $ref: "#/components/schemas/Mention"
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for POST /rundowns/:id/mentions. Status 201.
        "400":
          description: Body failed validation.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem400"
        "401": *a21
        "403": &a23
          description: Token role insufficient (EDIT required).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem403"
        "404": *a22
        "422":
          description: Name conflict or per-rundown cap exceeded.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem422"
        "429": *a1
  /rundowns/{id}/mentions:bulk:
    post:
      operationId: bulk_update_mentions
      summary: Update many mentions
      description: |-
        Atomic bulk update by `id`. Merge-only — every `id` must already
        exist; missing ids fail the whole batch with 404 `mentions.not_found`.
        Use `POST /mentions` to create new mentions.

        Errors:
        - 400 `mentions.invalid_payload` — body shape rejected by validation.
        - 404 `mentions.not_found` — one of the ids does not exist in this rundown.
        - 422 `mentions.duplicate_id` — two items share the same `id`. `details.duplicates[]` carries the conflicting indexes.
        - 422 `mentions.name_conflict` — two updates would produce mentions sharing the same `name`, or one update conflicts with an existing untouched mention.
      tags:
        - Mentions
      x-mcp: true
      parameters:
        - *a20
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                mentions:
                  minItems: 1
                  maxItems: 100
                  type: array
                  items:
                    type: object
                    properties:
                      id:
                        type: string
                        minLength: 1
                        description: Stable mention id. Used in URLs and bulk bodies; matches `<rs-mention data-mention-id>` references in cell `content_html`.
                        example: RH0Ao1j2pZu4DhccM4N3
                      name:
                        type: string
                        minLength: 1
                        maxLength: 50
                        pattern: ^[a-zA-Z0-9\s\-_]+$
                        description: Display name. Letters/numbers/spaces/hyphens/underscores only. Server picks a placeholder (`Mention N`) when omitted on create.
                        example: Alice Cooper
                      color:
                        type: string
                        pattern: ^hsla?\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)%\s*,\s*(\d+(?:\.\d+)?)%\s*(?:,\s*(0(?:\.\d+)?|1(?:\.0+)?)\s*)?\)$
                        description: "`hsl(...)` or `hsla(...)` color. Server picks a deterministic preset color from the name when omitted."
                        example: hsla(220, 70%, 50%, 1)
                      description:
                        type: string
                        maxLength: 5000
                        description: Free-form description. Empty string by default.
                        example: Guest in segment 3
                    required:
                      - id
                  description: Mentions to update by `id`. Merge-only — every `id` must already exist in the rundown (404 otherwise). Use `POST /mentions` to create new mentions.
              required:
                - mentions
              description: Body for `POST /rundowns/:id/mentions:bulk`. Merge semantics — every item updates an existing mention; missing ids fail the whole batch with 404 `mentions.not_found`. Atomic — the whole batch lands in one write or none. Duplicate `id` entries within one body are rejected with 422 `mentions.duplicate_id`.
      responses:
        "200":
          description: Mentions updated.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: Updated 5 mentions
                  data:
                    type: object
                    properties:
                      items:
                        type: array
                        items:
                          $ref: "#/components/schemas/Mention"
                      count:
                        type: integer
                        minimum: -9007199254740991
                        maximum: 9007199254740991
                        example: 5
                    required:
                      - items
                      - count
                    additionalProperties: false
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for POST /rundowns/:id/mentions:bulk.
        "400":
          description: Body failed validation.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem400"
        "401": *a21
        "403": *a23
        "404":
          description: Rundown or one of the referenced ids does not exist.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem404"
        "422":
          description: Duplicate ids in the body or a name conflict among the writes.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem422"
        "429": *a1
  /rundowns/{id}/mentions/{mention_id}:
    get:
      operationId: get_mention
      summary: Fetch one mention
      description: Returns the mention at `:mention_id`. 404 if no mention has that id.
      tags:
        - Mentions
      x-mcp: true
      parameters:
        - *a20
        - &a24
          name: mention_id
          in: path
          required: true
          description: Mention id.
          schema:
            type: string
            example: RH0Ao1j2pZu4DhccM4N3
      responses:
        "200":
          description: Mention payload.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: OK
                  data:
                    $ref: "#/components/schemas/Mention"
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for GET /rundowns/:id/mentions/:mention_id.
        "401": *a21
        "404":
          description: Rundown or mention does not exist.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem404"
        "429": *a1
    patch:
      operationId: update_mention
      summary: Update one mention
      description: |-
        Updates `name`, `color`, and/or `description`. Empty body is rejected
        with 422 `mentions.empty_patch`. `id` cannot be changed.

        Errors:
        - 400 `mentions.invalid_payload` — body shape rejected by validation.
        - 404 `mentions.not_found` — no mention with that id.
        - 422 `mentions.name_conflict` — another mention already has the new name.
        - 422 `mentions.empty_patch` — body is empty.
      tags:
        - Mentions
      x-mcp: true
      parameters:
        - *a20
        - *a24
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 50
                  pattern: ^[a-zA-Z0-9\s\-_]+$
                  description: Display name. Letters/numbers/spaces/hyphens/underscores only. Server picks a placeholder (`Mention N`) when omitted on create.
                  example: Alice Cooper
                color:
                  type: string
                  pattern: ^hsla?\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)%\s*,\s*(\d+(?:\.\d+)?)%\s*(?:,\s*(0(?:\.\d+)?|1(?:\.0+)?)\s*)?\)$
                  description: "`hsl(...)` or `hsla(...)` color. Server picks a deterministic preset color from the name when omitted."
                  example: hsla(220, 70%, 50%, 1)
                description:
                  type: string
                  maxLength: 5000
                  description: Free-form description. Empty string by default.
                  example: Guest in segment 3
              description: PATCH body for `/rundowns/:id/mentions/:mention_id`. Provide any subset of `name`, `color`, `description`. Empty body is rejected with 422 `mentions.empty_patch`. `id` cannot be changed.
      responses:
        "200":
          description: Mention updated.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: Mention updated
                  data:
                    $ref: "#/components/schemas/Mention"
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for PATCH /rundowns/:id/mentions/:mention_id.
        "400":
          description: Body failed validation.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem400"
        "401": *a21
        "403": *a23
        "404":
          description: Rundown or mention does not exist.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem404"
        "422":
          description: Name conflict or empty patch body.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem422"
        "429": *a1
    delete:
      operationId: delete_mention
      summary: Delete one mention
      description: Permanently deletes the mention at `:mention_id` — immediate and irreversible; mentions have no soft-delete or restore. 404 if no mention has that id.
      tags:
        - Mentions
      x-mcp: true
      parameters:
        - *a20
        - *a24
      responses:
        "200":
          description: Mention deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: Mention deleted
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                        example: RH0Ao1j2pZu4DhccM4N3
                      deleted_at:
                        type: string
                        example: 2026-05-21T12:00:00.000Z
                    required:
                      - id
                      - deleted_at
                    additionalProperties: false
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for DELETE /rundowns/:id/mentions/:mention_id.
        "401": *a21
        "403": *a23
        "404":
          description: Rundown or mention does not exist.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem404"
        "429": *a1
  /rundowns/{id}/text-variables:
    get:
      operationId: list_text_variables
      summary: List text variables
      description: |-
        Returns every text variable in the rundown, ordered by `key`
        (lexicographic). No pagination — counts are bounded (~100 per rundown).

        Example:
        ```
        curl -H "Authorization: Bearer $TOKEN" \
          "https://api-v1.rundownstudio.app/rundowns/YOmm4UG2SGSomuOci44c/text-variables"
        ```
      tags:
        - Text Variables
      x-mcp: true
      parameters:
        - &a25
          name: id
          in: path
          required: true
          description: Rundown id.
          schema:
            type: string
            example: YOmm4UG2SGSomuOci44c
      responses:
        "200":
          description: Text variables in `key` order.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: Returned 3 text variables
                  data:
                    type: object
                    properties:
                      items:
                        type: array
                        items:
                          $ref: "#/components/schemas/TextVariable"
                        description: "Text variables ordered by `key` (lexicographic). Soft cap: 100 per rundown."
                      count:
                        type: integer
                        minimum: -9007199254740991
                        maximum: 9007199254740991
                        example: 3
                    required:
                      - items
                      - count
                    additionalProperties: false
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for GET /rundowns/:id/text-variables. No pagination — counts are bounded (~100 per rundown).
        "401": &a26
          description: Token missing, malformed, expired, or revoked.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem401"
        "404": &a27
          description: Rundown does not exist or has been deleted.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem404"
        "429": *a1
    post:
      operationId: create_text_variable
      summary: Create one text variable
      description: |-
        Creates a new variable. Both `key` and `value` are required. Fails
        with 422 `text_variables.key_exists` if the key is already present —
        use `PATCH /text-variables/:key` to update.

        Errors:
        - 400 `text_variables.invalid_payload` — body shape rejected by validation.
        - 422 `text_variables.key_exists` — that key already exists.
        - 422 `text_variables.too_many` — would exceed the 100-variables-per-rundown cap.
      tags:
        - Text Variables
      x-mcp: true
      parameters:
        - *a25
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                key:
                  type: string
                  minLength: 1
                  maxLength: 50
                  pattern: ^[a-z0-9-]+$
                  description: Variable key. Lowercase letters, numbers, and hyphens only — referenced inline in cell content as `{{key}}`.
                  example: speaker-name
                value:
                  type: string
                  maxLength: 1000
                  description: 'Variable value. Empty string is a legitimate value — use `DELETE` (or `:bulk` with `mode: "replace"` and the key omitted) to remove a variable.'
                  example: Alice Cooper
              required:
                - key
                - value
              description: Body for `POST /rundowns/:id/text-variables`. Both fields are required. Fails with 422 `text_variables.key_exists` if the key is already present — use `PATCH /text-variables/:key` to update.
      responses:
        "201":
          description: Variable created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: Text variable created
                  data:
                    $ref: "#/components/schemas/TextVariable"
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for POST /rundowns/:id/text-variables. Status 201.
        "400":
          description: Body failed validation.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem400"
        "401": *a26
        "403": &a28
          description: Token role insufficient (EDIT required).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem403"
        "404": *a27
        "422":
          description: Key already exists or per-rundown cap exceeded.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem422"
        "429": *a1
  /rundowns/{id}/text-variables:bulk:
    post:
      operationId: bulk_write_text_variables
      summary: Update many text variables
      description: |-
        Required `mode`:
        - `"merge"` — items in `text_variables` are written; other keys are left alone.
        - `"replace"` — the full text-variable map is replaced atomically; keys absent from `text_variables` are removed.

        Both modes run inside a Firestore transaction. `replace` reads the
        current map, diffs, and applies the result in one write — concurrent
        edits race-resolve via the transaction's retry loop.

        Errors:
        - 400 `text_variables.invalid_payload` — body shape rejected by validation (includes missing `mode`).
        - 422 `text_variables.duplicate_key` — two items share the same `key`. `details.duplicates[]` carries the conflicting indexes.
        - 422 `text_variables.too_many` — the resulting map would exceed the 100-variables-per-rundown cap.
      tags:
        - Text Variables
      x-mcp: true
      parameters:
        - *a25
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                text_variables:
                  minItems: 1
                  maxItems: 100
                  type: array
                  items:
                    type: object
                    properties:
                      key:
                        type: string
                        minLength: 1
                        maxLength: 50
                        pattern: ^[a-z0-9-]+$
                        description: Variable key. Lowercase letters, numbers, and hyphens only — referenced inline in cell content as `{{key}}`.
                        example: speaker-name
                      value:
                        type: string
                        maxLength: 1000
                        description: 'Variable value. Empty string is a legitimate value — use `DELETE` (or `:bulk` with `mode: "replace"` and the key omitted) to remove a variable.'
                        example: Alice Cooper
                    required:
                      - key
                      - value
                  description: Variables to write. Atomic — the whole batch lands in one transaction or none.
                mode:
                  type: string
                  enum:
                    - merge
                    - replace
                  description: '`"merge"` — only keys in `text_variables` are written; other keys are left alone. `"replace"` — the full map is replaced atomically; keys absent from `text_variables` are deleted.'
                  example: merge
              required:
                - text_variables
                - mode
              description: "Body for `POST /rundowns/:id/text-variables:bulk`. The `mode` field is required.  - `mode: \"merge\"` — keys in `text_variables` are written; other keys are   left alone. - `mode: \"replace\"` — the rundown's entire text-variable map is replaced   with the items in `text_variables`; keys absent from the body are   removed. Atomic.  Duplicate `key` entries within one body are rejected with 422 `text_variables.duplicate_key`."
      responses:
        "200":
          description: Bulk write applied. Response carries the full map after the write.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: Wrote 5 text variables (merge)
                  data:
                    type: object
                    properties:
                      items:
                        type: array
                        items:
                          $ref: "#/components/schemas/TextVariable"
                        description: The rundown's full text-variable map after the write, ordered by `key`.
                      count:
                        type: integer
                        minimum: -9007199254740991
                        maximum: 9007199254740991
                        example: 5
                      mode:
                        type: string
                        enum:
                          - merge
                          - replace
                        description: '`"merge"` — only keys in `text_variables` are written; other keys are left alone. `"replace"` — the full map is replaced atomically; keys absent from `text_variables` are deleted.'
                        example: merge
                    required:
                      - items
                      - count
                      - mode
                    additionalProperties: false
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for POST /rundowns/:id/text-variables:bulk. `items` is the full map after the write.
        "400":
          description: Body failed validation.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem400"
        "401": *a26
        "403": *a28
        "404": *a27
        "422":
          description: Duplicate keys, or the resulting map would exceed the per-rundown cap.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem422"
        "429": *a1
  /rundowns/{id}/text-variables/{key}:
    get:
      operationId: get_text_variable
      summary: Fetch one text variable
      description: Returns the variable at `:key`. 404 if no variable has that key.
      tags:
        - Text Variables
      x-mcp: true
      parameters:
        - *a25
        - &a29
          name: key
          in: path
          required: true
          description: Text variable key.
          schema:
            type: string
            example: speaker-name
      responses:
        "200":
          description: Variable payload.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: OK
                  data:
                    $ref: "#/components/schemas/TextVariable"
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for GET /rundowns/:id/text-variables/:key.
        "401": *a26
        "404":
          description: Rundown or variable does not exist.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem404"
        "429": *a1
    patch:
      operationId: update_text_variable
      summary: Update one text variable
      description: |-
        Sets `value`. Key changes are not supported — delete + create instead.

        Errors:
        - 400 `text_variables.invalid_payload` — body shape rejected by validation.
        - 404 `text_variables.not_found` — no variable with that key.
      tags:
        - Text Variables
      x-mcp: true
      parameters:
        - *a25
        - *a29
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                value:
                  type: string
                  maxLength: 1000
                  description: 'Variable value. Empty string is a legitimate value — use `DELETE` (or `:bulk` with `mode: "replace"` and the key omitted) to remove a variable.'
                  example: Alice Cooper
              required:
                - value
              description: PATCH body for `/rundowns/:id/text-variables/:key`. Only `value` is editable; key changes require a delete + create.
      responses:
        "200":
          description: Variable updated.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: Text variable updated
                  data:
                    $ref: "#/components/schemas/TextVariable"
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for PATCH /rundowns/:id/text-variables/:key.
        "400":
          description: Body failed validation.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem400"
        "401": *a26
        "403": *a28
        "404":
          description: Rundown or variable does not exist.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem404"
        "429": *a1
    delete:
      operationId: delete_text_variable
      summary: Delete one text variable
      description: Permanently removes the variable at `:key` — immediate and irreversible; text variables have no soft-delete or restore. 404 if no variable has that key.
      tags:
        - Text Variables
      x-mcp: true
      parameters:
        - *a25
        - *a29
      responses:
        "200":
          description: Variable deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    const: true
                    example: true
                  message:
                    type: string
                    example: Text variable deleted
                  data:
                    type: object
                    properties:
                      key:
                        type: string
                        example: speaker-name
                      deleted_at:
                        type: string
                        example: 2026-05-21T12:00:00.000Z
                    required:
                      - key
                      - deleted_at
                    additionalProperties: false
                  meta:
                    $ref: "#/components/schemas/SuccessMeta"
                required:
                  - ok
                  - message
                  - data
                  - meta
                additionalProperties: false
                description: Success envelope for DELETE /rundowns/:id/text-variables/:key.
        "401": *a26
        "403": *a28
        "404":
          description: Rundown or variable does not exist.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem404"
        "429": *a1
components:
  schemas:
    Rundown:
      type: object
      properties:
        id:
          type: string
          example: YOmm4UG2SGSomuOci44c
        title:
          type: string
          example: Brave Show — June 14
        timezone:
          anyOf:
            - type: string
            - type: "null"
          example: America/New_York
        start_time:
          anyOf:
            - type: integer
              minimum: -9007199254740991
              maximum: 9007199254740991
            - type: "null"
          description: Wall-clock start, epoch milliseconds. Pair with `timezone` to render.
          example: 1750024800000
        end_time:
          anyOf:
            - type: integer
              minimum: -9007199254740991
              maximum: 9007199254740991
            - type: "null"
          description: Wall-clock end, epoch milliseconds. May be null for open-ended rundowns.
          example: 1750028400000
        status:
          type: string
          enum:
            - imported
            - draft
            - awaiting-data
            - approved
            - finalized
            - rejected
          description: Lifecycle state of the rundown. `draft` and `approved` are the live-show happy path.
          example: draft
        settings:
          type: object
          properties:
            cue_numbering:
              type: object
              properties:
                start_from:
                  type: integer
                  minimum: -9007199254740991
                  maximum: 9007199254740991
                  description: Number assigned to the first cue.
                  example: 1
                prefix:
                  type: string
                  description: Text prepended to every cue number, e.g. "A" → A1, A2.
                  example: A
                padding:
                  type: integer
                  minimum: -9007199254740991
                  maximum: 9007199254740991
                  description: Zero-pad width for the number, e.g. 2 → A01, A02.
                  example: 2
              required:
                - start_from
                - prefix
                - padding
              additionalProperties: false
              description: How cue display numbers are rendered. The cue objects carry no computed index — combine these with tree position to reproduce it.
            tod_display_format:
              anyOf:
                - type: string
                  enum:
                    - 12hNoAmPm
                    - 12h
                    - 24h
                - type: "null"
              description: Clock format for time-of-day displays. `null` = locale default.
              example: 24h
          required:
            - cue_numbering
            - tod_display_format
          additionalProperties: false
          description: Public rundown settings — a deliberate subset of internal storage. Read here; write via PATCH. Editor display preferences and live show-control state are not exposed.
        cue_order:
          type: array
          items:
            $ref: "#/components/schemas/CueOrderNode"
          description: "The run-of-show order: a hierarchical tree of cue IDs (groups carry `children`). IDs only — use `?include=cues` for full cue objects, correlating by `id`."
          example:
            - id: Ii6WeSEQmEqyKY6YAu8c
            - id: G0M48GugUIem6YOSIOyO
              children:
                - id: UEieqCc0q4wKsME0QSSm
        column_order:
          type: array
          items:
            type: string
          description: Column IDs in left-to-right display order. IDs only — use `?include=columns` for full column objects, correlating by `id`.
          example:
            - UEieqCc0q4wKsME0QSSm
            - G0M48GugUIem6YOSIOyO
        created_at:
          anyOf:
            - type: string
            - type: "null"
          example: 2026-05-01T10:00:00.000Z
        updated_at:
          anyOf:
            - type: string
            - type: "null"
          example: 2026-05-17T14:30:00.000Z
      required:
        - id
        - title
        - timezone
        - start_time
        - end_time
        - status
        - settings
        - cue_order
        - column_order
        - created_at
        - updated_at
      additionalProperties: false
      description: Default `GET /rundowns/:id` shape — top-level metadata + the cue-order tree as IDs only. Heavy content arrives via `?include=`.
    Cue:
      type: object
      properties:
        id:
          type: string
          example: Ii6WeSEQmEqyKY6YAu8c
        type:
          type: string
          enum:
            - cue
            - heading
            - group
          example: cue
        title:
          type: string
          example: Opening monologue
        subtitle:
          type: string
          example: Host walks on stage
        duration_ms:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: Duration in milliseconds. Always 0 on `heading` and `group` cues.
          example: 60000
        background_color:
          anyOf:
            - type: string
            - type: "null"
          example: "#3b0764"
        prevent_edits:
          type: boolean
          description: When true, the cue's **cells** are read-only. Cue identity, fields (title/duration/type), and position remain editable. Cell endpoints enforce this; cue endpoints surface it as a signal.
          example: false
        skipped_by:
          anyOf:
            - type: string
              enum:
                - self
                - group
            - type: "null"
          description: Why this cue is skipped during the show, or `null` if it runs. `self` — the cue's own skip setting is on. `group` — the cue itself is not skipped, but its parent group is. `self` takes precedence when both apply, so the cue's own setting is always `skipped_by === "self"` and the effective answer is `skipped_by !== null`. Read-only in v1 — not writable via PATCH.
          example: null
        start_time:
          anyOf:
            - type: integer
              minimum: -9007199254740991
              maximum: 9007199254740991
            - type: "null"
          description: Fixed wall-clock start time in epoch milliseconds. `null` for flexible cues; non-null marks the cue as a hard-start anchor.
          example: 1750024800000
        created_at:
          anyOf:
            - type: string
            - type: "null"
          example: 2026-05-01T10:00:00.000Z
        updated_at:
          anyOf:
            - type: string
            - type: "null"
          example: 2026-05-17T14:30:00.000Z
      required:
        - id
        - type
        - title
        - subtitle
        - duration_ms
        - background_color
        - prevent_edits
        - skipped_by
        - start_time
        - created_at
        - updated_at
      additionalProperties: false
      description: Public projection of a cue. `subtitle` and `background_color` mirror the dashboard; `prevent_edits` is the canonical "cells are locked" signal (replaces the legacy `locked` field).
    Column:
      type: object
      properties:
        id:
          type: string
          example: EoeiemGEmeMOY8GwUK6s
        name:
          type: string
          example: AUDIO
        created_at:
          anyOf:
            - type: string
            - type: "null"
          example: 2026-05-01T10:00:00.000Z
        updated_at:
          anyOf:
            - type: string
            - type: "null"
          example: 2026-05-12T11:20:00.000Z
      required:
        - id
        - name
        - created_at
        - updated_at
      additionalProperties: false
      description: "Public projection of a column. `type: text` in v1."
    Cell:
      type: object
      properties:
        cue_id:
          type: string
          example: Ii6WeSEQmEqyKY6YAu8c
        column_id:
          type: string
          example: EoeiemGEmeMOY8GwUK6s
        content:
          type: string
          description: Cell content as plain text — formatting stripped, mentions rendered as `@name`, variables as `$value` (the name/value as of the cell's last edit), and images kept as `![image](url)` markdown so the URL survives. The human/agent-friendly representation; read `content_html` for the live mention id / variable key.
          example: track-01
        content_html:
          type: string
          description: 'Cell content as rich-text HTML — the lossless, round-trippable representation. Write it back via `content_html` to preserve formatting, mentions, variables, and media. Standard formatting tags (`<p>`, `<strong>`, `<em>`, `<a>`, `<mark>`, `<span style="color">`, `<img src>`, lists, checkbox task lists, headings) plus three custom nodes carrying their payload in attributes: `<rs-mention data-mention-id>`, `<rs-variable data-variable-key>`, `<rs-file filename url>`. Empty for non-richtext columns.'
          example: <p>track-01</p>
        created_at:
          anyOf:
            - type: string
            - type: "null"
          example: 2026-05-01T10:00:00.000Z
        updated_at:
          anyOf:
            - type: string
            - type: "null"
          example: 2026-05-15T13:45:00.000Z
      required:
        - cue_id
        - column_id
        - content
        - content_html
        - created_at
        - updated_at
      additionalProperties: false
      description: Public projection of a cell. Addressed by the (cue_id, column_id) coordinate — no public cell id. `content` is plain text; `content_html` is the lossless raw form. A cleared or never-populated coordinate reads back as the empty cell (content `""`, null timestamps) — cells are sparse, so "no cell" and "empty cell" are the same state.
    Mention:
      type: object
      properties:
        id:
          type: string
          minLength: 1
          description: Stable mention id. Used in URLs and bulk bodies; matches `<rs-mention data-mention-id>` references in cell `content_html`.
          example: RH0Ao1j2pZu4DhccM4N3
        name:
          type: string
          example: Alice Cooper
        color:
          type: string
          example: hsla(220, 70%, 50%, 1)
        description:
          type: string
          example: Guest in segment 3
        created_at:
          anyOf:
            - type: string
            - type: "null"
          example: 2026-05-21T12:00:00.000Z
        updated_at:
          anyOf:
            - type: string
            - type: "null"
          example: 2026-05-21T12:00:00.000Z
      required:
        - id
        - name
        - color
        - description
        - created_at
        - updated_at
      additionalProperties: false
      description: Public projection of a mention. Addressed by `id` everywhere — URL paths, bulk bodies, `data-mention-id` references inside cell `content_html`.
    TextVariable:
      type: object
      properties:
        key:
          type: string
          minLength: 1
          maxLength: 50
          pattern: ^[a-z0-9-]+$
          description: Variable key. Lowercase letters, numbers, and hyphens only — referenced inline in cell content as `{{key}}`.
          example: speaker-name
        value:
          type: string
          example: Alice Cooper
      required:
        - key
        - value
      additionalProperties: false
      description: Public projection of a text variable. No timestamps — the underlying storage is a map field on the rundown doc.
    CueOrderNode:
      type: object
      properties:
        id:
          type: string
          example: Ii6WeSEQmEqyKY6YAu8c
        children:
          description: Present only on group cues; absent on flat cues/headings. Children are always leaves — nested groups are not supported.
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                example: UEieqCc0q4wKsME0QSSm
            required:
              - id
            additionalProperties: false
      required:
        - id
      additionalProperties: false
      description: Cue-order tree node. `children` is present on groups, absent otherwise. Children carry only an `id` — groups cannot nest.
    CuePlacement:
      anyOf:
        - type: string
          const: start
        - type: string
          const: end
        - type: object
          properties:
            after_cue_id:
              type: string
              minLength: 1
              example: Ii6WeSEQmEqyKY6YAu8c
          required:
            - after_cue_id
        - type: object
          properties:
            before_cue_id:
              type: string
              minLength: 1
              example: ca064OOCWK2wGIQ6KOGq
          required:
            - before_cue_id
      description: 'Where to place / move the cue. Four shapes: `"start"` — top of the rundown (root index 0). `"end"` — bottom of the rundown (default for POST when omitted). `{ "after_cue_id": "..." }` — immediately after the named cue. `{ "before_cue_id": "..." }` — immediately before the named cue.  Hierarchical placement (e.g. "first child of group G") is handled by the `:reorder` endpoint, not this descriptor.'
    ColumnPlacement:
      anyOf:
        - type: string
          const: start
        - type: string
          const: end
        - type: object
          properties:
            after_column_id:
              type: string
              minLength: 1
              example: EoeiemGEmeMOY8GwUK6s
          required:
            - after_column_id
        - type: object
          properties:
            before_column_id:
              type: string
              minLength: 1
              example: Sw0kqCqSuM8gKeICwsIa
          required:
            - before_column_id
      description: 'Where to place / move the column. Four shapes: `"start"` — first column. `"end"` — last column (default for POST when omitted). `{ "after_column_id": "..." }` — immediately after the named column. `{ "before_column_id": "..." }` — immediately before the named column.  Targets must be public column IDs — private columns are invisible to v1 and rejected with 404 `columns.not_found`.'
    ApiV1Status:
      type: object
      properties:
        server_time:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: Server wall-clock in epoch ms captured when the snapshot was assembled. Anchor for local-clock math.
          example: 1747000000000
        state:
          type: string
          enum:
            - running
            - paused
            - stopped
          description: "`running` — countdown is live; `paused` — frozen at `paused_at`; `stopped` — pre-show or post-show."
          example: running
        active_cue:
          anyOf:
            - type: object
              properties:
                id:
                  type: string
                  example: Ii6WeSEQmEqyKY6YAu8c
                title:
                  type: string
                  example: Opening monologue
                started_at:
                  type: integer
                  minimum: -9007199254740991
                  maximum: 9007199254740991
                  description: Epoch ms when the active cue's countdown effectively started. While paused, this slides on resume; clients should treat it as the anchor for `remaining = started_at + duration_ms - server_time` (running) or `… - paused_at` (paused).
                  example: 1747000000000
                paused_at:
                  anyOf:
                    - type: integer
                      minimum: -9007199254740991
                      maximum: 9007199254740991
                    - type: "null"
                  description: Epoch ms when the countdown was paused, or `null` when running.
                  example: null
                duration_ms:
                  type: integer
                  minimum: -9007199254740991
                  maximum: 9007199254740991
                  description: Active cue's current duration in milliseconds. Mirrors the cue document — `adjust-duration` writes here.
                  example: 60000
              required:
                - id
                - title
                - started_at
                - paused_at
                - duration_ms
              additionalProperties: false
              description: The currently-active cue. Always populated when `state` is `running` or `paused`; null when `stopped`.
            - type: "null"
        next_cue:
          anyOf:
            - type: object
              properties:
                id:
                  type: string
                  example: Ec0mmeOaGq68oiASGoSG
                title:
                  type: string
                  example: Guest intro
              required:
                - id
                - title
              additionalProperties: false
              description: The next cue the show will advance to. While `stopped` (pre-show), this is the first playable cue; null when the rundown has no remaining playable cues.
            - type: "null"
      required:
        - server_time
        - state
        - active_cue
        - next_cue
      additionalProperties: false
      description: Control-plane status snapshot. Same shape returned by every control action, and by the `status` read (`GET /rundowns/:id/status`). The read-live surface emits this exact shape as the fat `status` event over SSE — REST polling and the push channel share one projection.
    ApiV1OutputMessage:
      type: object
      properties:
        text:
          type: string
          description: Plain-text message. Renderers display this literally — embedded markup is shown as characters, not parsed.
          example: STAND BY
        visible:
          type: boolean
          description: Whether the message is shown on the stage display.
          example: false
        color:
          type: string
          description: Text colour as a CSS-compatible string (commonly a `#RRGGBB` hex).
          example: "#ffffff"
        bold:
          type: boolean
          example: false
        underline:
          type: boolean
          example: false
      required:
        - text
        - visible
        - color
        - bold
        - underline
      additionalProperties: false
      description: Stage-display output message — five fields, stored on the rundown, written by the show-caller during a live show. Same shape returned by both output-message endpoints (GET read, PATCH write).
    SuccessMeta:
      type: object
      properties:
        request_id:
          type: string
          description: Per-request log-correlation id. Mirrors the `X-Request-Id` response header.
          example: req_8f4c2b6d9e1a7c3b5d8e2f01
        server_time:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: Server wall-clock in epoch milliseconds, captured when the response was assembled.
          example: 1747000000000
        side_effects:
          type: array
          items:
            $ref: "#/components/schemas/SideEffect"
          description: Downstream effects caused by the request. Always present; often empty. Each entry is discriminated on `type` — see `V1SideEffect`.
          example: []
      required:
        - request_id
        - server_time
        - side_effects
      additionalProperties: false
      description: Envelope metadata block shared by every v1 success response.
    RundownResponseMeta:
      type: object
      properties:
        request_id:
          type: string
          description: Per-request log-correlation id. Mirrors the `X-Request-Id` response header.
          example: req_8f4c2b6d9e1a7c3b5d8e2f01
        server_time:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: Server wall-clock in epoch milliseconds, captured when the response was assembled.
          example: 1747000000000
        side_effects:
          type: array
          items:
            $ref: "#/components/schemas/SideEffect"
          description: Downstream effects caused by the request. Always present; often empty. Each entry is discriminated on `type` — see `V1SideEffect`.
          example: []
        applied_includes:
          type: array
          items:
            type: string
            enum:
              - cues
              - columns
              - cells
          description: Which includes the server applied. Reflects auto-implications (e.g. `cells` implies `cues` and `columns`). `[]` when none were requested.
          example:
            - cues
            - columns
      required:
        - request_id
        - server_time
        - side_effects
        - applied_includes
      additionalProperties: false
      description: Standard success meta plus `applied_includes` — the include set the server honored on an include-capable rundown response.
    SideEffect:
      oneOf:
        - type: object
          properties:
            type:
              type: string
              const: runner.timesnap
            state:
              type: string
              enum:
                - running
                - paused
                - stopped
            active_cue:
              anyOf:
                - type: object
                  properties:
                    id:
                      type: string
                      example: ca064OOCWK2wGIQ6KOGq
                    title:
                      type: string
                      example: Opening
                    started_at:
                      type: integer
                      minimum: -9007199254740991
                      maximum: 9007199254740991
                      example: 1747000000000
                    paused_at:
                      anyOf:
                        - type: integer
                          minimum: -9007199254740991
                          maximum: 9007199254740991
                        - type: "null"
                      example: null
                    duration_ms:
                      type: integer
                      minimum: -9007199254740991
                      maximum: 9007199254740991
                      example: 60000
                  required:
                    - id
                    - title
                    - started_at
                    - paused_at
                    - duration_ms
                  additionalProperties: false
                - type: "null"
            next_cue:
              anyOf:
                - type: object
                  properties:
                    id:
                      type: string
                      example: ca064OOCWK2wGIQ6KOGq
                    title:
                      type: string
                      example: Closing
                  required:
                    - id
                    - title
                  additionalProperties: false
                - type: "null"
          required:
            - type
            - state
            - active_cue
            - next_cue
          additionalProperties: false
        - type: object
          properties:
            type:
              type: string
              const: runner.active_cue_changed
            active_cue:
              anyOf:
                - type: object
                  properties:
                    id:
                      type: string
                      example: ca064OOCWK2wGIQ6KOGq
                    title:
                      type: string
                      example: Closing
                  required:
                    - id
                    - title
                  additionalProperties: false
                - type: "null"
          required:
            - type
            - active_cue
          additionalProperties: false
        - type: object
          properties:
            type:
              type: string
              const: runner.next_cue_changed
            next_cue:
              anyOf:
                - type: object
                  properties:
                    id:
                      type: string
                      example: ca064OOCWK2wGIQ6KOGq
                    title:
                      type: string
                      example: Closing
                  required:
                    - id
                    - title
                  additionalProperties: false
                - type: "null"
          required:
            - type
            - next_cue
          additionalProperties: false
        - type: object
          properties:
            type:
              type: string
              const: runner.retimed
            delta_ms:
              type: integer
              minimum: -9007199254740991
              maximum: 9007199254740991
              example: -5000
          required:
            - type
            - delta_ms
          additionalProperties: false
        - type: object
          properties:
            type:
              type: string
              const: rundown.changed
            rundown_id:
              type: string
              example: IkqMSGG2OmsIuy8uI2wI
            fields:
              type: array
              items:
                type: string
              example:
                - title
                - start_time
          required:
            - type
            - rundown_id
            - fields
          additionalProperties: false
        - type: object
          properties:
            type:
              type: string
              const: output_message.changed
            message:
              type: object
              properties:
                text:
                  type: string
                visible:
                  type: boolean
                color:
                  type: string
                bold:
                  type: boolean
                underline:
                  type: boolean
              required:
                - text
                - visible
                - color
                - bold
                - underline
              additionalProperties: false
          required:
            - type
            - message
          additionalProperties: false
        - type: object
          properties:
            type:
              type: string
              const: column.deleted
            column_id:
              type: string
              example: aAaoceeWc8OUCCGCKqEg
            name:
              type: string
              example: Notes
            cell_count:
              type: integer
              minimum: 0
              maximum: 9007199254740991
              example: 42
          required:
            - type
            - column_id
            - name
            - cell_count
          additionalProperties: false
      description: A single `meta.side_effects` entry. Discriminated on `type`. Vocabulary shared with the future WS push channel — same name, same payload shape. `server_time` lives on `meta`, not per entry.
      type: object
    Problem400:
      type: object
      properties:
        type:
          type: string
        title:
          type: string
        status:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
        code:
          type: string
        detail:
          type: string
        instance:
          type: string
        request_id:
          type: string
        server_time:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
        spec_url:
          type: string
          description: URL of the served OpenAPI spec — fetch it to discover the full set of valid routes. Present on every error.
        documentation_url:
          type: string
          description: URL of the interactive API reference (`/docs`) — where to explore every endpoint. The RFC 9457 `type` field carries the specific error's catalog entry.
        details:
          type: object
          propertyNames:
            type: string
          additionalProperties: {}
      required:
        - type
        - title
        - status
        - code
        - detail
        - instance
        - request_id
        - server_time
        - spec_url
        - documentation_url
      additionalProperties: false
      description: RFC 9457 problem+json error body (HTTP 400).  **The example below is a representative SAMPLE.** On a real 400 response the `code` and `detail` are specific to exactly what failed and `detail` is written to be actionable. For 400, expect e.g. `cursor.invalid`, `cues.invalid_payload`, `cells.invalid_query`. Codes are stable and documented at https://rundownstudio.app/docs/api-v1/errors.
      example:
        type: https://rundownstudio.app/docs/api-v1/errors#bad_request
        title: Bad Request
        status: 400
        code: bad_request
        detail: The request was malformed — a query parameter or body field failed validation.
        instance: /api-v1/rundowns/YOmm4UG2SGSomuOci44c/cues
        request_id: req_8f4c2b6d9e1a7c3b5d8e2f01
        server_time: 1747000000000
        spec_url: https://api-v1.rundownstudio.app/openapi.yml
        documentation_url: https://api-v1.rundownstudio.app/docs
        details:
          issues:
            - path:
                - field
              message: Invalid value
              code: invalid_type
    Problem401:
      type: object
      properties:
        type:
          type: string
        title:
          type: string
        status:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
        code:
          type: string
        detail:
          type: string
        instance:
          type: string
        request_id:
          type: string
        server_time:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
        spec_url:
          type: string
          description: URL of the served OpenAPI spec — fetch it to discover the full set of valid routes. Present on every error.
        documentation_url:
          type: string
          description: URL of the interactive API reference (`/docs`) — where to explore every endpoint. The RFC 9457 `type` field carries the specific error's catalog entry.
        details:
          type: object
          propertyNames:
            type: string
          additionalProperties: {}
      required:
        - type
        - title
        - status
        - code
        - detail
        - instance
        - request_id
        - server_time
        - spec_url
        - documentation_url
      additionalProperties: false
      description: RFC 9457 problem+json error body (HTTP 401).  **The example below is a representative SAMPLE.** On a real 401 response the `code` and `detail` are specific to exactly what failed and `detail` is written to be actionable. For 401, expect e.g. `auth.token_missing`, `auth.invalid_token`, `auth.token_expired`, `auth.token_revoked`. Codes are stable and documented at https://rundownstudio.app/docs/api-v1/errors.
      example:
        type: https://rundownstudio.app/docs/api-v1/errors#auth.invalid_token
        title: Unauthorized
        status: 401
        code: auth.invalid_token
        detail: The API token is missing, malformed, expired, or revoked. Mint a new token in the dashboard.
        instance: /api-v1/rundowns/YOmm4UG2SGSomuOci44c
        request_id: req_8f4c2b6d9e1a7c3b5d8e2f01
        server_time: 1747000000000
        spec_url: https://api-v1.rundownstudio.app/openapi.yml
        documentation_url: https://api-v1.rundownstudio.app/docs
    Problem403:
      type: object
      properties:
        type:
          type: string
        title:
          type: string
        status:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
        code:
          type: string
        detail:
          type: string
        instance:
          type: string
        request_id:
          type: string
        server_time:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
        spec_url:
          type: string
          description: URL of the served OpenAPI spec — fetch it to discover the full set of valid routes. Present on every error.
        documentation_url:
          type: string
          description: URL of the interactive API reference (`/docs`) — where to explore every endpoint. The RFC 9457 `type` field carries the specific error's catalog entry.
        details:
          type: object
          propertyNames:
            type: string
          additionalProperties: {}
      required:
        - type
        - title
        - status
        - code
        - detail
        - instance
        - request_id
        - server_time
        - spec_url
        - documentation_url
      additionalProperties: false
      description: RFC 9457 problem+json error body (HTTP 403).  **The example below is a representative SAMPLE.** On a real 403 response the `code` and `detail` are specific to exactly what failed and `detail` is written to be actionable. For 403, expect always `forbidden`; the `detail` names the required role (EDIT, SHOWCALL, or OWNER). Codes are stable and documented at https://rundownstudio.app/docs/api-v1/errors.
      example:
        type: https://rundownstudio.app/docs/api-v1/errors#forbidden
        title: Forbidden
        status: 403
        code: forbidden
        detail: The token's role is insufficient for this action. Mint a higher-role token (EDIT / SHOWCALL / OWNER) in the dashboard.
        instance: /api-v1/rundowns/YOmm4UG2SGSomuOci44c
        request_id: req_8f4c2b6d9e1a7c3b5d8e2f01
        server_time: 1747000000000
        spec_url: https://api-v1.rundownstudio.app/openapi.yml
        documentation_url: https://api-v1.rundownstudio.app/docs
    Problem404:
      type: object
      properties:
        type:
          type: string
        title:
          type: string
        status:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
        code:
          type: string
        detail:
          type: string
        instance:
          type: string
        request_id:
          type: string
        server_time:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
        spec_url:
          type: string
          description: URL of the served OpenAPI spec — fetch it to discover the full set of valid routes. Present on every error.
        documentation_url:
          type: string
          description: URL of the interactive API reference (`/docs`) — where to explore every endpoint. The RFC 9457 `type` field carries the specific error's catalog entry.
        details:
          type: object
          propertyNames:
            type: string
          additionalProperties: {}
      required:
        - type
        - title
        - status
        - code
        - detail
        - instance
        - request_id
        - server_time
        - spec_url
        - documentation_url
      additionalProperties: false
      description: RFC 9457 problem+json error body (HTTP 404).  **The example below is a representative SAMPLE.** On a real 404 response the `code` and `detail` are specific to exactly what failed and `detail` is written to be actionable. For 404, expect e.g. `rundowns.not_found`, `cues.not_found`, `columns.not_found`. Codes are stable and documented at https://rundownstudio.app/docs/api-v1/errors.
      example:
        type: https://rundownstudio.app/docs/api-v1/errors#not_found
        title: Not Found
        status: 404
        code: not_found
        detail: The requested resource does not exist, has been deleted, or is invisible to this token.
        instance: /api-v1/rundowns/YOmm4UG2SGSomuOci44c
        request_id: req_8f4c2b6d9e1a7c3b5d8e2f01
        server_time: 1747000000000
        spec_url: https://api-v1.rundownstudio.app/openapi.yml
        documentation_url: https://api-v1.rundownstudio.app/docs
    Problem409:
      type: object
      properties:
        type:
          type: string
        title:
          type: string
        status:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
        code:
          type: string
        detail:
          type: string
        instance:
          type: string
        request_id:
          type: string
        server_time:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
        spec_url:
          type: string
          description: URL of the served OpenAPI spec — fetch it to discover the full set of valid routes. Present on every error.
        documentation_url:
          type: string
          description: URL of the interactive API reference (`/docs`) — where to explore every endpoint. The RFC 9457 `type` field carries the specific error's catalog entry.
        details:
          type: object
          propertyNames:
            type: string
          additionalProperties: {}
      required:
        - type
        - title
        - status
        - code
        - detail
        - instance
        - request_id
        - server_time
        - spec_url
        - documentation_url
      additionalProperties: false
      description: RFC 9457 problem+json error body (HTTP 409).  **The example below is a representative SAMPLE.** On a real 409 response the `code` and `detail` are specific to exactly what failed and `detail` is written to be actionable. For 409, expect e.g. `cues.group_not_empty`, `cells.prevent_edits`, `runner.not_running`. Codes are stable and documented at https://rundownstudio.app/docs/api-v1/errors.
      example:
        type: https://rundownstudio.app/docs/api-v1/errors#conflict
        title: Conflict
        status: 409
        code: conflict
        detail: The request conflicts with the resource's current state.
        instance: /api-v1/rundowns/YOmm4UG2SGSomuOci44c/cues/myA2WwGus2YAqE4yY6qq
        request_id: req_8f4c2b6d9e1a7c3b5d8e2f01
        server_time: 1747000000000
        spec_url: https://api-v1.rundownstudio.app/openapi.yml
        documentation_url: https://api-v1.rundownstudio.app/docs
    Problem413:
      type: object
      properties:
        type:
          type: string
        title:
          type: string
        status:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
        code:
          type: string
        detail:
          type: string
        instance:
          type: string
        request_id:
          type: string
        server_time:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
        spec_url:
          type: string
          description: URL of the served OpenAPI spec — fetch it to discover the full set of valid routes. Present on every error.
        documentation_url:
          type: string
          description: URL of the interactive API reference (`/docs`) — where to explore every endpoint. The RFC 9457 `type` field carries the specific error's catalog entry.
        details:
          type: object
          propertyNames:
            type: string
          additionalProperties: {}
      required:
        - type
        - title
        - status
        - code
        - detail
        - instance
        - request_id
        - server_time
        - spec_url
        - documentation_url
      additionalProperties: false
      description: RFC 9457 problem+json error body (HTTP 413).  **The example below is a representative SAMPLE.** On a real 413 response the `code` and `detail` are specific to exactly what failed and `detail` is written to be actionable. For 413, expect always `payload.too_large`. Codes are stable and documented at https://rundownstudio.app/docs/api-v1/errors.
      example:
        type: https://rundownstudio.app/docs/api-v1/errors#payload.too_large
        title: Payload Too Large
        status: 413
        code: payload.too_large
        detail: The request or response payload exceeds the allowed size. Split it into smaller batches (use the `:bulk` endpoints) and retry.
        instance: /api-v1/rundowns/YOmm4UG2SGSomuOci44c
        request_id: req_8f4c2b6d9e1a7c3b5d8e2f01
        server_time: 1747000000000
        spec_url: https://api-v1.rundownstudio.app/openapi.yml
        documentation_url: https://api-v1.rundownstudio.app/docs
        details:
          limit: 1000
          received: 1840
          unit: cells
    Problem422:
      type: object
      properties:
        type:
          type: string
        title:
          type: string
        status:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
        code:
          type: string
        detail:
          type: string
        instance:
          type: string
        request_id:
          type: string
        server_time:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
        spec_url:
          type: string
          description: URL of the served OpenAPI spec — fetch it to discover the full set of valid routes. Present on every error.
        documentation_url:
          type: string
          description: URL of the interactive API reference (`/docs`) — where to explore every endpoint. The RFC 9457 `type` field carries the specific error's catalog entry.
        details:
          type: object
          propertyNames:
            type: string
          additionalProperties: {}
      required:
        - type
        - title
        - status
        - code
        - detail
        - instance
        - request_id
        - server_time
        - spec_url
        - documentation_url
      additionalProperties: false
      description: RFC 9457 problem+json error body (HTTP 422).  **The example below is a representative SAMPLE.** On a real 422 response the `code` and `detail` are specific to exactly what failed and `detail` is written to be actionable. For 422, expect e.g. `cues.invalid_position`, `columns.invalid_reorder`, `mentions.name_conflict`, `plan.limit_exceeded`. Codes are stable and documented at https://rundownstudio.app/docs/api-v1/errors.
      example:
        type: https://rundownstudio.app/docs/api-v1/errors#unprocessable_entity
        title: Unprocessable Entity
        status: 422
        code: unprocessable_entity
        detail: The request was well-formed but semantically rejected.
        instance: /api-v1/rundowns/YOmm4UG2SGSomuOci44c/cues
        request_id: req_8f4c2b6d9e1a7c3b5d8e2f01
        server_time: 1747000000000
        spec_url: https://api-v1.rundownstudio.app/openapi.yml
        documentation_url: https://api-v1.rundownstudio.app/docs
    Problem423:
      type: object
      properties:
        type:
          type: string
        title:
          type: string
        status:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
        code:
          type: string
        detail:
          type: string
        instance:
          type: string
        request_id:
          type: string
        server_time:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
        spec_url:
          type: string
          description: URL of the served OpenAPI spec — fetch it to discover the full set of valid routes. Present on every error.
        documentation_url:
          type: string
          description: URL of the interactive API reference (`/docs`) — where to explore every endpoint. The RFC 9457 `type` field carries the specific error's catalog entry.
        details:
          type: object
          propertyNames:
            type: string
          additionalProperties: {}
      required:
        - type
        - title
        - status
        - code
        - detail
        - instance
        - request_id
        - server_time
        - spec_url
        - documentation_url
      additionalProperties: false
      description: RFC 9457 problem+json error body (HTTP 423).  **The example below is a representative SAMPLE.** On a real 423 response the `code` and `detail` are specific to exactly what failed and `detail` is written to be actionable. For 423, expect currently always `cues.prevent_edits`. Codes are stable and documented at https://rundownstudio.app/docs/api-v1/errors.
      example:
        type: https://rundownstudio.app/docs/api-v1/errors#cues.prevent_edits
        title: Locked
        status: 423
        code: cues.prevent_edits
        detail: "The cue has `prevent_edits: true` — its contents are locked. The lock is a human-only toggle: open the rundown screen, then on the cue go Settings → Prevent Edits (checkbox) to release it."
        instance: /api-v1/rundowns/YOmm4UG2SGSomuOci44c/cues/myA2WwGus2YAqE4yY6qq
        request_id: req_8f4c2b6d9e1a7c3b5d8e2f01
        server_time: 1747000000000
        spec_url: https://api-v1.rundownstudio.app/openapi.yml
        documentation_url: https://api-v1.rundownstudio.app/docs
        details:
          cue_id: myA2WwGus2YAqE4yY6qq
          cue_title: Opening segment
    Problem429:
      type: object
      properties:
        type:
          type: string
        title:
          type: string
        status:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
        code:
          type: string
        detail:
          type: string
        instance:
          type: string
        request_id:
          type: string
        server_time:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
        spec_url:
          type: string
          description: URL of the served OpenAPI spec — fetch it to discover the full set of valid routes. Present on every error.
        documentation_url:
          type: string
          description: URL of the interactive API reference (`/docs`) — where to explore every endpoint. The RFC 9457 `type` field carries the specific error's catalog entry.
        details:
          type: object
          propertyNames:
            type: string
          additionalProperties: {}
      required:
        - type
        - title
        - status
        - code
        - detail
        - instance
        - request_id
        - server_time
        - spec_url
        - documentation_url
      additionalProperties: false
      description: RFC 9457 problem+json error body (HTTP 429).  **The example below is a representative SAMPLE.** On a real 429 response the `code` and `detail` are specific to exactly what failed and `detail` is written to be actionable. For 429, expect `rate_limit.exceeded` (request rate) or `rate_limit.too_many_connections` (too many concurrent `/events` streams); `details.bucket` names the limit tier. Codes are stable and documented at https://rundownstudio.app/docs/api-v1/errors.
      example:
        type: https://rundownstudio.app/docs/api-v1/errors#rate_limit.exceeded
        title: Too Many Requests
        status: 429
        code: rate_limit.exceeded
        detail: Your team exceeded its request quota for the current window. Wait the number of seconds in the `Retry-After` header, then retry; the `RateLimit-*` headers carry the limit, current usage, and reset time.
        instance: /api-v1/rundowns/YOmm4UG2SGSomuOci44c/cues
        request_id: req_8f4c2b6d9e1a7c3b5d8e2f01
        server_time: 1747000000000
        spec_url: https://api-v1.rundownstudio.app/openapi.yml
        documentation_url: https://api-v1.rundownstudio.app/docs
        details:
          bucket: restricted
          limit: 20
          window_seconds: 60
          retry_after_seconds: 30
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "Team-scoped API token. Mint one in the dashboard and send it as `Authorization: Bearer <token>`. Call `GET /auth` to introspect its scope, permissions, and expiry."
    tokenQuery:
      type: apiKey
      in: query
      name: token
      description: The same API token passed as a `?token=` query parameter. Accepted **only** on the control surface (`/status`, `/countdown`, `/events`, `/output-message`, and the `action:<verb>` POSTs) for production hardware that can set a URL but not a header. Every other route rejects a query token with `401`.
