Skip to Content
Gnomon Docs
Getting started

Concepts

Five ideas carry the whole system: tracked tables, coordinates, branches, merges and runs.

Tracked tables

select gnomon.track('orders') puts a tracked table under version control. Gnomon generates storage beside the table and a trigger on it, so every insert, update and delete records a version: the row's key, which columns changed, who changed them and the exact position in the write stream.

Tracking copies nothing and backfills nothing. It takes a brief lock while the objects are created, and history starts from that moment. The table itself is untouched — same name, same columns, same indexes, same plans for the queries you already run.

Reads and writes of a tracked table on a branch go through a routing view in gnomon_r_<schema>, which Gnomon keeps in step with the table. On main, nothing is in the way.

Coordinates

A coordinate is an exact position in the database's history: a snapshot, written as text, that names precisely which transactions had committed. select gnomon.coordinate_text() returns the one the current statement read.

Every read in the past takes a reference, and Gnomon resolves it to a coordinate:

ReferenceExample
A coordinate'e1:880:882:881@0/1A2B3C4'
A timestamp'2026-10-31 23:59', resolved through clock ticks
A tag'tag:before-migration'

A timestamp is approximate — it resolves to the latest tick at or before it, so it is as precise as your heartbeat. A coordinate is exact: reading at one gives the same rows every time, however much has changed since. That is what makes an agent's reads replayable.

Branches

A branch is a line of its own. It starts from a coordinate on its parent and keeps only what it changes:

  • Rows it hasn't touched are read from main, live, with no copy.
  • Rows it has written live in the branch's own storage.
  • Rows that changed on main after the branch started are rebuilt from history, so the branch keeps seeing the database as of its own starting point.

So a branch opens in about a millisecond and costs what it writes, whether the database holds a thousand rows or a billion. Branches nest: a branch of a branch sees its whole lineage. Scratch branches are unlogged and swept automatically, which suits tests; logged branches survive a restart and can be reviewed and merged.

Choosing a branch is a session setting, so your application doesn't change:

select gnomon.use('feature');     -- this session
select gnomon.begin('feature');   -- this transaction only, behind a pooler

Working on a branch →

Merges

A merge is three-way, like a merge in Git: Gnomon compares the branch against the base — the state at the fork — and against the target now, field by field.

  • Only the branch changed a field: the branch's value lands.
  • Only main changed it: main's value stays.
  • Both changed it: the column's strategy decides.

Strategies are per column (gnomon.set_merge_strategy). counter adds the two deltas, so two sales of the last item are two sales. jsonb merges key by key, set unions, text3 merges text line by line, and conflict — the default — refuses to guess. Anything a strategy can't settle becomes a row in gnomon.conflict for a person or a query to resolve.

Merges run into main or into another branch, a whole branch or one transaction (gnomon.cherry_pick). Undo uses the same engine, and refuses when a later change would be overwritten.

Merges → · Review → · History →

Runs

A run is a task with a branch of its own. The SDKs create one per unit of agent work, with limits already set: it is discarded unless it is merged or submitted for review, it can't read another run's branch even when runs share a database login, and every statement it makes is recorded with the moment it read.

await using run = await gnomon.startRun({ task: "Refund order 4182", actor: "support-bot" });
await run.sql("update orders set status = 'refunded' where id = 4182");
if ((await run.summary()).checks.passed) await run.merge();

Agents → · Runs and the SDKs →

What Postgres enforces

None of this depends on an agent, an ORM or an application behaving well. A confined role's writes are checked inside the database: it can write only the branches in its access list, it cannot merge into main without gnomon_merger, and a schema-qualified name does not get it past the routing views. An auto-merge policy's rules are evaluated by Postgres at merge time, whatever the agent was told to do.

Security →

On this page