Skip to main content
Version: 0.3.36

Branches

Create, fork, compare and merge branches over the WebSocket client, and scope any operation to a branch with onBranch(). A branch forked from another is an isolated copy of both schema and content.

Typical uses: give an AI agent its own branch and merge the result back when it is done, stage schema or content changes before promoting them to main, or run parallel experiments without touching production data. See Agent Memory with Branches for the agent pattern.

Scope an operation to a branch​

onBranch(name) returns a branch-scoped Database (or workspace client). Every read, write and SQL statement made through it targets that branch; the default is main.

const db = client.database('myapp');

const staging = db.onBranch('staging');
await staging.workspace('content').nodes().create({
type: 'raisin:Page',
path: '/articles/draft',
properties: { title: 'Draft' },
});
await staging.executeSql("SELECT path, properties->>'title' AS title FROM 'content'");
// main is untouched until you merge

db.branches()​

Returns a Branches handle for branch lifecycle operations.

create​

await db.branches().create(name, options?);
ArgumentTypeDescription
namestringNew branch name
options.fromBranchstring?Fork from this branch: copies its nodes, indexes and schema (NodeTypes, archetypes, element types). It also becomes the branch's default comparison base
options.fromRevisionstring?Fork at this revision of fromBranch instead of its head

Without either option the branch is created empty. Resolves to the branch record:

await db.branches().create('staging', { fromBranch: 'main' });
// { name: 'staging', head: '1788719933425-0', created_at: '...', created_by: 'system',
// created_from: '1788719933425-0', upstream_branch: 'main', protected: false, description: null }

list / get / delete​

const all = await db.branches().list();      // Branch[]
const branch = await db.branches().get('staging');
await db.branches().delete('staging'); // { success: true }

getHead / updateHead​

const head = await db.branches().getHead('main');   // { revision: '1788719933425-0' }
await db.branches().updateHead('staging', '1788719765785-0'); // { success: true }

updateHead moves the head to any revision, forward or back, and is the rollback primitive: reads on the branch then return the state at that revision while newer revisions stay in history.

compare​

Commits ahead and behind, plus the common ancestor.

const divergence = await db.branches().compare('staging', 'main');
// { ahead: 1, behind: 0, common_ancestor: '1788719933425-0' }

diff​

The nodes that changed on branch since it diverged from baseBranch.

const diff = await db.branches().diff('staging', 'main');
// { common_ancestor: '1788719933425-0',
// added: [{ node_id: '...', workspace: 'content', path: '/sdk-page', operation: 'added' }],
// modified: [],
// deleted: [] }

operation is added, modified, reordered or deleted.

merge​

Merge a source branch into a target branch. Defaults to a three-way merge.

// merge staging INTO main
const result = await db.branches().merge('staging', 'main', {
strategy: 'ThreeWay', // or 'FastForward'
message: 'Merge staging',
});
// { success: true, revision: 1788720003593, conflicts: [], fast_forward: false, nodes_changed: 1 }
ArgumentTypeDescription
sourceBranchstringBranch to merge from
targetBranchstringBranch to merge into
options.strategystring?'ThreeWay' (default) or 'FastForward'
options.messagestring?Merge commit message

revision is the millisecond part of the merge commit; call getHead for the full string. When a three-way merge finds conflicts, success is false and conflicts holds one entry per node with node_id, conflict_type, base_properties, target_properties and source_properties. Resolve them through the HTTP resolve-merge endpoint described in Merging Changes. A fast-forward that is not possible rejects with Fast-forward merge not possible: branches have diverged.

copyNodes​

Copy a set of nodes from one branch onto another: branch promotion. Unlike a merge, which takes a whole branch, this takes named roots, so it suits a model where one branch holds work in progress and another holds what is live.

await db.branches().copyNodes('staging', 'main', {
workspace: 'content',
roots: ['/products/kettle'],
recursive: true,
});
// { copied: 1, deleted: 0, revision: '1788720003641-0',
// changes: [{ node_id: '...', path: '/products/kettle', node_type: 'raisin:Page', operation: 'added' }] }

Node ids are preserved, so promoting the same root again updates the same target nodes rather than creating new ones, and references to a promoted node keep resolving. The whole set lands in one commit and the target head advances once. A promotion carries the whole node: properties, archetype, indexes, relations, branch-scoped secrets and translation overlays. Each root's parent path must already exist on the target branch; a missing one fails the call.

ArgumentTypeDescription
sourceBranchstringBranch to copy from
targetBranchstringBranch to copy onto
options.workspacestringWorkspace the nodes live in
options.rootsstring[]Node paths to copy (non-empty)
options.recursiveboolean?Also copy descendants (default true)
options.deleteMissingboolean?Prune target nodes under the roots that are absent from the copied set (default false)
options.sourceRevisionstring?Read the source as of this revision instead of its head

Pinning a promotion to a revision​

Copying a large set takes time. Without a pin the source is read at head as each node's turn comes, so a write landing mid-copy ends up partly inside the result. Capture the head when the promotion is decided and pass it back:

const at = await db.branches().getHead('staging');

// later, after a review step, while other people keep working
await db.branches().copyNodes('staging', 'main', {
workspace: 'content',
roots: ['/products'],
recursive: true,
sourceRevision: at.revision,
});

The pin says what to copy; the target branch is always written at its head. An unparseable revision is an error rather than a fallback to the head. Requires server 0.3.15 or later.

db.tags()​

A tag is an immutable name for one revision.

const head = await db.branches().getHead('main');
await db.tags().create('v1.0', head.revision, 'First release');
// { name: 'v1.0', revision: '1788719933425-0', created_at: '...', created_by: 'system',
// message: 'First release', protected: false }
await db.tags().list();
await db.tags().get('v1.0');
await db.tags().delete('v1.0'); // { success: true }

To read content at a tag, use its revision in a SQL __revision predicate or the HTTP rev/{revision} endpoints.

Node history​

A node's revisions, newest first, are available on any workspace client:

const history = await db.onBranch('staging').workspace('content').nodes().historyByPath('/sdk-page');
// [{ revision: '1788720003556-0', updated_at: '...', updated_by: 'system', deleted: false,
// message: 'Created node: b7ed40a1-...', is_system: false }]

See Node Operations for history() and historyByPath().

note

Branch merge, compare, diff and copyNodes require a server running the RocksDB storage backend (the default).