# How Ego-Browser Element Refs (`@N` Format) Work and Why Re‑snapshot Is Required

> Learn how ego-browser @N element refs map to Chromium DevTools backendNodeIds and why automatic re-snapshotting is essential to avoid stale node errors in your application.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: internals
- Published: 2026-08-15

---

**Ego-browser `@N` element references are numeric snapshot IDs tied to Chromium DevTools `backendNodeId`s that require automatic re-snapshoting when the DOM changes to prevent stale node errors.**

The `ego-lite` browser automation library uses a compact `@N` syntax to reference DOM elements without brittle CSS selectors. This article explains how these references are created, resolved, and automatically refreshed through the re-snapshot mechanism.

## What Are `@N` Element References?

Ego-browser represents DOM elements with **snapshot references** — short strings like `@12`. These refs are **numeric IDs** that correspond to the Chromium DevTools Protocol's `backendNodeId` for a node at the moment a snapshot is taken.

The `@` prefix distinguishes snapshot refs from other selector types. You can also use the equivalent `ref=12` syntax. Both map to internal numeric keys stored in a runtime `RefMap`.

## How `@N` References Are Created

When the runtime takes a snapshot of the page, it walks the accessibility tree and stores each node in a **`RefMap`** located at [`package/ego-browser/src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/ref-map.ts).

For every node, [`ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/ref-map.ts) records:

```ts
this.map.set(refId, {
  backendNodeId,   // the CDP node identifier
  role,
  name,
  nth,
  selector: undefined,
  frameId,
});

```

The public helper `parseRef` (same file) turns the user-visible string `@12` or `ref=12` into the numeric key `"12"`:

```ts
export function parseRef(input) {
  const trimmed = String(input || "").trim();
  for (const candidate of [
    trimmed.startsWith("@") ? trimmed.slice(1) : null,
    trimmed.startsWith("ref=") ? trimmed.slice(4) : null,
    trimmed,
  ]) {
    if (candidate && /^\d+$/.test(candidate)) {
      return candidate;               // → "12"
    }
  }
  return null;
}

```

This implementation accepts three input formats and validates that the result contains only digits.

## Resolving References to DOM Nodes

Helper functions like `page.elementCenter('@12')` call the resolver in [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts), which looks up the ID in the current `RefMap`.

If the map contains the entry, the resolver obtains the `backendNodeId` and performs CDP actions such as:

- Clicking elements
- Getting bounding boxes
- Capturing screenshots

## Why Re‑snapshot Is Required

The snapshot map only reflects the DOM **as it existed at the snapshot moment**. Navigation, DOM updates, or a page reload can invalidate those IDs.

When a helper tries to resolve a ref **and the map is empty or the ID is missing**, the resolver treats this as a **"transient"** failure and automatically triggers a **new snapshot**.

This auto-re-snapshot rebuilds the `RefMap`, repopulating it with fresh `backendNodeId`s that match the current page state. Consequently, the same `@N` syntax continues to work across separate script rounds without the agent needing to remember to take an explicit snapshot.

## Practical Usage Examples

```js
// Take a screenshot of an element using its snapshot ref
await page.screenshot({ path: 'logo.png', selector: '@7' });

// Click the center of an element referenced by @12
await page.click('@12');

// Get the bounding box of a node — re-snapshot happens automatically if needed
const box = await page.boundingBox('@3');   // If the map is stale, ego-browser
                                            // will capture a fresh snapshot first

```

## Design Rationale

The `@N` reference system provides three key advantages:

1. **Terseness** — Numeric refs keep the public API compact and readable
2. **Stability** — References remain valid for the duration of a round
3. **Transparency** — Automatic re-snapshot abstracts DOM churn, preventing stale node errors

This architecture enables long-running agents that may pause between actions without requiring manual snapshot management.

## Summary

- `@N` and `ref=N` syntax both resolve to numeric keys in the `RefMap`
- `parseRef` in [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts) handles input parsing with flexible prefix support
- References map to Chromium DevTools `backendNodeId` values captured at snapshot time
- The resolver in [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) triggers automatic re-snapshot when IDs are missing or stale
- This design ensures agents always operate on current DOM state without explicit synchronization

## Frequently Asked Questions

### What happens if I use an `@N` ref after the page reloads?

The resolver detects the missing ID in `RefMap`, treats it as a transient failure, and automatically captures a fresh snapshot before retrying the operation. Your script continues without manual intervention.

### Can I mix `@N` refs with other selector types?

Yes. [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) handles multiple selector formats including `@N`, `loc=` (accessibility locators), and `xpath=` expressions. The resolver routes each to the appropriate resolution path.

### Where is the automatic re-snapshot logic implemented?

The re-snapshot trigger lives in [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts), which coordinates with the `RefMap` in [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts). This separation keeps reference storage decoupled from resolution retry logic.

### Why does ego-browser use `backendNodeId` instead of stable selectors?

`backendNodeId` provides direct Chromium DevTools Protocol integration with minimal overhead. The automatic re-snapshot mechanism compensates for the ID's transient nature, offering both performance and reliability.