Collections
A collection is a table with typed columns. Each row is a JSON object, a document, checked against the columns. People edit it as a sheet; agents use tools.
Column types: text, number, bool, date (YYYY-MM-DD or RFC 3339) and json.
opf coll.create name=expenses columns:='[{"name":"date","type":"date"},{"name":"amount","type":"number"},{"name":"note","type":"text"}]'opf coll.add collection=expenses row:='{"date":"2026-10-03","amount":12.5,"note":"taxi"}'opf coll.add collection=expenses rows:='[{…},{…}]' # up to 500 at onceopf coll.query collection=expenses where:='{"note":"taxi"}' search=hotel sort=-amount limit=50opf coll.update collection=expenses id=<ID> set:='{"amount":13}' if_version=2opf coll.delete collection=expenses id=<ID>- Validation: unknown columns and wrong types are rejected with the list of valid columns, which catches typos.
- Partial updates:
coll.updatechanges only the given fields;nullclears a field. - Versions: every row has one;
if_versiongives a conflict instead of overwriting. - Changing columns:
coll.alterreplaces the column list. Values of removed columns stay stored and come back if the column returns. - Dropping a collection:
coll.dropdeletes the table and all rows. It defaults toaskfor agents. - Reacting to changes: use
events.wait module=coll subject_prefix=expenses/.
The sheet
Section titled “The sheet”Click a cell to edit. Enter saves and Esc cancels; clicking elsewhere saves if you changed something. Bool cells toggle with one click. The input row at the bottom adds a row. You can sort by any column, search, and page through 200 rows at a time. Changes by agents appear within seconds; refreshing pauses while you edit.
Computed columns
Section titled “Computed columns”A column with a formula is computed: a JavaScript expression over the row, evaluated every time rows are read, so it’s never stale. In the column editor, write name:type = formula:
price:numberqty:numbertotal:number:Total = row.price * row.qtybig:bool = row.total >= 100net:number = round(row.total / 1.19, 2)wait:number = row.due ? days(today(), row.due) : null- References: a formula sees the row’s fields as
row.name, including computed columns to its left, so formulas can build on each other. - Helpers:
round(x, digits),days(from, to)andtoday(), plus all of JavaScript’sMath. - Uncertainty:
rate_ci(k, n)gives a share with its 95 % interval ({rate, lo, hi, n}, Wilson),count_ci(k)a count with its 95 % interval ({count, lo, hi}, Poisson). Small samples then show how little they say:rate_ci(4, 16)is 25 %, but anywhere from 10 to 49 %. Example:conv:text = rate_ci(row.won, row.trials).lo >= 0.2 ? "good" : "unclear" - Read-only: computed columns can’t be written;
coll.addandcoll.updatereject them. - Queries: filters, search and sort work on computed values like any other column.
- Errors: a failing formula shows
#ERRin that cell (hover for the reason). The API returns the reason in the row’serrors; the query itself still succeeds. - Safety: formulas run in an isolated JavaScript interpreter with no access to anything but the row, under a time limit of 2 seconds per read. A formula that hits the limit is paused for a minute. Fixing it takes effect immediately.
Via the API, a computed column is a column with formula:
opf coll.alter name=orders columns:='[…, {"name":"total","type":"number","formula":"row.price * row.qty"}]'To act on changes (send a message, update another table) or to run something on a schedule, use functions.