Omnigraph
Branching & history

Branches, Commits, and History

A branch is an isolated, durable graph history. Use branches to prepare and

A branch is an isolated, durable graph history. Use branches to prepare and review a multi-step change without exposing intermediate results on main.

Branch workflow

# Create an isolated branch from main.
omnigraph branch create review/add-benchmark --from main --target ./graph.omni

# Write and inspect it.
omnigraph load --data benchmark.jsonl --mode append \
  --branch review/add-benchmark --target ./graph.omni
omnigraph query sources_for_claim --query queries.gq \
  --params '{"claim":"lower-latency"}' \
  --branch review/add-benchmark --target ./graph.omni

# Publish its result to main, then remove the source branch.
omnigraph branch merge review/add-benchmark --into main --delete-branch \
  --target ./graph.omni

List and delete branches with:

omnigraph branch list --target ./graph.omni
omnigraph branch delete review/abandoned --target ./graph.omni

Creating a branch defaults to main when --from is omitted. To start a branch at a past commit instead of a branch head, use --from-commit; see start a branch at a commit. A load can create a missing target branch by combining --branch <name> with --from <base>.

Each branch operation is also a GQ statement, so a client that already sends .gq source needs no second transport. The three control writes go to POST /mutate, and the listing and the branch reads to POST /query, each with no request target, name, or parameters:

branch create "review/add-benchmark" from main
branch create "fix/import" from commit "<commit-id>"
branch merge "review/add-benchmark" into main
branch delete "review/abandoned"
branch list
branch diff "review/add-benchmark" into main
branch merge-base "review/add-benchmark" into main
branch merge "review/add-benchmark" into main dry run

from defaults to main and into defaults to main. A name outside the identifier alphabet (a lowercase letter or _, then letters, digits, or _), such as one containing /, -, or ., is quoted, as above; main, b0, and _x are bare. A control write answers a ChangeOutput whose outcome.kind is created, deleted, or merged (outcome.merge holds the merge result), with zero affected counts; a merge that publishes a commit reports it in commit. branch list answers one row per branch in column name, sorted by name. Each statement runs under the same policy check as its HTTP route (POST /branches, DELETE /branches/{branch_name}, POST /branches/merge, GET /branches), and branch list is a read, so it is refused on POST /mutate as a control write is refused on POST /query. The branch reads branch diff, branch merge-base and branch merge … dry run answer rows as branch list does and take their routes' checks; see reviewing a branch and previewing a merge. A bare dry run after the names is the clause, so a branch named dry is quoted.

Branches are cheap until written: unchanged data remains shared with the source. A branch remains after a normal merge, so prefer --delete-branch or delete it when review is complete. Live branches retain the history they depend on and can prevent omnigraph cleanup from reclaiming old data.

Branch names

Branch names may contain /, but live names must not be path prefixes of one another. For example, review and review/alice cannot coexist. main is reserved. A parent branch can be deleted while descendants remain. A path segment may not end in . followed by 26 upper-case letters and digits: that shape is reserved for OmniGraph's internal per-life branch identity. Ordinary dotted names such as release.1.2 are fine.

A branch name is used exactly as written. A name that is empty, has leading or trailing whitespace, or contains a control character is refused wherever a branch is named: the branch a read or write targets, from, merge sources and targets, branch create and delete, Blob writes, change feeds and branch statements. The server answers 400 Bad Request before authorization, the embedded engine returns a bad-request error before any effect, and the CLI reports either error. Names are never trimmed, so main does not name main.

Deleting a branch and creating another with the same name yields a fresh branch lifetime. Readers that captured the deleted branch fail with a typed error rather than seeing the replacement. Deletion retires the logical name and leaves native manifest history and table storage for explicit omnigraph cleanup; it starts no background table-reclaim work. Cleanup preserves data still needed by other branches and their underlying histories.

Branch-control operations are safe across handles in one writer process. Do not run branch create/delete control concurrently from separate writer processes against the same graph.

Atomicity model

OmniGraph does not provide connection-scoped BEGIN and ROLLBACK.

ScopeGuarantee
One mutation queryAll statements publish as one commit or none become visible.
One load requestThe complete batch publishes as one commit or none becomes visible.
Several commands on a branchEach command that publishes a change is a durable branch commit. Earlier commands are not rolled back if a later one fails.
Branch mergeThe resulting source state becomes visible on the target in one atomic commit.

Deleting an abandoned branch discards that workspace from normal access, but it is an explicit lifecycle action rather than transaction rollback.

Commits

Every write that publishes a change records a graph commit. A successful mutation that matches no entities publishes no commit. A commit includes its id, parent, branch, actor when known, and timestamp. Merge commits have two parents.

omnigraph commit list --target ./graph.omni --branch main
omnigraph commit show <commit-id> --target ./graph.omni

commit list is newest first. Omitting --branch shows history reachable from main; selecting a branch includes the history inherited at its fork plus its own commits.

A long history can be read a page at a time:

omnigraph commit list --target ./graph.omni --branch main --limit 50 --json
omnigraph commit list --target ./graph.omni --branch main --page-token <token> --json

--limit (1 to 1,000) returns one page with head_commit_id, the head the listing resolved, and next_page_token while older commits remain. Pass the token back with the same --branch for the next page, of 100 commits unless --limit sets the size. Later pages answer for that head even after the branch moves, so a page never repeats or skips a commit. A page reads only the history it returns; without --limit or --page-token the whole history is read and listed at once. Served under a policy, a caller whose read does not cover every branch continues only from a commit of the branch's history, and is refused with 400 otherwise: a commit the branch still holds, or one a branch the caller may read published, costs nothing more, and any other is looked for down the branch's history, which costs what the whole listing costs. Served, the same listing is GET /commits?listing=history&branch=<name>&limit=<n> continued with page_token=<token>; omitting listing also selects history. Paging applies to a branch's history, not to --unpublished or --unattached.

HTTP listing accepts history, unpublished or unattached; non-history listings refuse limit and page_token. The retired query keys unpublished and unattached return 400 bad_request even when false or combined with listing; omit them or use listing=history for ordinary history.

Start a branch at a commit

A branch can start at a published graph commit instead of a branch's head. It holds that commit's rows and history, as if it had been created while the commit was the head. Use it to try a fix or a revert where a change was made, then merge the branch as usual; the commit is its merge base.

omnigraph commit list --target ./graph.omni --branch main --json
omnigraph branch create fix/import --from-commit <commit-id> --target ./graph.omni

The statement is branch create "fix/import" from commit "<commit-id>", and POST /branches takes the id in from_commit, which excludes from (a request with both answers 400). An id no commit has answers 404. The receipt names the branch the new branch was forked off in from, for a commit start the branch that wrote the commit, and the commit it starts at in from_commit_id. Every receipt carries from_commit_id: for a start from a branch it is that branch's head when the branch was created. A branch starts only at a commit whose whole snapshot is still there under the graph's current schema; otherwise nothing is created, and the error's code is branch_start_refusal, with action: fix:

from_commit namesAnswerbranch_start_refusal.reason
A commit whose table versions cleanup reclaimed, wholly or in a run that stopped part way410not_retained, with the type in type_name
A commit written under an earlier schema than the graph's409schema_changed
A commit written before the storage upgrade409earlier_storage_format
A commit of a branch that was deleted, even if a branch has since been created under its name409branch_deleted, with the branch in branch
An unpublished commit409unpublished_commit; publish it, then start at the published commit
An unattached snapshot409unattached_snapshot; it never becomes branch history

While the new branch lives, cleanup keeps the commit's table versions, so a later start at the same commit succeeds too.

Unpublished commits

A mutation or a merge written as commit records its result as an unpublished commit: the branch head is unchanged, the result is readable at the commit's id (--snapshot <id>), and the commit lists with published: false.

query propose_bob() as commit {
    insert Person { name: "bob", age: 41 }
}

branch merge feature into main as commit

commit publish "<commit-id>"
commit drop "<commit-id>"

commit publish installs the commit on the branch it recorded as its target. The published commit keeps the actor and timestamp of the write that created it; the publish records no publisher. A published commit is answered as it stands by a repeat publish and is never dropped. An unpublished commit expires once an ordinary write moves its target branch's head past the commit's parent, or when that branch is deleted or recreated, and can then only be dropped: a publish is refused (a moved head fails its precondition), reads at its id (--snapshot, commit show, commit changes) answer not found, and it leaves the --unpublished listing.

omnigraph branch merge feature --into main --as-commit --target ./graph.omni
omnigraph commit list --target ./graph.omni --branch main --unpublished
omnigraph commit publish <commit-id> --target ./graph.omni
omnigraph commit drop <commit-id> --target ./graph.omni

Served, the same control writes are POST /commits/{commit_id}/publish (the published commit), DELETE /commits/{commit_id} (204), and GET /commits?listing=unpublished&branch=<name>; POST /branches/merge takes as_commit, which excludes delete_branch. Publishing and dropping run under the commit_publish and commit_drop policy actions on the recorded target branch (Policy).

Unattached branches

A mutation written as unattached branch records its result as an unattached snapshot: the branch head is unchanged, the result is readable at the snapshot's id (--snapshot <id>), and the snapshot never publishes. A second write on "<id>" (or on $base with a String parameter) builds on that snapshot and returns a new id; the caller keeps the latest id as its tip. The snapshot's branch is the one the first write ran on (main when the request names none); a write on a snapshot runs on that recorded branch, and a request that names another branch is refused. Two writes on one base each get their own snapshot and see nothing of each other's.

query add_person($name: String) as unattached branch {
    insert Person { name: $name }
}

query add_address($id: String, $base: String) as unattached branch on $base {
    insert Address { owner: $id, city: "Delft" }
}

A write that changes nothing returns commit: null and creates no snapshot; the caller keeps its previous tip. The receipt's commit carries kind: "unattached", published: false and root_commit_id, the first snapshot of the chain (its own id on that first snapshot); parent_commit_id is the base snapshot, or the branch head the first write ran on. A base must be a live unattached snapshot: a published commit or an unpublished commit is refused.

omnigraph commit list --target ./graph.omni --branch main --unattached
omnigraph query people --query queries.gq --snapshot <snapshot-id> --target ./graph.omni
omnigraph commit drop <snapshot-id> --target ./graph.omni

commit list --unattached shows the branch's live snapshots, newest first, flat; parent_commit_id tells the chains apart. commit drop of a snapshot also drops every snapshot built on it; dropping the first one discards the whole chain, and a snapshot built on another chain is untouched. commit publish of an unattached snapshot is refused with HTTP 409 and the error code unattached_cannot_publish; nothing is written. Every snapshot of a chain expires when an ordinary write moves the branch head past the head the first write captured, or when the branch is deleted or recreated: expired snapshots answer not found to reads and writes and leave the listing. Dropping or expiring a snapshot promises no storage reclamation; cleanup reports what it retains.

Served, the listing is GET /commits?listing=unattached&branch=<name> and the drop is the same DELETE /commits/{commit_id} (204). Writing needs change on the recorded branch; listing and reading a snapshot need read on it (Policy).

Historical reads

Every read targets either a live branch or an immutable graph commit. Take a graph_commit_id from commit list --json and pass it as the snapshot:

omnigraph query sources_for_claim --query queries.gq \
  --params '{"claim":"lower-latency"}' \
  --snapshot <graph-commit-id> --target ./graph.omni

A query stays on one snapshot for its entire lifetime. Wildcard traversal is refused on an explicit historical target because historical schema membership is not reconstructed. Explicit edge alternatives retain named-edge historical read rules.

A historical query is written against the current schema. A type renamed since the commit still reads under its current name, but a property reads only where the commit stores that same property under its current name. A property added since the commit, renamed since, or dropped and re-added with the same name is refused at that commit, naming the property, rather than answering with null or with the dropped property's values. A whole-node return { $p } reads every current property, so it is refused at commits made before a property was added. Table data written before v0.10 records no property identity, so a commit that reads such data refuses its properties too, unless that data is still the table's current version.

Historical reads can eventually fail after destructive cleanup removes the versions that commit needs. Branch deletion can likewise end access to branch-only history.

Changes and feeds

Inspect the logical changes made by one commit, compare two points, or review what a branch changed since it left main:

omnigraph commit changes <commit-id> --target ./graph.omni --json
omnigraph diff --from <commit-id> --to main --target ./graph.omni --json
omnigraph branch diff feature --target ./graph.omni --json

For continuous consumption, omnigraph changes poll follows complete commits on one branch and returns an opaque resume cursor. If cleanup has reclaimed required history, create a coherent snapshot and new cursor with omnigraph changes baseline.

See Changes, Diffs and Change Feeds for filters, pagination, cursor checkpointing, and 410 retention-gap recovery.

See Merging Branches for merge outcomes and conflict handling.

On this page