# How Snapshot References (@N) Work in ego-browser: Mapping to Backend Node IDs

> Understand how ego-browser snapshot references @N map to backend node IDs. Learn to target elements stably in automation scripts with the RefMap class.

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

---

**Snapshot references in ego-browser are compact aliases for Chrome DevTools Protocol (CDP) backend node IDs, enabling stable element targeting across automation scripts via the RefMap class.**

In the `citrolabs/ego-lite` repository, **snapshot references** provide a concise way to interact with DOM elements without brittle CSS selectors. When you write `@21` in an ego-browser script, you are referencing a numeric identifier that maps directly to a `backendNodeId` supplied by the Chrome DevTools Protocol. This architecture decouples the static accessibility snapshot from live JavaScript objects, ensuring references remain valid across multiple script execution rounds.

## Understanding the @N Notation

The `@N` syntax (where N is an integer) represents a shorthand maintained by the **RefMap** class. During snapshot generation, ego-browser traverses the page's accessibility tree and assigns sequential reference IDs to each node. However, these integers are not arbitrary—they are paired with the `backendNodeId` values that Chrome assigns to DOM nodes internally.

In [`package/ego-browser/src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/ref-map.ts), the `RefMap` class stores this relationship:

```typescript
// RefMap – stores the mapping from refId to backendNodeId
export class RefMap {
  add(refId, backendNodeId, role, name, nth = undefined) {
    this.addWithFrame(refId, backendNodeId, role, name, nth, undefined);
  }
  // ... additional methods for frame-aware storage
}

```

Each entry binds a numeric `refId` (e.g., `21`) to its corresponding `backendNodeId`, along with optional metadata including the node's role, accessible name, nth-index, and frame identifier.

## Parsing and Resolving Snapshot References

When user code contains a snapshot reference, the `parseRef` helper extracts the numeric component. Located in [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts), this utility strips the `@` prefix and validates the input:

```typescript
// parseRef – turns “@42” into “42”
export function parseRef(input) {
  const trimmed = String(input || "").trim();
  // ... validation logic ...
  if (candidate && /^\d+$/.test(candidate)) {
    return candidate;          // e.g., “42”
  }
  // ... error handling ...
}

```

The resulting string is then used as a key in the RefMap lookup. If the reference exists, the associated `backendNodeId` is retrieved and passed to CDP commands for element interaction.

## Element Resolution via backendNodeId

The actual resolution logic resides in [`package/ego-browser/src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/element-resolver.ts). This module bridges the gap between the static snapshot reference and the live browser context:

```typescript
// element-resolver – resolve a ref to a backendNodeId
if (entry.backendNodeId !== undefined && entry.backendNodeId !== null) {
  // The ref points to a known backend node.
  // The resolver can now query CDP for the actual object.
}

```

By utilizing `backendNodeId`, ego-browser avoids the performance overhead of re-querying selectors on the page. The resolver immediately knows which CDP node to target, enabling direct method calls like `DOM.focus` or `DOM.querySelector` without traversing the DOM tree again.

## Why backendNodeId Provides Cross-Session Stability

The decision to link `@N` references to `backendNodeId` rather than transient JavaScript handles addresses three critical requirements for browser automation:

* **Stability**: CDP-provided `backendNodeId` values remain constant for a given DOM node while the page remains loaded, unlike ephemeral JavaScript object references that change between script evaluations.
* **Performance**: Resolving a reference becomes a constant-time `Map.get` operation rather than requiring expensive selector re-evaluation or DOM traversal.
* **Cross-Frame Support**: The RefMap optionally stores `frameId` alongside `backendNodeId`, allowing snapshot references to correctly target elements inside iframes without complex context switching logic.

This design ensures that an agent can execute `await click("@27")` in round one and `await type("@27", "text")` in round five, confident that both commands target the same underlying DOM node even if the accessibility tree has been regenerated.

## Handling Stale References and Automatic Re-Snapshotting

DOM mutations and navigation events can invalidate snapshot references. The runtime in [`package/ego-browser/src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts) monitors for missing `backendNodeId` entries during resolution. When a reference lookup fails—indicating the node no longer exists or the snapshot is outdated—the system automatically triggers a **re-snapshot**:

1. The runtime detects that `refMap.get(ref)` returns undefined or the `backendNodeId` is invalid.
2. A new accessibility snapshot is captured, generating fresh `backendNodeId` values.
3. The RefMap is rebuilt with updated reference IDs.
4. The original command is retried with the new mapping.

This self-healing mechanism allows scripts to recover from page changes without manual intervention, though best practices recommend taking explicit snapshots after known navigation events.

## Practical Usage Examples

The public API exposed in [`package/ego-browser/src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts) accepts snapshot references directly in interaction methods:

```typescript
// Example 1 – Taking a snapshot and inspecting refs
await ego.snapshot();               // builds a new RefMap
const refs = ego.refMap;            // internal map
for (const [refId, info] of refs.entries()) {
  console.log(`@${refId} → backendNodeId=${info.backendNodeId}`);
}

```

```typescript
// Example 2 – Using snapshot references in agent scripts
await click("@27");                 // resolves “27” via RefMap → backendNodeId
await type("@12", "hello world");   // same mechanism for typing

```

```typescript
// Example 3 – Manual CDP lookup using parsed reference
import { parseRef, RefMap } from 'ego-browser';
const ref = parseRef("@45");                 // “45”
const backendId = ego.refMap.get(ref).backendNodeId;
await ego.cdp('DOM.focus', { backendNodeId: backendId });

```

## Summary

* **Snapshot references** (`@N`) in ego-browser are lightweight pointers to CDP `backendNodeId` values stored in the **RefMap** class ([`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts)).
* The **parseRef** utility extracts numeric IDs from `@N` strings, enabling lookup in the reference map.
* **backendNodeId** provides stable element identification across script rounds, unlike transient JavaScript handles.
* The **element-resolver** ([`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts)) converts refIds into CDP-compatible backend node identifiers for direct DOM interaction.
* **Automatic re-snapshotting** occurs when references become stale, rebuilding the RefMap with fresh `backendNodeId` values to maintain script reliability.

## Frequently Asked Questions

### What happens if I use an invalid @N reference in ego-browser?

If you reference an ID not present in the current RefMap (e.g., `@999` when only 50 nodes exist), the runtime will attempt to resolve it and trigger an automatic re-snapshot. If the reference still cannot be resolved after refreshing the accessibility tree, the operation will throw an error indicating the element could not be found.

### Can snapshot references work across different iframe contexts?

Yes. The RefMap stores an optional `frameId` alongside each `backendNodeId`. When a snapshot includes nodes from cross-origin or same-origin iframes, the reference ID maintains its association with the correct frame context, allowing commands like `click("@5")` to target elements inside iframes without requiring manual frame switching.

### How is @N different from using CSS selectors?

CSS selectors require the browser to query the DOM dynamically, which is computationally expensive and can return different elements if the page structure changes. In contrast, `@N` references map directly to stable `backendNodeId` values through the RefMap, providing constant-time lookups and consistent targeting even as the DOM mutates between script executions.

### When does ego-browser automatically trigger a re-snapshot?

The runtime triggers a re-snapshot when element resolution fails due to a missing or invalid `backendNodeId` in the RefMap. This typically occurs after navigation events, significant DOM mutations that remove referenced nodes, or when a script attempts to use a reference from a previous session that has expired.