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:
| Reference | Example |
|---|---|
| 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 poolerMerges
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.