---
title: Snapshots and refs
description: What a snapshot contains, how refs are allocated and stored, and how a ref resolves back to a live element or goes stale.
url: "https://agent-browser.dev/desktop/snapshots-and-refs"
docs_index: /desktop/llms.txt
lastUpdated: 2026-10-30
navTitle: "Snapshots and refs"
---

> For an index of all documentation, see [/desktop/llms.txt](/desktop/llms.txt).

A snapshot is one observation of an application window's accessibility tree. agent-desktop saves it with a ref map and gives every actionable element a ref that you pass to later commands.

```bash
agent-desktop snapshot --app gnome-calculator -i --compact
```

```json
{
  "version": "2.5",
  "ok": true,
  "command": "snapshot",
  "data": {
    "app": "gnome-calculator",
    "complete": true,
    "ref_count": 40,
    "snapshot_id": "s26u7pv9qta5g4",
    "window": {
      "id": "w-1.11-org-gnome-Calculator-a11y-948fd89a_9bda_453b_9c10_4a9e9eed3feb",
      "title": "Calculator"
    },
    "tree": {
      "role": "window",
      "name": "Calculator",
      "native_id": { "kind": "ax_identifier", "value": "MathWindow" },
      "children": [
        {
          "available_actions": ["Click", "SetFocus"],
          "description": "Clear Display [Escape]",
          "name": "C",
          "native_id": { "kind": "ax_identifier", "value": "calc_clear_button" },
          "ref_id": "@s26u7pv9qta5g4:e6",
          "role": "button"
        }
      ]
    }
  }
}
```

The tree above is trimmed to one button. The real tree nests it under several unnamed `group` nodes.

## What a snapshot contains

<table>
  <thead>
    <tr>
      <th>Field</th>

      <th>Meaning</th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td><code>data.app</code>, <code>data.window</code></td>

      <td>The application and the window that was read (<code>id</code>, <code>title</code>).</td>
    </tr>

    <tr>
      <td><code>data.snapshot_id</code></td>

      <td>The id of the saved snapshot. It is part of every ref.</td>
    </tr>

    <tr>
      <td><code>data.ref_count</code></td>

      <td>The number of refs the snapshot holds. After a <code>--root</code> drill it counts the whole snapshot, not only the drilled region.</td>
    </tr>

    <tr>
      <td><code>data.complete</code></td>

      <td>Present on every snapshot. <code>false</code> means the observation ran out of budget. See <a href="/desktop/skeleton-and-drill-down#partial-observations">Partial observations</a>.</td>
    </tr>

    <tr>
      <td><code>data.truncated</code>, <code>data.nodes_observed</code></td>

      <td>Present only when <code>complete</code> is <code>false</code>.</td>
    </tr>

    <tr>
      <td><code>data.tree</code></td>

      <td>The root node. Every node nests its <code>children</code>.</td>
    </tr>
  </tbody>
</table>

A node always carries `role` and, when they exist, `ref_id`, `name`, `value`, `description`, `native_id`, `states`, `available_actions`, `children_count`, `subtree_truncated`, and `children`. Empty fields are omitted. `bounds` appears only with `--include-bounds`:

```json
{
  "available_actions": ["Click", "SetFocus"],
  "bounds": { "height": 44.0, "width": 64.0, "x": 554.0, "y": 555.0 },
  "name": "7",
  "native_id": { "kind": "ax_identifier", "value": "calc_7_button" },
  "ref_id": "@s2jqig9fk9u6gz:e9",
  "role": "button"
}
```

`children_count` and `subtree_truncated` mark where the tree was cut; [Skeleton and drill-down](/desktop/skeleton-and-drill-down) explains both. Three flags shrink the tree, and the [commands reference](/desktop/commands/observation#snapshot) lists every flag:

- `-i` (`--interactive-only`) drops leaves that have no ref. It keeps nodes that lead to refs and nodes marked with `children_count`.
- `--compact` collapses a single-child wrapper that has no name, value, description, id, state, or action.
- `--max-depth` limits depth. The default is 10 and the maximum is 50.

## Choose the window

`snapshot` and `find` read one window. Without `--app` they read the focused window, and `WINDOW_NOT_FOUND` means there is none. With `--app` and no `--window-id`, agent-desktop narrows the app's windows in this order and stops at the first step that leaves exactly one:

1. If the app has only one window, use it.
2. Keep only visible windows.
3. Keep only the focused window.
4. Keep only accessible windows.

If several windows are still left, the command fails with `AMBIGUOUS_TARGET`, and `details.candidates` lists up to 10 of them. Run `list-windows --app <name>` and pass `--window-id`.

## Refs

A ref is an element id with its snapshot id inside it:

```text
@<snapshot_id>:eN        # for example @s26u7pv9qta5g4:e6
```

Every command accepts only this qualified form. A bare `@e6` fails with `INVALID_ARGS`; `@eN` is the key inside the saved ref map, and agent-desktop never prints it as output. Because each ref names its snapshot, it keeps pointing at its own snapshot when you read several apps or windows in turn.

```json
{
  "version": "2.5",
  "ok": false,
  "command": "click",
  "error": {
    "code": "INVALID_ARGS",
    "message": "Invalid ref_id '@e1': expected @<snapshot_id>:e<N>",
    "suggestion": "Use the snapshot-qualified ref that snapshot prints, for example @s8f3k2p9:e1. A bare @eN ref is not accepted.",
    "disposition": { "delivery": "not_delivered", "retry": "safe" }
  }
}
```

### Which elements get a ref

Refs are allocated depth-first in document order, from `e1` upward, so a parent gets its number before its children. The counter belongs to the snapshot, and a later drill-down continues it instead of reusing numbers.

An element gets a ref when either condition holds:

- Its role is interactive: `button`, `cell`, `checkbox`, `colorwell`, `combobox`, `dockitem`, `incrementor`, `link`, `listbox`, `menubutton`, `menuitem`, `option`, `radiobutton`, `slider`, `switch`, `tab`, `textfield`, or `treeitem`.
- It advertises a primary action. `scrollarea` (Scroll) and `disclosure` (Expand and Collapse) qualify this way, because `scroll`, `expand`, and `collapse` need a ref to target them. In the example above, the calculator's history pane is a `scrollarea` with a ref.

`SetFocus`, `RightClick`, and `ScrollTo` do not count on their own. Web runtimes advertise them on nearly every node, so counting them would give every inert container a ref and overflow the [ref map size limit](#where-refs-are-stored). Static text and inert containers stay in the tree without a ref. `find` returns them as context matches with `"ref_id": null`.

A container cut off by a skeleton can also receive a ref as a drill target. See [Find the drill target](/desktop/skeleton-and-drill-down#find-the-drill-target).

## What a ref stores

Each ref maps to a `RefEntry`: the evidence needed to find the same element again. The ref map holds evidence, not live objects.

<table>
  <thead>
    <tr>
      <th>Group</th>

      <th>Fields</th>

      <th>Used for</th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>Process</td>

      <td><code>pid</code>, <code>process_instance</code></td>

      <td>Reject a ref whose process exited or whose pid was reused.</td>
    </tr>

    <tr>
      <td>Identity</td>

      <td><code>role</code>, <code>name</code>, <code>value</code>, <code>description</code>, <code>native_id</code></td>

      <td>Match the live element. A control's <code>value</code> is never identity for roles whose value changes while you use them, such as <code>textfield</code>, <code>checkbox</code>, and <code>slider</code>.</td>
    </tr>

    <tr>
      <td>Geometry</td>

      <td><code>bounds</code>, <code>bounds_hash</code>, <code>bounds_space</code></td>

      <td>Tell apart candidates that match equally. For an element with no other identity, a unique matching rectangle can identify it.</td>
    </tr>

    <tr>
      <td>Capabilities</td>

      <td><code>states</code>, <code>available_actions</code></td>

      <td>What the element offered at snapshot time.</td>
    </tr>

    <tr>
      <td>Source</td>

      <td><code>source_app</code>, <code>source_window_id</code>, <code>source_window_title</code>, <code>source_window_bounds_hash</code>, <code>source_surface</code></td>

      <td>Tie the ref to its window and surface.</td>
    </tr>

    <tr>
      <td>Scope</td>

      <td><code>path</code>, <code>root_ref</code>, <code>path_is_absolute</code></td>

      <td>The element's position as child indexes, and which drill-down created the ref.</td>
    </tr>
  </tbody>
</table>

## How a ref resolves

A command never reuses a live handle from the snapshot. Every command that takes a ref, including `click`, `get`, `is`, `snapshot --root`, and `find --root`, re-identifies the element from its stored evidence first.

1. The command parses `@s26u7pv9qta5g4:e6` into the snapshot id and the key `@e6`. A bare or malformed ref fails with `INVALID_ARGS`.
2. `RefStore` loads `snapshots/s26u7pv9qta5g4/refmap.json` from the selected session's namespace. A missing snapshot fails with `SNAPSHOT_NOT_FOUND`.
3. The ref map looks up `@e6` and returns its `RefEntry`. A missing key fails with `STALE_REF`.
4. The platform adapter resolves the entry strictly against the live UI: it checks the process and the source window, then searches the window's live tree for elements with the stored role and stable identity.
5. Exactly one match: the command acts on it or reads it. No match: `STALE_REF`. Several matches that nothing separates: `AMBIGUOUS_TARGET`.
6. After `STALE_REF` or `AMBIGUOUS_TARGET`, take a fresh observation and use the new ref.

The stored position is a hint for where to look, and the identity check decides. Identifier and stable text come first. Geometry identifies an element only when the ref has no identifier and no stable text, and only on a unique rectangle. When several live elements match, the stored bounds can pick one of them; if they cannot, the answer is `AMBIGUOUS_TARGET`.

<table>
  <thead>
    <tr>
      <th>Error</th>

      <th>Meaning</th>

      <th>What to do</th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td><code>STALE_REF</code></td>

      <td>The key is not in the snapshot's ref map, or no live element answers to the stored evidence. The UI changed, the window closed, or a re-drill replaced the ref.</td>

      <td>Take a fresh snapshot, or re-drill the region, and use the new ref. Nothing was sent to the app.</td>
    </tr>

    <tr>
      <td><code>AMBIGUOUS_TARGET</code></td>

      <td>Several live elements match the stored evidence, and the stored bounds cannot separate them. The same code reports several windows for one <code>--app</code>.</td>

      <td>Narrow the target with <code>find --role --name --exact</code> or <code>--root</code>, then use the new ref.</td>
    </tr>

    <tr>
      <td><code>SNAPSHOT_NOT_FOUND</code></td>

      <td>No saved snapshot has that id: it was pruned, it belongs to another <a href="/desktop/sessions">session</a>, or the state root differs.</td>

      <td>Take a new snapshot under the same <code>--session</code> and state root as the command that uses it.</td>
    </tr>
  </tbody>
</table>

The full `STALE_REF` envelope after a re-drill is in [Skeleton and drill-down](/desktop/skeleton-and-drill-down#re-drill-after-a-change), and every code with its retry rule is in [Errors and recovery](/desktop/errors-and-recovery).

## Refresh after the UI changes

Refs describe one observation. Re-read after anything that can change the tree, and act only on refs from the latest read of the region:

- `snapshot` returns a new snapshot id and a new ref map.
- `find` with matches also saves a new snapshot, and its `data.snapshot_id` qualifies the refs it returns.
- `snapshot --root` re-reads one region inside an existing snapshot. See [Skeleton and drill-down](/desktop/skeleton-and-drill-down#re-drill-after-a-change).

An older snapshot stays on disk until it is pruned, and its refs keep resolving while their elements still match. Do not rely on that: after the UI changes, an old ref can fail, or match an element that is no longer the one you meant.

## Where refs are stored

`RefStore` keeps one file per snapshot at `snapshots/<snapshot_id>/refmap.json` under the [state root](/desktop/configuration#state-root). Inside a [session](/desktop/sessions), the same layout sits under `sessions/<session_id>/`, and a lookup never searches another session's snapshots.

<table>
  <thead>
    <tr>
      <th>Limit</th>

      <th>Value</th>

      <th>What happens</th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>Size of one ref map</td>

      <td>1 MB (1,048,576 bytes)</td>

      <td>The write fails with <code>INTERNAL</code>. Narrow the observation with <code>--skeleton</code>, <code>--max-depth</code>, <code>--root</code>, or a tighter <code>find</code>.</td>
    </tr>

    <tr>
      <td>Saved snapshots per namespace</td>

      <td>128</td>

      <td>When a save pushes the count past 128, the oldest are removed until 96 remain. The snapshot just written is never removed.</td>
    </tr>
  </tbody>
</table>

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)