Tables
Store your application's rows in versioned Tables that live in your Project's bucket.
A Table is where your application keeps its data: one row per camera frame, robot episode, or log record, with columns for metadata, embeddings, and media. The Table's files live in your Project's bucket, and your application reads and writes them directly through the SDK. Every write publishes a new version, so a job that pinned last week's version keeps reading exactly those rows while new data lands.
What a Table holds
A Table has a name, a schema, and rows. The running example in these docs is
frames, a Table of camera frames from a robot:
frame_id | camera | captured_at | image | embedding |
|---|---|---|---|---|
f-000184 | front | 2026-10-12 09:14:03.120 | JPEG bytes | [0.12, -0.40, …] |
f-000185 | left | 2026-10-12 09:14:03.120 | JPEG bytes | [0.08, 0.31, …] |
A column can hold a number, a string, a timestamp, binary data, a list, a struct, or a fixed-size vector, so a frame's metadata, its image, and its embedding sit in the same row. Each field inside a struct or list is stored as its own column; see Structs and lists.
Tables do not enforce primary keys or unique constraints. Keep your own
identifier, such as frame_id, in an ordinary column. Duplicate values are
allowed.
Every write is a new version
A Table version never changes after it is written. An append, an update, a schema change, or a new Table writes new files and then publishes a new version in a commit:
frames
v1 create, 10,000 frames
v2 append 25,000 frames
v3 append 18,000 framesA reader always sees one complete version, never a half-finished write.
Readers that pinned v2 keep reading v2 after v3 lands, which is what
makes a training run or an evaluation repeatable. To pin a whole Project at
once, select a commit or a tag; see
Select an exact state.
Concurrent writers do not lose data. Each commit names the version that it started from. If another writer published first, the commit reports a conflict and publishes nothing, and you repeat the write from the new head.
What you can do
| You want to | Use |
|---|---|
| Create a Table from Arrow batches | Create a Table |
| Add, update, merge, or delete rows | Write rows |
| Read rows as Arrow batches, or query with SQL | Read rows |
| Add, rename, or drop columns without rewriting data | Change the schema |
| Give existing rows a new column of values | Add columns |
| Read only the rows that changed since an earlier version | Read changes |
| Keep scans fast after many small appends | Maintain a Table |
Add a derived value, such as is_blurry, to every row | Enrichments |
| Find the rows with the nearest embeddings | Vector Indexes |
How Tables are stored
Tables use a storage format built for data that grows by appending and gains new columns over time. Files are never edited: an append adds new files, and an update or a delete adds a small change record. Every row has a hidden row ID that stays the same when maintenance reorganizes the files. How Tables are stored explains what this means for cost, pruning, and the files in your bucket.
Where it runs
Your application writes and reads Table files directly in Project storage, with short-lived storage access for one Table. Spiral's control plane checks permissions, allocates storage, and publishes versions; the rows never pass through it. See Architecture.
The Tables HTTP reference documents the storage and version operations.
HTTP API
The HTTP reference documents the contract at
/api/tables. It uses camelCase fields, tagged union payloads, and decimal
strings for 64-bit counts and timestamps. Existing Rust and CLI clients keep
using the compatible resource endpoints.