# Worked Examples

Seven situations tether is built for, run end to end as one story. Each section starts where the previous one left the dataset, and ends with `jj log` and a pointer to the page that explains the mechanics. The dataset is an Icechunk store `zarr/imaging`, a Lance table `features`, a directory of raw files `raw/plate1`, and the analysis code repository `code`. (The Neon database of the other pages is left out, so the story runs locally.)

| Section | Situation |
|----|----|
| [1](#reproduce-an-analysis-months-later) | Reproduce an analysis months later |
| [2](#reprocess-on-a-branch-then-land-it-or-throw-it-away) | Reprocess on a branch, then land it or throw it away |
| [3](#ab-two-candidates-keep-one) | A/B: two candidates, keep one |
| [4](#catch-drift-nightly-without-a-watcher) | Catch drift nightly without a watcher |
| [5](#keep-the-data-bill-down) | Keep the data bill down |
| [6](#recover) | Recover from mistakes |
| [7](#publish-history-to-a-registry-experimental) | Publish history to a registry (experimental) |

> **Note: Executed, not written**
>
> `tests/test_use_cases.py` runs every command on this page against local copies of the objects (and an ephemeral Postgres for section 7). It writes the command and output blocks you see (`user_guide/_generated/06/`), and CI fails when a fresh run differs. Ids that change per run are replaced with stable stand-ins, so the same id means the same thing everywhere on this page, and the operation log runs on the story's clock.

`jj log` uses a compact template set in the examples' jj config: change id, commit id, bookmarks, other workspaces' working copies, `(empty)`, and the description. `@` is this checkout's working copy, `◆` the root. Under git, read a bookmark as a branch and `@` as `HEAD`.


# 0. Set up

``` bash
jj git init analysis && cd analysis
```

    Initialized repo in "analysis"

``` bash
tether init
```

    initialized tether dataset at ~/analysis (dataset id 0a1b2c3d: its pins and working branches are tether.0a1b2c3d.* / tether.ws.0a1b2c3d.*)

``` bash
tether add zarr/imaging --kind icechunk ~/data/imaging.icechunk --branch main
tether add features --kind lance ~/data/features.lance
tether add raw/plate1 --kind file ~/data/raw/plate1
tether add code --kind git ~/analysis-code
```

    added zarr/imaging (icechunk)
    added features (lance)
    added raw/plate1 (file)
    added code (git)

Nothing has been contacted yet. The VCS sees a few small TOML files (the config and one manifest per object), and a `main` bookmark `init` put on the working copy: the trunk, which stands for every object's own `main`.

``` bash
jj status
```

    Working copy changes:
    A .tether/.gitignore
    A .tether/objects/code.toml
    A .tether/objects/features.toml
    A .tether/objects/raw/plate1.toml
    A .tether/objects/zarr/imaging.toml
    A tether.toml
    Working copy  (@) : wttkmnpn 8049f3df main | (no description set)
    Parent commit (@-): zzzzzzzz 00000000 (empty) (no description set)

`.tether/workspace.toml` and `.tether/ops.jsonl` are not listed: they are per-checkout state, ignored by the `.gitignore` that `init` wrote.


# 1. Reproduce an analysis months later

*The report is out. Someone asks how a number was computed. The code is in git; the data it read has moved on three times since.*

Commit the inputs next to the code that read them:

``` bash
tether commit -m "Inputs for the Q3 report"
```

      pinned code -> tether.0a1b2c3d.cd0006c590b9435c
      pinned features -> tether.0a1b2c3d.ee78ce6240ad74c9
      pinned zarr/imaging -> tether.0a1b2c3d.83df68a53a600331
      recorded raw/plate1 (not recoverable)
    committed 695fe286e11b

That commit holds the manifests - one file per object naming the pin - and sits in the same history as `make_report.py`. Months later, from any clone, the commit id is enough:

``` bash
tether open zarr/imaging --rev 695fe286e11b
```

    ~/data/imaging.icechunk#tether.0a1b2c3d.83df68a53a600331

``` python
from tether import Repo

repo = Repo.find(".")
h = repo.open("zarr/imaging", rev="695fe286e11b")   # read-only, at the pinned snapshot
```

`TETHER_REV=695fe286e11b python make_report.py` does the same for a whole script: every `repo.open()` in it resolves at that commit's pins. And in CI, so you find out the day a pin stops resolving rather than the day you need it:

``` bash
tether verify --all-history
```

            ok  e960d896ec67:code  
            ok  e960d896ec67:features  
            ok  e960d896ec67:zarr/imaging  
            ok  695fe286e11b:code  
            ok  695fe286e11b:features  
            ok  695fe286e11b:zarr/imaging

(Under jj, `--all-history` includes the empty working-copy commit on top of the one you just made; it holds the same manifests, so it verifies the same pins.)

What tether guaranteed: the Icechunk, Lance and git tags are native refs their systems will not expire or collect. The raw prefix was *recorded* (size, count, etag digest), and [verify](../reference/ObjectStatus.md#tether.ObjectStatus.verify) says whether it still matches, but files cannot be pinned; the commit said so.

This commit was made on the trunk bookmark, `main`, where each object's working ref is its own `main`: the inputs are whatever those branches held. The commit moved the bookmark with it, and jj opened a fresh empty working copy on top, as `jj commit` would:

``` bash
jj log
```

    @  mqzmqkrw e960d896ec67 (empty) (no description set)
    ○  wttkmnpn 695fe286e11b main Inputs for the Q3 report
    ◆  zzzzzzzz 000000000000 (empty) (no description set)

Mechanics: [Pinning](pinning.md).


# 2. Reprocess on a branch, then land it or throw it away

*A new model should relabel the imaging store and rewrite the features table. Production readers must not see a half-done state, and the attempt may fail.*

``` bash
tether new -b relabel-v3
```

    on bookmark relabel-v3; working refs set up
      code -> tether.ws.0a1b2c3d.relabel-v3  (created on first writable open)
      features -> tether.ws.0a1b2c3d.relabel-v3  (created on first writable open)
      zarr/imaging -> tether.ws.0a1b2c3d.relabel-v3  (created on first writable open)

A `relabel-v3` bookmark now sits where `main` is, and every system will get a branch of the same name. Nothing was created yet: the job opens what it writes, and the branch for that object appears in its system at that moment, forked from the pin:

``` python
import lance
import pyarrow as pa
import zarr
from tether import Repo

repo = Repo.find(".")
h = repo.open("zarr/imaging", read_only=False)  # creates the Icechunk branch
root = zarr.open_group(store=h.session.store, mode="a")
root["labels"][:] = 3  # model v3's labels
h.session.commit("labels from model v3")

f = repo.open("features", read_only=False)  # and the Lance branch
areas = pa.table({"cell_id": [1, 2, 3], "area": [112.0, 96.0, 128.5]})
lance.write_dataset(areas, f.dataset, mode="append")
```

`main` - the bookmark and every system's branch - is untouched throughout. When the job is done, commit pins the branches and moves `relabel-v3`. Only the two objects the job wrote get new pins; `code` and `raw/plate1` are as the last commit left them:

``` bash
tether commit -m "Relabel plate1 with model v3"
```

      pinned features -> tether.0a1b2c3d.9f8c161504d93a19
      pinned zarr/imaging -> tether.0a1b2c3d.c440b8efabd2b427
    committed bae316fbb4cc

``` bash
tether diff main --content
```

        changed  features  [+1 -0 ~0 fragments]
                      added  fragment 1  +3 rows
        changed  zarr/imaging  [+0 -0 ~1 nodes]
                   modified  /labels  4 chunks

``` bash
jj log
```

    @  ooskrvsm 658ec0b2dd6c (empty) (no description set)
    ○  mqzmqkrw bae316fbb4cc relabel-v3 Relabel plate1 with model v3
    ○  wttkmnpn 695fe286e11b main Inputs for the Q3 report
    ◆  zzzzzzzz 000000000000 (empty) (no description set)

The bookmark moved; `main` did not. Two endings. Land it:

``` bash
tether promote
```

      skipped        code
      skipped        raw/plate1
      refused        features: the lance backend cannot move a branch; Lance cannot move a branch head; write the result onto the base branch (or keep reading the fork) -- there is no branch merge or fast-forward yet
      held           zarr/imaging: would fast-forward: main snapshot_id=363DSWSOMCFUBPNP… -> snapshot_id=RJL5IZZDMBO5QO75… (base unchanged since fork); held because features refused, and a bookmark is planned whole or not at all
    nothing moved: a bookmark is planned whole or not at all; name keys to land a subset, or finish the refused systems on the trunk and promote again

Nothing moved. Lance refuses, since it cannot move one branch onto another, and says what to do instead: write the result onto the base branch. Icechunk *could* have fast-forwarded, and [promote](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.promote) says so, but holds it: new labels beside old features on `main` is the half-done state this exercise exists to avoid ([planned whole or not at all](branching-and-writing.md#landing-it-tether-promote)). The two objects the job never opened are skipped.

Landing it by hand is then two steps. Name the system that can move, the explicit way to land a subset (the trunk bookmark does not move for one):

``` bash
tether promote zarr/imaging
```

    fast-forwarded zarr/imaging -> {'snapshot_id': 'RJL5IZZDMBO5QO75LEAH'}

Then finish the Lance half on the trunk and commit there. Pins are content-addressed, so the imaging store, now at what `relabel-v3` pinned, gets the very same pin id, and only `features` gets a new one:

``` bash
tether new main
```

    on trunk bookmark main; working refs set up
      code -> main
      features -> main
      zarr/imaging -> main

``` python
import lance
import pyarrow as pa
from tether import Repo

repo = Repo.find(".")
f = repo.open("features", read_only=False)  # on the trunk: Lance's own main
areas = pa.table({"cell_id": [1, 2, 3], "area": [112.0, 96.0, 128.5]})
lance.write_dataset(areas, f.dataset, mode="append")
```

``` bash
tether commit -m "Features rebuilt for model v3"
```

      pinned features -> tether.0a1b2c3d.e04adf84a168acd1
      pinned zarr/imaging -> tether.0a1b2c3d.c440b8efabd2b427
    committed e1fc77e40112

``` bash
jj log
```

    @  pozttxqm 2c47c77396a0 (empty) (no description set)
    ○  toqmypkw e1fc77e40112 main Features rebuilt for model v3
    │ ○  mqzmqkrw bae316fbb4cc relabel-v3 Relabel plate1 with model v3
    ├─╯
    ○  wttkmnpn 695fe286e11b Inputs for the Q3 report
    ◆  zzzzzzzz 000000000000 (empty) (no description set)

Two lines of history describe the same data now, and the bookmark has done its job. `drop` throws it away. It is a plan first: the commits only this bookmark reaches, the bookmark, and what `gc` will release once they are gone:

``` bash
tether drop relabel-v3
```

    plan: drop (5 write(s))
          abandon-commit                bae316fbb4cc  Relabel plate1 with model v3
         delete-bookmark                relabel-v3  jj: goes with its commits; git: the branch
                   unpin  features      [lance] tether.0a1b2c3d.9f8c161504d93a19  no manifest in history references it
           delete-branch  features      [lance] tether.ws.0a1b2c3d.relabel-v3  bookmark relabel-v3 (gone); head was committed by a dropped commit
           delete-branch  zarr/imaging  [icechunk] tether.ws.0a1b2c3d.relabel-v3  bookmark relabel-v3 (gone); head is pinned
                          keeping live bookmarks: main
                          store actions are what gc will plan once the commits are gone; they are re-planned at apply
    (not applied; `--no-dry-run` drops it)

``` bash
tether drop relabel-v3 --no-dry-run
```

    dropped relabel-v3: 1 commit(s) (bae316fbb4cc)
    unpinned 1 pin(s)
      lance: 0a1b2c3d.9f8c161504d93a19
    deleted 2 working branch(es)
      features: tether.ws.0a1b2c3d.relabel-v3
      zarr/imaging: tether.ws.0a1b2c3d.relabel-v3

The one pin nothing else names, the Lance version on the branch, is released (the imaging pin is also `main`'s). Both branches go: Icechunk's head is `main`'s pin, and Lance's head is a state the dropped commit pinned. That last rule is [`drop`'s own](branching-and-writing.md#throwing-it-away-tether-drop): `gc --prune-bookmarks` would keep the branch as "unpinned writes".

``` bash
jj log
```

    @  pozttxqm 2c47c77396a0 (empty) (no description set)
    ○  toqmypkw e1fc77e40112 main Features rebuilt for model v3
    ○  wttkmnpn 695fe286e11b Inputs for the Q3 report
    ◆  zzzzzzzz 000000000000 (empty) (no description set)

The other ending, a failed attempt, is the same `drop` without the trunk commit in between. Run from the bookmark itself, it leaves for the trunk first (`--to` picks another bookmark). `drop` is not undoable by tether, but [three steps](branching-and-writing.md#throwing-it-away-tether-drop) bring a bookmark back.

> **Note: Rehearsing a database migration**
>
> The same flow rehearses a schema change on a Neon database. Run `tether new -b migration`, apply the migration at the Neon branch's URL, run the tests, and watch the row counts move with `tether diff main --content`. Neon cannot promote a child branch, so landing it is the trunk recipe above, or the migration applied to `main` itself: the branch was the rehearsal.

Mechanics: [Branching and Writing](branching-and-writing.md#landing-it-tether-promote).


# 3. A/B: two candidates, keep one

*Two labeling approaches. Run both against the same inputs, compare, promote the winner, and leave nothing of the loser behind.*

Each candidate is a bookmark, `a` and `b`, and each bookmark is a branch of the same name in every system, so the two never share a branch. Run them side by side from two checkouts (a jj workspace or git worktree each): **A** works in `analysis/`, **B** in `analysis-b/`.

**Set up.** B's checkout is created from A's; each starts its own bookmark. From A:

``` bash
jj workspace add ../analysis-b
```

    Created workspace in "../analysis-b"
    Working copy  (@) now at: nrlrltqp 89866d49 (empty) (no description set)
    Parent commit (@-)      : toqmypkw e1fc77e4 main | Features rebuilt for model v3
    Added 7 files, modified 0 files, removed 0 files

``` bash
tether new -b a
```

    on bookmark a; working refs set up
      code -> tether.ws.0a1b2c3d.a  (created on first writable open)
      features -> tether.ws.0a1b2c3d.a  (created on first writable open)
      zarr/imaging -> tether.ws.0a1b2c3d.a  (created on first writable open)

And in B:

``` bash
cd ../analysis-b
tether new -b b
```

    on bookmark b; working refs set up
      code -> tether.ws.0a1b2c3d.b  (created on first writable open)
      features -> tether.ws.0a1b2c3d.b  (created on first writable open)
      zarr/imaging -> tether.ws.0a1b2c3d.b  (created on first writable open)

**Run and commit.** Each job labels the imaging store through its own handle. Each [commit](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.commit) pins its bookmark's branch, lands a dataset commit, and moves the bookmark onto it. A:

``` python
import zarr
from tether import Repo

repo = Repo.find(".")
h = repo.open("zarr/imaging", read_only=False)
root = zarr.open_group(store=h.session.store, mode="a")
root["labels"][:] = 7
h.session.commit("labels: candidate A")
```

``` bash
tether commit -m "Labels: candidate A"
```

      pinned zarr/imaging -> tether.0a1b2c3d.4f2ff54c238bea8f
    committed 785a58aa9c09

B:

``` python
import zarr
from tether import Repo

repo = Repo.find(".")
h = repo.open("zarr/imaging", read_only=False)
root = zarr.open_group(store=h.session.store, mode="a")
root["labels"][:] = 9
h.session.commit("labels: candidate B")
```

``` bash
tether commit -m "Labels: candidate B"
```

      pinned zarr/imaging -> tether.0a1b2c3d.4d04e2df7f555238
    committed 1775ea300681

Two bookmarks, two lines of history, one fork point - and the same shape in the Icechunk store, where `tether.ws.0a1b2c3d.a` and `tether.ws.0a1b2c3d.b` both branch off the snapshot `main` pins. From A, B's working copy shows as `analysis-b@`:

``` bash
jj log
```

    @  povkylmx 753abba76924 (empty) (no description set)
    ○  pozttxqm 785a58aa9c09 a Labels: candidate A
    │ ○  nsuwpqlp f9c0b0a439fa analysis-b@ (empty) (no description set)
    │ ○  nrlrltqp 1775ea300681 b Labels: candidate B
    ├─╯
    ○  toqmypkw e1fc77e40112 main Features rebuilt for model v3
    ○  wttkmnpn 695fe286e11b Inputs for the Q3 report
    ◆  zzzzzzzz 000000000000 (empty) (no description set)

**Compare**, from either checkout, as datasets rather than directories. Icechunk cannot diff one branch head into another directly, so tether diffs each against the snapshot they diverged from:

``` bash
tether diff a b --content
```

        changed  zarr/imaging  [+0 -0 ~1 nodes; diverged at snapshot RJL5IZZDMBO5QO75LEAH; changes on either side]
                   modified  /labels  8 chunks

**Land A.** One command from A. [promote](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.promote) fast-forwards Icechunk's `main` onto `a`'s branch and skips the objects A never opened. Everything having landed, it moves the `main` bookmark onto A's dataset commit.

``` bash
tether promote
```

      fast-forwarded zarr/imaging -> {'snapshot_id': 'CVQNZW3XBIOVWIPOLW4M'}
      skipped        code
      skipped        features
      skipped        raw/plate1
    main -> 785a58aa9c09

B is untouched: its bookmark and branches are its own. If B's checkout later moves onto the new `main` (`jj new main`), `tether status` says the working copy left bookmark `b`, and `tether new b` returns to it.

**Retire B** - from A's checkout, since B's is about to stop being one. Forget B's checkout first: `drop` refuses a bookmark another live checkout is working on, and that is the guard you want. Then drop the bookmark:

``` bash
tether forget-workspace e4077898
```

    forgot workspace e4077898
      removed  ~/analysis-b/.tether/workspace.toml
      removed  ~/analysis-b/.tether/ops.jsonl
      vcs      jj workspace 'analysis-b' forgotten (its directory is left in place)

``` bash
tether drop b --no-dry-run
```

    dropped b: 1 commit(s) (1775ea300681)
    unpinned 1 pin(s)
      icechunk: 0a1b2c3d.4d04e2df7f555238
    deleted 1 working branch(es)
      zarr/imaging: tether.ws.0a1b2c3d.b

Nothing is left to run in `analysis-b/`, and the directory can be deleted (`git worktree remove` already did, for git). B's Icechunk branch went with the commit that pinned its head, by `drop`'s own rule.

``` bash
jj log
```

    @  povkylmx 753abba76924 (empty) (no description set)
    ○  pozttxqm 785a58aa9c09 a main Labels: candidate A
    ○  toqmypkw e1fc77e40112 Features rebuilt for model v3
    ○  wttkmnpn 695fe286e11b Inputs for the Q3 report
    ◆  zzzzzzzz 000000000000 (empty) (no description set)

B is gone from history, its pin from the store, and its branch from Icechunk. `a` and `main` point at the same commit; `jj bookmark delete a` and a later `gc --prune-bookmarks` retire the winner's branches the same way once nobody works on them.

Mechanics: [Workspaces and bookmarks](branching-and-writing.md#workspaces-and-bookmarks), [`drop`](branching-and-writing.md#throwing-it-away-tether-drop).


# 4. Catch drift nightly without a watcher

*More inputs across four systems. Some are "immutable" raw files. Something will change under you eventually, and you would rather hear it from a cron job than from a wrong result.*

Back on the trunk, register the rest of the inventory - by hand, or from the registry or spreadsheet that already lists it - and commit it:

``` bash
tether new main
```

    on trunk bookmark main; working refs set up
      code -> main
      features -> main
      zarr/imaging -> main

``` bash
tether import ../inventory.csv
```

    note: export/publish/import are experimental (tether.experimental.registry): the schema may change before 0.2
    added 3, updated 0, removed 0, unchanged 0
      added   qc/focus
      added   raw/plate2
      added   raw/plate3
    run `tether commit` to record their states

``` bash
tether commit -m "Inventory as of 2026-09-01"
```

      pinned qc/focus -> tether.0a1b2c3d.e1d708f361c4f9cf
      recorded raw/plate2 (not recoverable)
      recorded raw/plate3 (not recoverable)
    committed 0a8a92e7f529

``` bash
jj log
```

    @  txoqxnsq b46316d95661 (empty) (no description set)
    ○  uxwzktoy 0a8a92e7f529 main Inventory as of 2026-09-01
    ○  pozttxqm 785a58aa9c09 a Labels: candidate A
    ○  toqmypkw e1fc77e40112 Features rebuilt for model v3
    ○  wttkmnpn 695fe286e11b Inputs for the Q3 report
    ◆  zzzzzzzz 000000000000 (empty) (no description set)

Then schedule two commands. `status --snapshot` fingerprints every object concurrently and only *reports*. On the trunk, [modified](../reference/testing.ObjectDiff.md#tether.testing.ObjectDiff.modified) means an upstream branch moved, and [behind](../reference/ObjectStatus.md#tether.ObjectStatus.behind) that a branch-less object's system is past the pin; both exit 0. [verify](../reference/ObjectStatus.md#tether.ObjectStatus.verify) checks that every pin still names what the manifest says. Each exits 1 when something is wrong: an unreachable object, a changed immutable file, a pin that does not resolve. On a quiet night:

``` bash
tether status --snapshot
tether verify
```

    dataset a864ece136f3; on trunk bookmark main
          clean  code  [git/forkable]
          clean  features  [lance/forkable]
          clean  qc/focus  [lance/forkable]
          clean  raw/plate1  [file/observed] unrecoverable
          clean  raw/plate2  [file/observed] unrecoverable
          clean  raw/plate3  [file/observed] unrecoverable
          clean  zarr/imaging  [icechunk/forkable]
            ok  code  
            ok  features  
            ok  qc/focus  
            ok  raw/plate1  
            ok  raw/plate2  
            ok  raw/plate3  
            ok  zarr/imaging

**A raw file overwritten in place.** The instrument re-exports one well of a plate registered as `file = "immutable"` (the default). It is the *bytes* that count, not the timestamp; [status](../reference/backends.VerifyReport.md#tether.backends.VerifyReport.status) labels the plate [error](../reference/ObjectStatus.md#tether.ObjectStatus.error), reports the rest of the dataset as usual, and exits 1:

``` bash
cp ~/exports/plate2/well_1.tif ~/data/raw/plate2/well_1.tif
```

``` bash
tether status --snapshot
```

    dataset a864ece136f3; on trunk bookmark main
          clean  code  [git/forkable]
          clean  features  [lance/forkable]
          clean  qc/focus  [lance/forkable]
          clean  raw/plate1  [file/observed] unrecoverable
          error  raw/plate2  [file/observed] unrecoverable
          clean  raw/plate3  [file/observed] unrecoverable
          clean  zarr/imaging  [icechunk/forkable]
    error: raw/plate2: [raw/plate2] immutable object 'raw/plate2' changed since it was committed; re-register with acceptance to record the new state

That is deliberate: an "immutable" input silently changing is the worst kind of drift. Either something wrote where it should not have, or - as here - the change is legitimate, and accepting it is a re-registration, not a pull:

``` bash
tether remove raw/plate2
tether add raw/plate2 --kind file ~/data/raw/plate2
tether commit -m "plate2 re-exported by the instrument"
```

    removed raw/plate2
    added raw/plate2 (file)
      recorded raw/plate2 (not recoverable)
    committed 6cb575772d10

**Pins deleted by hand.** Someone "tidies up" the tags in two stores:

``` python
import icechunk as ic
import lance

lance.dataset("~/data/features.lance").tags.delete("tether.0a1b2c3d.e04adf84a168acd1")
storage = ic.local_filesystem_storage("~/data/imaging.icechunk")
ic.Repository.open(storage).delete_tag("tether.0a1b2c3d.4f2ff54c238bea8f")
```

``` bash
tether verify
```

            ok  code  
       missing  features  tag tether.0a1b2c3d.e04adf84a168acd1 missing
            ok  qc/focus  
            ok  raw/plate1  
            ok  raw/plate2  
            ok  raw/plate3  
       missing  zarr/imaging  tag tether.0a1b2c3d.4f2ff54c238bea8f missing

`repair` recreates every missing pin from its recorded state, while the store still has that state:

``` bash
tether repair
```

    repinned  features -> 0a1b2c3d.e04adf84a168acd1
    repinned  zarr/imaging -> 0a1b2c3d.4f2ff54c238bea8f

Lance gets its tag back under the same name. Icechunk never lets a deleted tag's name be reused, so tether creates the pin under the next *generation* of the same id, `tether.<id>.2`. The pin id and the manifest stay, and readers resolve a pin through its live generation, so history needs no rewriting. [verify](../reference/ObjectStatus.md#tether.ObjectStatus.verify) says which one:

``` bash
tether verify
```

            ok  code  
            ok  features  
            ok  qc/focus  
            ok  raw/plate1  
            ok  raw/plate2  
            ok  raw/plate3  
            ok  zarr/imaging  pinned as tether.0a1b2c3d.4f2ff54c238bea8f.2

``` bash
tether open zarr/imaging --rev 6cb575772d10
```

    ~/data/imaging.icechunk#tether.0a1b2c3d.4f2ff54c238bea8f.2

(Between the deletion and the `repair`, reads and forks at that commit worked too, by the snapshot id the manifest records: a deleted tag does not make the data unreachable while the store keeps the snapshot.)

**Upstream moves on its own.** A pipeline appends to the Lance table; a colleague commits to the code repository. On the trunk those branches *are* the working refs, so [status](../reference/backends.VerifyReport.md#tether.backends.VerifyReport.status) sees them as [modified](../reference/testing.ObjectDiff.md#tether.testing.ObjectDiff.modified):

``` python
import lance
import pyarrow as pa

qc = pa.table({"cell_id": [4, 5], "area": [101.0, 99.5]})
lance.write_dataset(qc, "~/data/features.lance", mode="append")
```

``` bash
git -C ~/analysis-code commit -qam "Print the status in the report"
```

``` bash
tether status --snapshot
```

    dataset c7e89eb7a798; on trunk bookmark main
       modified  code  [git/forkable]
       modified  features  [lance/forkable]
          clean  qc/focus  [lance/forkable]
          clean  raw/plate1  [file/observed] unrecoverable
          clean  raw/plate2  [file/observed] unrecoverable
          clean  raw/plate3  [file/observed] unrecoverable
          clean  zarr/imaging  [icechunk/forkable]

Nothing watches anything. Each run fingerprints the objects and compares them with the manifests, one round of metadata calls. Neither command touches history. To have the dataset *trail* the world rather than watch it, a nightly `tether pull` on `main` commits whatever moved:

``` bash
tether pull
```

      pulled code  sha=7247f96ae1b29996… dirty=False -> sha=68610dc2b2ea1ca3… dirty=False
      pulled features  branch=main version=2 -> branch=main version=3
      up to date qc/focus
      up to date raw/plate1
      up to date raw/plate2
      up to date raw/plate3
      up to date zarr/imaging
    committed 3d94692cbc21 on main

``` bash
jj log
```

    @  ptmkykpx 5fec25fc5a46 (empty) (no description set)
    ○  yyqkmnkz 3d94692cbc21 main pull main: 2 objects
    ○  txoqxnsq 6cb575772d10 plate2 re-exported by the instrument
    ○  uxwzktoy 0a8a92e7f529 Inventory as of 2026-09-01
    ○  pozttxqm 785a58aa9c09 a Labels: candidate A
    ○  toqmypkw e1fc77e40112 Features rebuilt for model v3
    ○  wttkmnpn 695fe286e11b Inputs for the Q3 report
    ◆  zzzzzzzz 000000000000 (empty) (no description set)

Mechanics: [Drift](pinning.md#drift) and [Verify](pinning.md#verify); `import` is in [Registries and SQL](registries-and-sql.md#import).


# 5. Keep the data bill down

*Two weeks of experiments, five commits a day, each pinning a snapshot the store must now keep. Only the last one matters.*

Decide at [add](../reference/testing.ObjectDiff.md#tether.testing.ObjectDiff.add) time which objects deserve durable pins. The sweep's scratch table gets `--pin record`: its state is recorded in every commit, but no tag is created, so the table's own retention decides how long each version lives. Commit the registration, then run the sweep on its own bookmark:

``` bash
tether add scratch/embeddings --kind lance ~/data/scratch.lance --pin record
tether commit -m "Track the sweep's scratch table"
```

    added scratch/embeddings (lance)
      pinned scratch/embeddings -> (addressable)
    committed 40fb50ebd8d3

``` bash
tether new -b sweep
```

    on bookmark sweep; working refs set up
      scratch/embeddings -> tether.ws.0a1b2c3d.sweep
      code -> tether.ws.0a1b2c3d.sweep  (created on first writable open)
      features -> tether.ws.0a1b2c3d.sweep  (created on first writable open)
      qc/focus -> tether.ws.0a1b2c3d.sweep  (created on first writable open)
      zarr/imaging -> tether.ws.0a1b2c3d.sweep  (created on first writable open)

(A `record` object gets its branch at [new](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.new) time, while the recorded version is known to exist.) Each trial relabels the imaging store and appends to the scratch table, in `sweep.py`:

``` python
import sys

import lance
import pyarrow as pa
import zarr
from tether import Repo

trial = int(sys.argv[sys.argv.index("--trial") + 1])
repo = Repo.find(".")
h = repo.open("zarr/imaging", read_only=False)
root = zarr.open_group(store=h.session.store, mode="a")
root["labels"][:] = 10 + trial
h.session.commit(f"labels: sweep trial {trial}")

s = repo.open("scratch/embeddings", read_only=False)
rows = pa.table({"cell_id": [1, 2, 3], "embedding_0": [trial / 10.0] * 3})
lance.write_dataset(rows, s.dataset, mode="append")
```

``` bash
python sweep.py --trial 1 && tether commit -m "sweep: trial 1"
python sweep.py --trial 2 && tether commit -m "sweep: trial 2"
python sweep.py --trial 3 && tether commit -m "sweep: trial 3"
python sweep.py --trial 4 && tether commit -m "sweep: trial 4"
```

      pinned scratch/embeddings -> (addressable)
      pinned zarr/imaging -> tether.0a1b2c3d.0a7a909e41d53e9f
    committed f6b7dc3d4684
      pinned scratch/embeddings -> (addressable)
      pinned zarr/imaging -> tether.0a1b2c3d.4b82058e9ca8fc47
    committed dee05e5b687e
      pinned scratch/embeddings -> (addressable)
      pinned zarr/imaging -> tether.0a1b2c3d.33df955000c4bc23
    committed 72e6ad98408a
      pinned scratch/embeddings -> (addressable)
      pinned zarr/imaging -> tether.0a1b2c3d.0e509868c4ad0242
    committed 37522ceba6eb

``` bash
jj log -r main::@
```

    @  zqrpxxky 55901daa14e8 (empty) (no description set)
    ○  psozpoln 37522ceba6eb sweep sweep: trial 4
    ○  utvsynsz 72e6ad98408a sweep: trial 3
    ○  ksqvytqn dee05e5b687e sweep: trial 2
    ○  molrptzv f6b7dc3d4684 sweep: trial 1
    ○  ptmkykpx 40fb50ebd8d3 main Track the sweep's scratch table
    │
    ~

Four Icechunk tags the store must now keep. When the experiment is over, drop the commits that were only tries and release what only they pinned, in one step. Later commits keep their manifests exactly as they were (they are snapshots, not patches), so trial 4 still names its own pins:

``` bash
tether abandon ksqvytqn utvsynsz --gc
```

    abandoned dee05e5b687e, 72e6ad98408a
    unpinned 2 pin(s)
      icechunk: 0a1b2c3d.33df955000c4bc23
      icechunk: 0a1b2c3d.4b82058e9ca8fc47

``` bash
jj log -r main::@
```

    @  yrzoywwn 76baaebab26d (empty) (no description set)
    ○  psozpoln 8e78d3cb811a sweep sweep: trial 4
    ○  molrptzv f6b7dc3d4684 sweep: trial 1
    ○  ptmkykpx 40fb50ebd8d3 main Track the sweep's scratch table
    │
    ~

Trial 4 was rebased onto trial 1 (new commit id, same change id, same manifests), and the `sweep` bookmark followed it. `scratch/embeddings` freed nothing: it had no pins, which was the point of `--pin record`. ([abandon](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.abandon) restores each descendant's manifests, so the empty working copy is created again on top, with a new change id.) Releasing a tag makes the snapshot *eligible*; the store reclaims the bytes on its own schedule. Recorded states past their retention show up in `verify --all-history` as missing: the trade you made with `--pin record`, stated back to you.

**A store that should not outlive its probe** *(experimental)*. So far every store existed before tether saw it, and tether never deletes what it merely adopted. A probe often needs a store of its own, such as a second embeddings table, with no reason to exist once the probe is thrown away. `add --create` has tether make it, on a bookmark of its own ([Stores tether created](reclaiming-storage.md#stores-tether-created)).

``` bash
ls ~/data
```

    features.lance
    imaging.icechunk
    qc.lance
    raw
    scratch.lance

``` bash
tether new -b probe
```

    on bookmark probe; working refs set up
      scratch/embeddings -> tether.ws.0a1b2c3d.probe
      code -> tether.ws.0a1b2c3d.probe  (created on first writable open)
      features -> tether.ws.0a1b2c3d.probe  (created on first writable open)
      qc/focus -> tether.ws.0a1b2c3d.probe  (created on first writable open)
      zarr/imaging -> tether.ws.0a1b2c3d.probe  (created on first writable open)

``` bash
tether add scratch/probe --kind icechunk ~/data/probe.icechunk --create
```

    added scratch/probe (icechunk, created)
    note: the store lifecycle (`--create`, `gc --delete-stores`) is experimental: deleting a store has no `repair`; see the reclaiming-storage guide

The store is created empty, with an owner marker naming this dataset, and remembered with the bookmark it was made on in `.jj/repo/tether-created.jsonl`. On a bookmark its working branch is forked at once, so the first writable [open](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.open) lands on it. (`repo.create("scratch/probe", "icechunk", {"uri": ...})` does the [add](../reference/testing.ObjectDiff.md#tether.testing.ObjectDiff.add) and the [open](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.open) in one call.)

``` python
import zarr
from tether import Repo

repo = Repo.find(".")
h = repo.open("scratch/probe")
root = zarr.create_group(store=h.session.store)
root.create_array("embeddings", shape=(3, 4), dtype="f4")[:] = 0.5
h.session.commit("embeddings: probe")
```

``` bash
tether commit -m "probe: embeddings v2"
```

      pinned scratch/probe -> tether.0a1b2c3d.569de089097300d2
    committed d4756b6b8e3d

``` bash
tether status
```

    dataset 713126458caf; on bookmark probe
      states as fingerprinted 0s ago; --snapshot to refresh
          clean  code  [git/forkable]
          clean  features  [lance/forkable]
          clean  qc/focus  [lance/forkable]
          clean  raw/plate1  [file/observed] unrecoverable
          clean  raw/plate2  [file/observed] unrecoverable
          clean  raw/plate3  [file/observed] unrecoverable
          clean  scratch/embeddings  [lance/forkable]
          clean  scratch/probe  [icechunk/forkable] (created)
          clean  zarr/imaging  [icechunk/forkable]

``` bash
jj log -r main::@
```

    @  rlvpxxsn 0aed347cebd2 (empty) (no description set)
    ○  yrzoywwn d4756b6b8e3d probe probe: embeddings v2
    ○  psozpoln 8e78d3cb811a sweep sweep: trial 4
    ○  molrptzv f6b7dc3d4684 sweep: trial 1
    ○  ptmkykpx 40fb50ebd8d3 main Track the sweep's scratch table
    │
    ~

The probe did not pan out. One `drop`, from the bookmark itself: `--to sweep` says where to leave for, and `--delete-stores` opts into the experimental step for the store the probe created:

``` bash
tether drop probe --to sweep --delete-stores --no-dry-run
```

    note: reclaiming created stores is experimental: `delete-store` has no `repair`; read the plan
    left probe for sweep
    dropped probe: 1 commit(s) (d4756b6b8e3d)
    unpinned 1 pin(s)
      icechunk: 0a1b2c3d.569de089097300d2
    deleted 2 working branch(es)
      scratch/embeddings: tether.ws.0a1b2c3d.probe
      scratch/probe: tether.ws.0a1b2c3d.probe
    forgot 1 working ref(s) of removed object(s)
    deleted 1 created store(s)
      scratch/probe: ~/data/probe.icechunk

``` bash
ls ~/data
```

    features.lance
    imaging.icechunk
    qc.lance
    raw
    scratch.lance

Leave, drop the commit, unpin, delete the branches (the sweep table's too: its recorded state was the dropped commit's), and, with nothing left but the owner marker, delete the store. The data directory is as it was before the probe. A store tether *adopted* is never deleted this way, and editing a manifest cannot make it happen: only the marker in the store and the index in your repository say a store is tether's to remove. Had another clone forked its own branch in the store meanwhile, the plan would have kept the store whole, and said so.

Mechanics: [Reclaiming Storage](reclaiming-storage.md).


# 6. Recover

*Something went wrong. Three kinds of wrong, and what comes back.*

**An unwanted [new](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.new)** - you started a bookmark you did not mean to, and `--eager` forked every branch at once. The op log knows what was created:

``` bash
tether new -b scratch --eager
```

    on bookmark scratch; working refs set up
      code -> tether.ws.0a1b2c3d.scratch
      features -> tether.ws.0a1b2c3d.scratch
      qc/focus -> tether.ws.0a1b2c3d.scratch
      scratch/embeddings -> tether.ws.0a1b2c3d.scratch
      zarr/imaging -> tether.ws.0a1b2c3d.scratch

``` bash
tether ops -n 3
```

    29739c9a980b  2026-09-01T09:00:02+00:00  new      forked code, features, qc/focus, scratch/embeddings, zarr/imaging
    e0ff80f46ab8  2026-09-01T09:00:01+00:00  gc       unpinned 1, deleted 2 branch(es), deleted 1 store(s)  (step of b1b678f8dc80)
    cda6e861821c  2026-09-01T09:00:00+00:00  new      kept scratch/embeddings, zarr/imaging; deferred code, features, qc/focus; at sweep  (step of b1b678f8dc80)

``` bash
tether undo
```

    undid 29739c9a980b (new: forked code, features, qc/focus, scratch/embeddings, zarr/imaging)
      restored   code: deleted tether.ws.0a1b2c3d.scratch
      restored   features: deleted tether.ws.0a1b2c3d.scratch
      restored   qc/focus: deleted tether.ws.0a1b2c3d.scratch
      restored   scratch/embeddings: deleted tether.ws.0a1b2c3d.scratch
      restored   zarr/imaging: deleted tether.ws.0a1b2c3d.scratch
      restored   VCS working copy back where it was
      restored   workspace.toml restored (bookmark; base_states, working_refs, pending_forks, fork_points of code, features, qc/focus, scratch/embeddings, scratch/probe, zarr/imaging)
      restored   bookmark scratch deleted

Five branches deleted, the workspace file and the VCS working copy put back, the bookmark gone: the checkout is on `sweep` again as if nothing happened. `sweep`'s own branches were never in play, since a new bookmark forks its own.

**One object's branch on the wrong state** - everything else is fine, only `zarr/imaging` should go back to what an older commit pinned. `restore` resets the current bookmark's branch for that object, and nothing else:

``` bash
tether restore zarr/imaging --from 695fe286e11b
```

    zarr/imaging -> tether.ws.0a1b2c3d.sweep  (from 695fe286e11b)

``` bash
tether status --snapshot
```

    dataset 7e225fca7bd0; on bookmark sweep
          clean  code  [git/forkable]
          clean  features  [lance/forkable]
          clean  qc/focus  [lance/forkable]
          clean  raw/plate1  [file/observed] unrecoverable
          clean  raw/plate2  [file/observed] unrecoverable
          clean  raw/plate3  [file/observed] unrecoverable
          clean  scratch/embeddings  [lance/forkable]
       modified  zarr/imaging  [icechunk/forkable]

The other branches, the manifests and the working copy stay put, and the next [commit](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.commit) would pin what you restored. Here it was a demonstration. `undo` does not guess where a branch it *reset* should go, so it restores the workspace record, says what the branch was, and leaves the move to you:

``` bash
tether undo
```

    undid d937ea68b370 (restore: restored zarr/imaging from 695fe286e11b)
      restored   workspace.toml restored (fork_points, last_snapshot of code, features, qc/focus, scratch/embeddings, zarr/imaging)
      IRREVERSIBLE zarr/imaging: tether.ws.0a1b2c3d.sweep was reset (was snapshot_id=4JV2FQGRZYNAGTXX…); `tether restore zarr/imaging --from REV` or `tether new --discard` puts it where you want

`restore` is its own undo: name the commit whose state you want. The branch holds the demonstration's state, which this workspace never committed, so `restore` asks for `--discard` before it throws that away:

``` bash
tether restore zarr/imaging --from 8e78d3cb811a --discard
```

    zarr/imaging -> tether.ws.0a1b2c3d.sweep  (from 8e78d3cb811a)

**Several things in a row.** A restore you did not mean, then a bookmark you did not mean. `undo` reverses the newest operation each time; run it once per slip:

``` bash
tether restore features --from 695fe286e11b
```

    features -> tether.ws.0a1b2c3d.sweep  (from 695fe286e11b)

``` bash
tether new -b again --eager
```

    on bookmark again; working refs set up
      code -> tether.ws.0a1b2c3d.again
      features -> tether.ws.0a1b2c3d.again
      qc/focus -> tether.ws.0a1b2c3d.again
      scratch/embeddings -> tether.ws.0a1b2c3d.again
      zarr/imaging -> tether.ws.0a1b2c3d.again

``` bash
tether ops -n 3
```

    1a9979c28619  2026-09-01T09:00:05+00:00  new      forked code, features, qc/focus, scratch/embeddings, zarr/imaging
    4897a4aca888  2026-09-01T09:00:04+00:00  restore  restored features from 695fe286e11b
    85dbb59077c7  2026-09-01T09:00:03+00:00  restore  restored zarr/imaging from 8e78d3cb811a

``` bash
tether undo
```

    undid 1a9979c28619 (new: forked code, features, qc/focus, scratch/embeddings, zarr/imaging)
      restored   code: deleted tether.ws.0a1b2c3d.again
      restored   features: deleted tether.ws.0a1b2c3d.again
      restored   qc/focus: deleted tether.ws.0a1b2c3d.again
      restored   scratch/embeddings: deleted tether.ws.0a1b2c3d.again
      restored   zarr/imaging: deleted tether.ws.0a1b2c3d.again
      restored   VCS working copy back where it was
      restored   workspace.toml restored (bookmark; base_states, working_refs, pending_forks, fork_points, last_snapshot of code, features, qc/focus, scratch/embeddings, scratch/probe, zarr/imaging)
      restored   bookmark again deleted

``` bash
tether undo
```

    undid 4897a4aca888 (restore: restored features from 695fe286e11b)
      restored   features: deleted tether.ws.0a1b2c3d.sweep
      restored   workspace.toml restored (working_refs, pending_forks, fork_points, last_snapshot of features)

The restore had *created* `features`' branch on `sweep` (nothing had written to `features` there yet), so undoing it deletes that branch again, and `sweep` has exactly the branches it had before.

**A pin deleted by hand, or by a `gc` run on the wrong clone**: the manifests still promise it, and section 4 ran this one. [verify](../reference/ObjectStatus.md#tether.ObjectStatus.verify) reports `missing`, and `repair` recreates the pin from the recorded state; until then, reads and forks at that commit fall back to the recorded snapshot.

None of this touched history: the `scratch` and `again` bookmarks came and went without a commit, and `restore` acts on the stores. The log is what section 5 left, and the workspace is back on `sweep`:

``` bash
jj log -r main::@
```

    @  ovsumqmm 68c2991eb8b3 (empty) (no description set)
    ○  psozpoln 8e78d3cb811a sweep sweep: trial 4
    ○  molrptzv f6b7dc3d4684 sweep: trial 1
    ○  ptmkykpx 40fb50ebd8d3 main Track the sweep's scratch table
    │
    ~

tether's `undo` and `repair` cover tether's side, and say exactly what they could not do (a snapshot the store already expired, a [promote](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.promote)); `jj undo` covers the VCS's ([Undo](branching-and-writing.md#undo)).

Mechanics: [`ops`, `undo`, `repair`](cli.md#ops-undo-repair) in the CLI Guide.


# 7. Publish history to a registry (experimental)

*A dashboard, a training pipeline, and a data catalog all want to know which state of which table a run used. None of them should have to clone the repository to find out.*

Push the history into Postgres - after every commit to `main`, typically from CI. Every commit on every bookmark goes, with its parents, refs, and one row per object per commit:

``` bash
tether publish --to "$REGISTRY_DSN" --schema tether
```

    note: export/publish/import are experimental (tether.experimental.registry): the schema may change before 0.2
    published 111 row(s) to schema 'tether' (0 commit(s) already present)
      tether_meta            5 row(s)
      commits               10 row(s)
      commit_parents         9 row(s)
      refs                   4 row(s)
      objects               65 row(s)
      object_states         18 row(s)
      listings               0 row(s)
      listing_entries        0 row(s)
      workspace              0 row(s)

Consumers join their own run log against `tether.objects` - the per-commit table; `tether.object_states` is the distinct (system, state) pairs a catalog keys on. A training run recorded which dataset commit it read, and the join says what that meant for every input:

``` python
import os

import psycopg

with psycopg.connect(os.environ["REGISTRY_DSN"]) as conn:
    conn.execute("create table runs (run_id text, dataset_commit text)")
    conn.execute(
        "insert into runs values ('train-2026-09-14', %s)",
        ("8e78d3cb811a673475d671980c31aa3830b3c176",),
    )
    rows = conn.execute(
        """
        select r.run_id, o.key, o.pin_id, o.state_json
        from   runs r
        join   tether.objects o on o.commit_id = r.dataset_commit
        where  r.run_id = 'train-2026-09-14'
        order  by o.key
        """
    ).fetchall()
for run_id, key, pin_id, state in rows:
    print(run_id, key, pin_id, state)
```

    train-2026-09-14 code 0a1b2c3d.1be70ba1eb967f7f {'sha': '68610dc2b2ea1ca33630406d6a60d6ea0f912acb', 'dirty': False}
    train-2026-09-14 features 0a1b2c3d.9990e2f2b8b77725 {'branch': 'main', 'version': 3}
    train-2026-09-14 qc/focus 0a1b2c3d.e1d708f361c4f9cf {'branch': 'main', 'version': 1}
    train-2026-09-14 raw/plate1 None {'size': 192, 'type': 'dir', 'count': 3, 'digest': 'a3fb5832d75cdfb070f8d0599f98c7b3'}
    train-2026-09-14 raw/plate2 None {'size': 192, 'type': 'dir', 'count': 3, 'digest': '47532d1166feda0d67f0151292ebf110'}
    train-2026-09-14 raw/plate3 None {'size': 192, 'type': 'dir', 'count': 3, 'digest': '5e98f0e256dee8240f20f6068ad30a5b'}
    train-2026-09-14 scratch/embeddings None {'branch': 'tether.ws.0a1b2c3d.sweep', 'version': 5, 'branch_id': '36828d436e87dff0e9f9d701fbff1798'}
    train-2026-09-14 zarr/imaging 0a1b2c3d.0e509868c4ad0242 {'snapshot_id': '4JV2FQGRZYNAGTXXRJER'}

`pin_id` is null for the recorded-only objects (the raw plates, the `--pin record` scratch table); their `state_json` is still the exact state. For a provenance dashboard, `tether export` writes the same tables as files:

``` bash
tether export ../history --format parquet
```

    note: export/publish/import are experimental (tether.experimental.registry): the schema may change before 0.2
    exported 10 file(s) to ~/history (parquet)
      tether_meta            5 row(s)
      commits               10 row(s)
      commit_parents         9 row(s)
      refs                   4 row(s)
      objects               65 row(s)
      object_states         18 row(s)
      listings               0 row(s)
      listing_entries        0 row(s)
      workspace              0 row(s)

When the registry, not the repository, is where objects are declared, run the other direction: `tether import` from a query, then [commit](../reference/vcs.VcsAdapter.md#tether.vcs.VcsAdapter.commit). Section 4 did that from a CSV.

Producers and consumers meet in the middle. The producer commits and publishes. The consumer's pipeline verifies the commit it is about to read, and refuses to start on a pin that is gone (`--deep` opens each recorded state, the only way to check a `record`-pinned object):

``` bash
tether verify --rev 8e78d3cb811a --deep
```

            ok  code  
            ok  features  
            ok  qc/focus  
            ok  raw/plate1  
            ok  raw/plate2  
            ok  raw/plate3  
            ok  scratch/embeddings  
            ok  zarr/imaging

`publish` and `export` do not write to the repository, so the story ends where section 5 left it: the sweep on its bookmark, `main` behind it. Landing it is section 2's recipe. [promote](../reference/testing.ObjectBackend.md#tether.testing.ObjectBackend.promote) will refuse the Lance scratch table and hold the imaging store, so `promote zarr/imaging` lands the labels on purpose, and the scratch rows stay on their branch.

``` bash
jj log
```

    @  ovsumqmm 68c2991eb8b3 (empty) (no description set)
    ○  psozpoln 8e78d3cb811a sweep sweep: trial 4
    ○  molrptzv f6b7dc3d4684 sweep: trial 1
    ○  ptmkykpx 40fb50ebd8d3 main Track the sweep's scratch table
    ○  yyqkmnkz 3d94692cbc21 pull main: 2 objects
    ○  txoqxnsq 6cb575772d10 plate2 re-exported by the instrument
    ○  uxwzktoy 0a8a92e7f529 Inventory as of 2026-09-01
    ○  pozttxqm 785a58aa9c09 a Labels: candidate A
    ○  toqmypkw e1fc77e40112 Features rebuilt for model v3
    ○  wttkmnpn 695fe286e11b Inputs for the Q3 report
    ◆  zzzzzzzz 000000000000 (empty) (no description set)

Mechanics: [Registries and SQL](registries-and-sql.md).
