How Snapshot References (@N) Work in ego-browser: Mapping to Backend Node IDs
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, the RefMap class stores this relationship:
// 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, this utility strips the @ prefix and validates the input:
// 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. This module bridges the gap between the static snapshot reference and the live browser context:
// 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
backendNodeIdvalues 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.getoperation rather than requiring expensive selector re-evaluation or DOM traversal. - Cross-Frame Support: The RefMap optionally stores
frameIdalongsidebackendNodeId, 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 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:
- The runtime detects that
refMap.get(ref)returns undefined or thebackendNodeIdis invalid. - A new accessibility snapshot is captured, generating fresh
backendNodeIdvalues. - The RefMap is rebuilt with updated reference IDs.
- 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 accepts snapshot references directly in interaction methods:
// 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}`);
}
// 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
// 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 CDPbackendNodeIdvalues stored in the RefMap class (src/ref-map.ts). - The parseRef utility extracts numeric IDs from
@Nstrings, 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) 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
backendNodeIdvalues 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →