# Understanding the @N Ref System (backendNodeId) in ego-browser and How RefMap Rebuilds on Snapshots

> Explore the @N ref system in ego-browser and how it uses backendNodeId for stable DOM references. Learn how the RefMap rebuilds on snapshots to maintain page state synchronization.

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

---

**The @N ref system in ego-browser leverages Chrome DevTools Protocol backend node IDs to create stable DOM element references, automatically rebuilding the internal RefMap on each snapshot to ensure references stay synchronized with the current page state.**

The citrolabs/ego-lite repository provides a lightweight browser automation framework that enables AI agents to interact with web pages using concise element addressing. This article explains how the **@N ref system** (backendNodeId) creates persistent handles to DOM elements and how the framework reconstructs its reference map when snapshots are taken.

## What Is the @N Ref System?

The @N syntax represents a **backend node ID**—a unique identifier assigned by the Chrome DevTools Protocol (CDP) to every DOM node. In `ego-browser`, writing `@42` tells the runtime to locate the element whose `backendNodeId` is `42` in the current CDP session.

These IDs are stable for the lifetime of the page, meaning `@42` consistently points to the same node across multiple interactions. However, the IDs are session-specific; they change when the page reloads or when a fresh snapshot initializes a new CDP session. The runtime stores this mapping in a singleton `browserRefMap` (defined in [`package/ego-browser/src/ref-state.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/ref-state.ts)), which parses `@N` strings and resolves them to live DOM nodes.

## How the RefMap Rebuilds on Snapshots

When the page state changes, the ref-map must synchronize with the new DOM structure. This happens through a four-stage pipeline:

### 1. Snapshot Request

Functions like `snapshotRaw()` in [`package/ego-browser/src/driver/observe.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/driver/observe.ts) invoke the browser runtime's snapshot capability. This call returns a result object containing a `refs` array, where each entry includes the `backendNodeId`, accessibility role, and name for every node in the current DOM tree.

### 2. Conversion and Population

The helper `browserSnapshotRefsToRefMap()` (located in [`package/ego-browser/src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts)) iterates over the `refs` array. For each entry, it populates the `browserRefMap` using `RefMap.add()`, associating the numeric ID with the node's metadata and current state.

### 3. Clearing Stale Data

Before inserting new entries, the runtime explicitly calls `refMap.clear()` (lines 9–11 in [`package/ego-browser/src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts)). This prevents orphaned references from previous snapshots from polluting the current session.

### 4. Lazy Refresh Mechanism

If code attempts to resolve a selector like `@123` while the map is empty, the `ensureRefMapForRef()` function in [`package/ego-browser/src/ref-state.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/ref-state.ts) automatically triggers a snapshot. This lazy initialization (lines 12–24) guarantees that refs are always resolvable without requiring manual snapshot calls.

## Practical Code Examples

The ref system integrates seamlessly with helper functions and manual snapshot workflows:

```typescript
// Resolve element center using @N notation
await helpers.elementCenter("@42"); 
// Internally calls ensureRefMapForRef then resolveElementCenter

```

```typescript
// Manual snapshot inspection
const snap = await egoBrowser.snapshotRaw({ includeStableLocator: true });
console.log(snap.refs.map(r => `${r.backendNodeId}: ${r.role}`));
// RefMap is now populated for subsequent @N queries

```

```typescript
// Force ref-map availability
await egoBrowser.ensureRefMapForRef("@99"); 
// Triggers snapshot only if map is empty

```

## Summary

- **@N syntax** represents CDP `backendNodeId` values, providing stable element addresses for the page lifetime.
- The **RefMap** rebuilds by clearing existing entries and repopulating from the `refs` array returned by `snapshotRaw()`.
- **Stale data prevention** occurs via `refMap.clear()` in [`browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts) before each snapshot ingestion.
- **Lazy loading** via `ensureRefMapForRef()` eliminates the need for manual snapshot management when using refs.

## Frequently Asked Questions

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

Backend node IDs are tied to a specific CDP session for a single page load. After a reload, the browser assigns new IDs to all DOM nodes, so previous refs like `@42` become invalid and will fail to resolve until you capture a fresh snapshot.

### How does ego-browser prevent stale refs from persisting between snapshots?

Before processing new snapshot data, the `browserSnapshotRefsToRefMap()` function in [`package/ego-browser/src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts) calls `refMap.clear()` to wipe the existing map. This ensures only current DOM nodes from the latest `refs` array are addressable.

### Can I use @N refs without explicitly calling snapshot methods?

Yes. The `ensureRefMapForRef()` function in [`package/ego-browser/src/ref-state.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/ref-state.ts) automatically triggers a snapshot if the RefMap is empty when you attempt to resolve an `@N` reference. This lazy initialization allows immediate use of ref-based selectors without manual state management.

### Where does the backendNodeId originate?

The ID originates from Chrome DevTools Protocol's DOM domain. When `snapshotRaw()` executes in [`package/ego-browser/src/driver/observe.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/driver/observe.ts), it queries the browser for the current DOM tree, and each node's `backendNodeId` is captured in the `refs` array that ultimately populates the RefMap.