Skip to content

How to

Read and write records

Get, list, create, update, and delete records, with version checks, dry runs, bulk data, and change streams.

apicmd/xdb/cli

Read this when
  • You write a script or an agent that changes records
  • A write fails with CONFLICT or SCHEMA_VIOLATION
  • You move many records at once

Go, JSON-RPC, and the CLI send reads and writes to the same store. The store applies the same validation and versioning to each client. The examples use the CLI and the xdb://com.example/posts schema from Get started.

clients share a store

Read

Read one record by its URI. --fields selects the fields in the output.

Terminal window
xdb records get xdb://com.example/posts/p-1 --fields title,views

List the records that match a CEL filter. --limit sets the page size. The default limit is 20.

Terminal window
xdb records list xdb://com.example/posts --filter 'views > 10' --fields _id,title --limit 10

With -o json or -o yaml, a list returns a page:

{ "items": [{ "_id": "p-1", "title": "Hello" }], "total": 1 }

If there are more records, the page also has next_offset. To get the next page, give that value to --offset. To get all the pages, pass --page-all. -o ndjson writes one record on each line.

On SQLite, the store compiles the filter to SQL. On the other backends, the store scans the records and filters them. Filters lists the operators.

Write

CommandResult
records createCreates the record. If the record exists with the same data, the command succeeds. If the data is different, it fails with CONFLICT.
records updateChanges only the fields in the payload
records upsertReplaces the record
records delete --forceDeletes the record
Terminal window
xdb records update xdb://com.example/posts/p-1 --json '{"views":43}'
xdb records upsert xdb://com.example/posts/p-1 --json '{"title":"Hi"}'
xdb records delete xdb://com.example/posts/p-1 --force

To delete a schema and its records, add --cascade:

Terminal window
xdb schemas delete xdb://com.example/posts --force --cascade

CAUTION: --cascade deletes each record in the schema.

Check the version before a write

Each record has a _version that starts at 1. Each write increments it and returns the new version. If an update includes a _version that is not 0, the store writes only when the stored version is the same:

Terminal window
xdb records update xdb://com.example/posts/p-1 --json '{"_version":1,"views":43}'

For a delete, give the version with --if-version:

Terminal window
xdb records delete xdb://com.example/posts/p-1 --force --if-version 7
{
"code": "CONFLICT",
"message": "records.delete xdb://com.example/posts/p-1: record is at version 1, not 7: [xdb/core] revision conflict [expected=7, got=1, fix=re-read the record and retry with the current _version]",
"resource": "records",
"action": "delete",
"uri": "xdb://com.example/posts/p-1",
"hint": "re-read the record and retry with the current _version",
"details": { "expected": "7", "got": "1" }
}

If you get CONFLICT, read the record again. Then send the write with the current version.

version checks on transactional backends

Memory and SQLite do the check and the write in one transaction. On the filesystem and Redis backends, another write can occur between the check and the write. Versioning gives the full contract.

Validate a write without writing

--dry-run validates the payload against the schema and writes nothing:

Terminal window
xdb records update xdb://com.example/posts/p-1 --json '{"views":"lots"}' --dry-run
{
"code": "SCHEMA_VIOLATION",
"message": "[xdb/core] schema violation: field \"views\": cannot decode as INTEGER [expected=INTEGER, got=STRING, reason=decode_failed, field=views]",
"resource": "records",
"action": "update",
"uri": "xdb://com.example/posts/p-1",
"hint": "run xdb describe --uri <schema-uri> to inspect the schema",
"details": { "expected": "INTEGER", "field": "views", "got": "STRING", "reason": "decode_failed" }
}

If the payload is valid, the result shows the record that the write would store:

{
"dry_run": true,
"valid": true,
"would": "update",
"record": {
"_id": "p-1",
"_ns": "com.example",
"_schema": "posts",
"_updated": "2026-09-11T12:39:22Z",
"_version": 1,
"title": "Hello",
"views": 43
}
}

Move many records

Export the records of a schema as NDJSON. Then import the file:

Terminal window
xdb export --uri xdb://com.example/posts > posts.ndjson
xdb import --uri xdb://com.example/posts -f posts.ndjson
{ "imported": 1, "skipped": 0, "failed": 0 }

Import upserts each record. With --create-only, import skips the records that exist with different data.

xdb batch applies a stream of operations. Each line of NDJSON is one operation:

Terminal window
xdb batch - < ops.ndjson
{"op":"records.upsert","uri":"xdb://com.example/posts/p-2","data":{"title":"Hi"}}

The operations are records.create, records.update, records.upsert, records.delete, schemas.create, schemas.update, and schemas.delete. A batch is atomic on the sqlite and memory backends. On the fs and redis backends, pass --non-atomic.

Watch changes

xdb watch streams the changes to a namespace, a schema, or a record as NDJSON:

Terminal window
xdb watch xdb://com.example/posts
{"ts":"...","type":"record.create","uri":"xdb://com.example/posts/p-2","version":1}
{"ts":"...","type":"record.update","uri":"xdb://com.example/posts/p-1","version":4}

Each record event includes the version. A delete event has the last version before the delete. If the versions of one record have a gap, the stream missed a write. Delivery is at most once, and the stream does not replay old events.