The room as a store — your app's data, sealed with the conversation
A Space can hold your app's data, not only its conversation: rows in named collections, sealed like everything else, ordered by the server without reading them, and readable by the assistant. An app can keep all its state in its rooms, with no database of its own.
Part of the SDK · every call: the reference.
Declare and write
Declare the collections when you make the room, or later with configure.
Give each field one line: what it is, and the rule for it. Those lines go into the assistant's tool descriptions on
every turn and are read at the moment it fills the field, so a rule about a field belongs on the field; who the
assistant is belongs in its instructions.
const space = await wb.createSpace({
agent: { name: 'Planner', instructions: 'Keep my week honest.' },
data: {
tasks: { description: 'things to do', fields: { title: 'short text', due: 'ISO date — only if I said one', done: '0 or 1' } },
projects: { fields: { name: 'short text' } },
},
})
await space.data.put('tasks', { title: 'Buy milk', due: '2026-09-22', done: 0 })
await space.data.put('tasks', { title: 'Buy oat milk', done: 1 }, { id: 't1' }) // with an id: replace that row
await space.data.delete('tasks', 't2')
space.data.rows('tasks') // [{ id, title, due, done, _updated, _by, _v }, …]
space.data.get('tasks', 't1')
space.on('data', ({ ops, from, self }) => rerender()) // every write, yours or the assistant'sspace.data.batch(ops) writes several rows as one entry. A write answers { ok: true, id, ops } and throws on a
refusal.
The store is the room's history. Every write is one entry in the room — ▦ Dana saved tasks “Buy milk” — so every
member sees it, two devices writing different rows never collide, and any device holding the key rebuilds the store by
reading the room. There is no second copy to drift. A row is at most 16 KB.
What the assistant gets, in a room that declares collections:
| Tool | Runs | What it does |
|---|---|---|
data_query |
in the turn, at once | SQL over one table, rows(collection, id, row JSON, updated, by) — SELECT … WHERE json_extract(row,'$.due') < ? — so it answers "what is due this week" without being handed everything. Read-only, enforced by the engine; at most 200 rows. A query that runs longer than a few seconds is stopped, and the assistant is told to narrow it. |
data_put |
proposes | Add or replace one row. It is written when a member approves, never during the turn. |
data_delete |
proposes | Delete one row by id, the same way. |
dataWrites: false lets the assistant read the store without proposing writes (builder mode).
id and every field name starting with _ are the room's own: a write that sets one is refused (reserved_field) —
what they carry is in who did what.
Row schemas
Field descriptions guide the assistant; a schema is enforced. Give a collection schema — JSON Schema for one row —
and the room checks every write against it, so a row that does not fit is never stored:
const space = await wb.createSpace({
agent: { name: 'Ops' },
data: {
tasks: {
description: 'things to do',
schema: {
type: 'object',
properties: {
title: { type: 'string', minLength: 1, maxLength: 80, description: 'what to do' },
due: { type: 'string', format: 'date' },
priority: { enum: ['low', 'normal', 'high'] },
estimate: { type: 'number', minimum: 0, maximum: 40, description: 'hours' },
},
required: ['title'],
additionalProperties: false,
},
},
},
})
try { await space.data.put('tasks', { title: 'Call', priority: 'urgent' }) }
catch (e) { e.code === 'invalid_row' /* tasks: priority must be one of "low", "normal", "high" */ }- Your writes: a row that does not fit is refused with
invalid_row, naming the field. In abatch, one bad row refuses the whole batch. - The assistant's writes: a
data_putthat does not fit is never proposed — the assistant is told why and tries again, so no member is asked to approve a row that would fail. A card is checked again when it is approved, in case the schema changed meanwhile. - The assistant reads the rules: each field's type, allowed values,
requiredanddescriptionappear in its data tools. A schema's descriptions take the place offields. - Supported:
type,properties,required,additionalProperties(true/false),enum,const,minLength/maxLength,minimum/maximum/exclusiveMinimum/exclusiveMaximum,items,minItems/maxItems, andformat(date,date-time,email,uri);description,title,defaultandexamplesare kept as notes. Anything else —patterntoo, since our servers do not run regular expressions from a room's settings — is refused when you declare it (invalid_schema), never silently ignored. Up to 8 KB a schema, nested 6 levels. - New writes only. Changing a schema with
configure({ data })does not touch the rows already stored, and rows an older client's move carried are not checked again. Confidential rooms run the same checks.
Conditional writes
Two devices editing the same row is the one case a room's history cannot settle by itself — the last write would
win — so a write can be made conditional. Every row carries _v, the id of the entry that last wrote it; pass it back,
and the write is refused if the row has changed since:
const t = space.data.get('tasks', 't1')
try {
await space.data.put('tasks', { ...t, done: 1 }, { id: 't1', ifVersion: t._v })
} catch (e) {
if (e.code === 'row_changed') { /* e.conflicts: [{ collection, id, version }] — read again, merge, retry */ }
else throw e
}
await space.data.put('tasks', { title: 'first' }, { id: 't9', ifVersion: null }) // only if t9 does not exist yet
await space.data.delete('tasks', 't1', { ifVersion: t._v })- The check and the write are one step on the server: it reads, compares, and appends only if the room has not grown
in between, retrying under contention, so the losing device gets a clean
409 row_changedand never a silent overwrite. snapshot()keeps_v, so a conditional write after a resume compares against the right version.- Without
ifVersion, a write is unconditional.
Collections only admins write
Declare a collection write: 'admins' and only the room's admins may write it — see admins.
The limits on collections and fields: Reference.
Machine-readable source: sdk-store.md · Generated 2026-10-27T07:05:42Z · build f301388c · every doc in one fetch: llms-full.txt (HTML)