Getting started

What is Rundown Studio?Create an account

Rundown

Rundown basicsColumnsTemplatesSettingsTrashCell historyMentionsText variablesRunning a showImport CSV rundown

Event

Event basicsSharing events

API

Getting startedAPI reference ↗Build a rundown from CSVWorking with cell contentLive updates over SSEAdvanced usageError referenceMigrating from v0API v0 (deprecated)

Integrations

Companion ModuleQLab

Sharing and outputs

Read-only rundownEditable rundownOutputPrompterPDF exportCSV export

Account

Your teamSubscription and invoices

Updates

Changelog
Docs API

Build a rundown from CSV

Build a rundown from CSV

A worked example — turn a spreadsheet into a fully populated rundown with one API call.

A very common integration is “I have my show in a spreadsheet, get it into Rundown Studio.” The API doesn’t parse CSV for you — instead, POST /rundowns accepts one JSON document describing the whole rundown: cues, columns, and cell contents, all nested. You (or your script, or your AI agent) transform the spreadsheet into that JSON shape and send it in a single request.

This page walks through the whole flow with a real example.

How it worked in v0

The v0 API had no import surface at all — it was a control-and-read API. CSV imports happened in the dashboard UI, or people scripted them against endpoints that were never meant for it. If that’s you, this page is your migration path; see also Migrating from v0.

The v1 shape

POST /rundowns is one endpoint with two modes: send { "title": "..." } and you get an empty rundown; send a fully nested document and you get a populated one. The same body shape, just more or less of it.

The mapping from a spreadsheet is:

  • Spreadsheet columns that are cue properties (title, duration) map to fields on each cue.
  • The other spreadsheet columns become entries in columns — each cue then carries a cells map keyed by column name (IDs don’t exist yet at import time).
  • Section-header rows become cues of type: "heading" — with no cells.

Worked example

Here’s a small show CSV — three data rows, one section header, four columns:

Title,Duration,AUDIO,NOTES
ACT 1,,,
Opening monologue,0:01:00,track-01,Host walks on stage
Guest interview,0:08:00,lav-2,Confirm name pronunciation
Outro,0:00:30,track-02,

Transformed into the POST /rundowns body:

{
  "title": "Brave Show — June 14",
  "timezone": "America/New_York",
  "start_time": 1750024800000,
  "columns": [
    { "name": "AUDIO" },
    { "name": "NOTES" }
  ],
  "cues": [
    { "type": "heading", "title": "ACT 1" },
    {
      "type": "cue",
      "title": "Opening monologue",
      "duration_ms": 60000,
      "cells": {
        "AUDIO": "track-01",
        "NOTES": "Host walks on stage"
      }
    },
    {
      "type": "cue",
      "title": "Guest interview",
      "duration_ms": 480000,
      "cells": {
        "AUDIO": "lav-2",
        "NOTES": "Confirm name pronunciation"
      }
    },
    {
      "type": "cue",
      "title": "Outro",
      "duration_ms": 30000,
      "cells": {
        "AUDIO": "track-02"
      }
    }
  ]
}

Things to notice:

  • Title and Duration became cue fields (title, duration_ms in milliseconds), not columns.
  • The ACT 1 row became a heading — no duration_ms, no cells.
  • In cells, values are plain strings keyed by column name. Empty cells are simply omitted (the Outro cue has no NOTES entry).

Send it:

curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 1f8e4a2c-7b3d-4e9f-a1c5-2d6b8e0f4a7c" \
  -d @rundown.json \
  https://api-v1.rundownstudio.app/rundowns

The response

You get 201 Created with the full rundown echoed back — every cue, column, and cell now carries its server-assigned ID, ready for follow-up edits:

{
  "ok": true,
  "message": "Rundown created",
  "data": {
    "id": "YOmm4UG2SGSomuOci44c",
    "title": "Brave Show — June 14",
    "timezone": "America/New_York",
    "start_time": 1750024800000,
    "status": "draft",
    "cue_order": [
      { "id": "Ii6WeSEQmEqyKY6YAu8c" },
      { "id": "G0M48GugUIem6YOSIOyO" },
      { "id": "UEieqCc0q4wKsME0QSSm" },
      { "id": "Ec0mmeOaGq68oiASGoSG" }
    ],
    "column_order": ["EoeiemGEmeMOY8GwUK6s", "Sw0kqCqSuM8gKeICwsIa"],
    "created_at": "2026-06-14T10:00:00.000Z",
    "updated_at": "2026-06-14T10:00:00.000Z"
  },
  "included": {
    "cues": [
      { "id": "Ii6WeSEQmEqyKY6YAu8c", "type": "heading", "title": "ACT 1" },
      { "id": "G0M48GugUIem6YOSIOyO", "type": "cue", "title": "Opening monologue", "duration_ms": 60000 }
    ],
    "columns": [
      { "id": "EoeiemGEmeMOY8GwUK6s", "name": "AUDIO" },
      { "id": "Sw0kqCqSuM8gKeICwsIa", "name": "NOTES" }
    ],
    "cells": [
      {
        "cue_id": "G0M48GugUIem6YOSIOyO",
        "column_id": "EoeiemGEmeMOY8GwUK6s",
        "content": "track-01",
        "content_html": "<p>track-01</p>"
      }
    ]
  },
  "meta": {
    "request_id": "req_8f4c2b6d9e1a7c3b5d8e2f01",
    "server_time": 1750024801000,
    "side_effects": []
  }
}

(Abbreviated — the real response carries every cue, column, and cell, each with full audit fields.)

  • data is the shallow rundown; cue_order and column_order give you the running order.
  • included carries the created cues, columns, and cells with their IDs — store these if you plan follow-up edits; cells are addressed by (cue_id, column_id) and have no ID of their own.
  • meta.side_effects is [] on creates; it gets populated on later mutations (e.g. editing a live cue’s duration re-times the runner).

The create is best-effort atomic: if any part of the document fails validation, nothing is created and the error’s details.issues[] points at the offending field (e.g. cues[2].cells.AUDIO). Fix one thing, retry — with the same Idempotency-Key this is always safe.

Common gotchas

  • No cells on heading or group cues. Section-header rows must map to type: "heading" (or "group" with children) and carry no cells field — getting this wrong is rejected with rundowns.invalid_payload.
  • Column names must be unique within columns[]. Spreadsheets happily allow duplicate column headers; the API rejects them.
  • text is the only writable column type. Existing dropdown / image / attachment columns can be read but not written through the API yet.
  • Durations are milliseconds. 0:01:00 in the spreadsheet is 60000 on the wire.
  • Always send an Idempotency-Key on the create. A POST /rundowns repeated with the same key within 24 hours is replayed from cache instead of creating a duplicate — re-running your import script after a network error is safe.
  • Caps: at most 500 cues per create request and 1,000 per rundown (group children count), 50 columns, 1 MB body. A bigger show is created shell-first, then padded with POST /rundowns/:id/cues:bulk batches.

One more call: start the show

The created rundown is immediately controllable. Start it:

curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  https://api-v1.rundownstudio.app/rundowns/YOmm4UG2SGSomuOci44c/action:start

The response is a status snapshot — the same shape GET /rundowns/:id/status returns — with the first cue running.

Where to go next

  • API reference — the full POST /rundowns schema with every optional field (settings, cue numbering, groups).
  • Working with cell content — when plain strings aren’t enough and you want formatting in cells.
  • Error reference — what every validation error means.