Write data
Every write goes through one function, db.transact, and it either lands completely or not at all (Ramose calls a write a transaction — glossary). This page is for anyone who has defined a schema and wants to create, change and delete records; Reef’s own writes are the examples.
The four verbs
Section titled “The four verbs”The entire write vocabulary — no update, no merge:
| call | does |
|---|---|
tx.entity() | hands back a handle for a brand-new record (glossary) |
tx.add(e, field, value) | states a fact — this record’s field has this value (glossary) |
tx.retract(e, field, value?) | takes one value back, or every value of that field |
tx.retractEntity(e) | deletes the record — every fact about it, and any records it owns |
The handle from tx.entity() carries the same three verbs without the first argument: issue.add(…), issue.retract(…), issue.retractEntity(). Values are checked against the schema as you type.
Create a record
Section titled “Create a record”Reef’s “New issue” dialog is one write; everything inside lands together or not at all:
export const createIssue = ( db: ReefDb, myEid: number, lastRankInColumn: number | undefined, draft: NewIssue,) => db.transact(function* (tx) { const issue = yield* tx.entity(); yield* issue.add(Issue.title, draft.title); // … yield* issue.add(Issue.status, draft.status); yield* issue.add(Issue.priority, draft.priority); yield* issue.add(Issue.rank, rankAfter(lastRankInColumn)); yield* issue.add(Issue.createdAt, new Date()); yield* issue.add(Issue.creator, myEid); // … for (const labelId of draft.labelIds ?? []) { yield* issue.add(Issue.labels, labelId); } });A reference field takes the other record’s id as a number: issue.add(Issue.creator, myEid) (glossary).
Change fields
Section titled “Change fields”Dragging a card in Reef is one write of two facts — new column, new position:
export const moveIssue = ( db: ReefDb, issueId: number, status: Status, rank: number,) => db.transact(function* (tx) { yield* tx.add(issueId, Issue.status, status); yield* tx.add(issueId, Issue.rank, rank); });A single-value field replaces its old value (glossary): adding Issue.status again takes the old status back in the same write. You never write that step.
src/app/mutations.ts:113-122 Remove a value, delete a record
Section titled “Remove a value, delete a record”Clearing a field is retract with no value; deleting a record is retractEntity:
export const setDescription = (db: ReefDb, issueId: number, text: string) => db.transact(function* (tx) { if (text === "") yield* tx.retract(issueId, Issue.description); else yield* tx.add(issueId, Issue.description, text); });export const deleteIssue = (db: ReefDb, issueId: number) => db.transact(function* (tx) { yield* tx.retractEntity(issueId); });Neither erases history — Time travel can still show the issue as it was.
Many-valued fields
Section titled “Many-valued fields”A field declared cardinality: "many" is a set. One add per value; retract with the value takes one out:
export const toggleLabel = ( db: ReefDb, issueId: number, labelId: number, on: boolean,) => db.transact(function* (tx) { if (on) yield* tx.add(issueId, Issue.labels, labelId); else yield* tx.retract(issueId, Issue.labels, labelId); });Ids, unique keys and lookups
Section titled “Ids, unique keys and lookups”The first argument of tx.add names the record. It can be:
- an id as a number. A query that selects
id: Issue.idgives yourow.idas anumber— pass it straight in (that isissueIdabove). A query with no.selectreturns{ id }objects — passrow.idthen too. - a handle from
tx.entity()in the same write. - a lookup by unique key —
[User.sub, "user_ada"]names the user whosesubis that value, no id in hand (glossary).
What a write returns
Section titled “What a write returns”transact resolves with { t, txEid, datomCount, dbAfter }:
t— the version number the database is at after your write (glossary).txEid— the write’s own record id.datomCount— how many facts landed.dbAfter— the same database, pinned att. Read through it and you see your own write with no second round trip.
A new record’s id is not in the report. Query for it — Reef does that when you first enter a workspace, reading its own write through dbAfter (mineQuery selects { id: User.id } where sub is yours):
const report = yield* db.transact(function* (tx) { const user = yield* tx.entity(); yield* user.add(User.sub, me.id); yield* user.add(User.name, me.name); yield* user.add(User.email, me.email); }); const after = yield* report.dbAfter.q(mineQuery); return after[0]?.id;A write also moves every open screen forward. A live query re-runs itself when the database changes (glossary); after your write, every open one re-runs at t. Nothing in your UI has to announce the change.
When a write is refused
Section titled “When a write is refused”Unauthorized(HTTP 403) — the permission check said no before anything reached the writer, the one thing per database that commits writes in order (glossary). It carriescode: "policy"and the field that tripped:retract denied on :issue/status.TxRejected(HTTP 409) — the writer refused it: a schema violation, a unique-key conflict, or a denial the first check missed. No version number is spent.
Both are values you can match by name — see Errors. Writes to one database apply one at a time, so there are no conflicts to retry, only refusals to handle.
In React, useTransact runs the write and hands you the refusal:
// Every write is one `run(...)`; a policy denial (or any DbError) becomes // a toast — enforcement is server-side, the UI is only a hint. const { run } = useTransact({ onError: (error) => toast("error", errorMessage(error)), });