How to Use Snapshot References (backendNodeId) for Element Interaction in Ego Lite

Ego Lite leverages numeric backendNodeId identifiers from Chrome’s Accessibility (AX) tree to create stable snapshot references (e.g., @21) that enable direct, high-performance element interaction without expensive DOM queries.

The citrolabs/ego-lite browser automation framework captures DOM snapshots as lightweight numeric references tied to Chrome's internal node identifiers. This approach eliminates the fragility of CSS selectors by storing backendNodeId values—the same identifiers Chrome DevTools Protocol (CDP) uses to address elements directly—enabling faster, more stable automation scripts.

Understanding Snapshot References and backendNodeId

When Ego Lite captures a page snapshot via Accessibility.getFullAXTree, it generates snapshot references that map human-readable strings like @21 to Chrome's internal backendNodeId integers. These identifiers point directly to nodes in the browser's accessibility tree, providing a stable addressing mechanism that survives DOM mutations until the element is removed or recreated.

The reference lifecycle follows four distinct phases:

  1. Snapshot Capture – The runtime invokes Accessibility.getFullAXTree to build the AX tree and extract backendDOMNodeId values from each node.
  2. Reference Storage – The RefMap class in src/ref-map.ts persists mappings between reference strings (e.g., @21), backendNodeId integers, element roles, and accessibility names.
  3. Direct Resolution – When a helper receives a reference like @21, the parseRef function (lines 44-53 in src/ref-map.ts) extracts the numeric identifier for lookup.
  4. Stale Recovery – If the stored backendNodeId no longer exists, the system falls back to role/name-based lookup via findBackendNodeIdByRoleName.

Resolving Elements from References

The src/element-resolver.ts file contains the core logic for transforming snapshot references into actionable element handles. Two primary functions handle different interaction requirements:

resolveElementCenter (lines 63-104): Converts a reference into screen coordinates by first looking up the stored backendNodeId, then calling DOM.getBoxModel via CDP to calculate the element's center point. If the node is stale, it triggers findBackendNodeIdByRoleName to refresh the mapping.

resolveElementObjectId (lines 149-202): Returns a CDP runtime object ID required for event dispatch. This function queries the RefMap for the backendNodeId, then executes DOM.resolveNode to obtain a live object reference, falling back to accessibility tree traversal on failure.

Both functions rely on parseRef to normalize input formats including @N, ref=N, or plain numeric strings.

Practical Implementation Examples

The following examples demonstrate how to interact with elements using snapshot references in Ego Lite scripts. These helpers are automatically injected into the runtime environment.

Clicking Elements by Reference

Use the click helper to dispatch mouse events via stored backend node IDs:

// Target element from previous snapshot with reference @21
const { click } = ego;
await click('@21');

Internally, click invokes resolveElementObjectId, which parses @21 through parseRef, retrieves the entry from RefMap, and calls DOM.resolveNode with the stored backendNodeId. The resulting object ID is passed to Input.dispatchMouseEvent to execute the click.

Retrieving Element Coordinates

Extract precise center coordinates for visual validation or custom event dispatch:

const { getCenter } = ego;
const { x, y } = await getCenter('@42');
console.log(`Element center at (${x}, ${y})`);

This flows through resolveElementCenter, which uses DOM.getBoxModel on the stored backendNodeId, then calculates the center via boxModelCenter (lines 53-68 in src/element-resolver.ts).

Handling Stale References

When page navigation or DOM updates invalidate stored identifiers, force a fresh snapshot:

// Reference @33 may be stale after previous actions
await ego.waitForRef('@33');  // Triggers new snapshot if needed
await ego.click('@33');       // Safe execution on fresh backendNodeId

The waitForRef method guarantees that RefMap contains a valid entry before proceeding, preventing errors from stale backendNodeId values.

Manual Reference Management

For custom tooling scenarios, manually inject references into the map:

// Directly register a backendNodeId with custom reference '99'
ego.refMap.add('99', 123456, 'button', 'Submit');
await ego.click('ref=99');

The RefMap.add method (lines 8-10 in src/ref-map.ts) stores the mapping immediately, allowing subsequent helper calls to resolve the reference through standard CDP methods.

Key Source Files in citrolabs/ego-lite

The snapshot reference system spans four critical files in the repository:

  • src/ref-map.ts: Implements the RefMap class and parseRef utility for reference string normalization and storage.
  • src/element-resolver.ts: Contains resolveElementCenter, resolveElementObjectId, and findBackendNodeIdByRoleName for CDP-based element resolution.
  • src/helpers.ts: Exposes the public API (click, getCenter, waitForRef) that orchestrates resolution and interaction.
  • src/browser-runtime.ts: Manages snapshot creation via Accessibility.getFullAXTree and populates the RefMap with fresh backendNodeId values.

Summary

  • Snapshot references in Ego Lite map strings like @21 to Chrome's internal backendNodeId integers from the accessibility tree.
  • The RefMap class (src/ref-map.ts) persists these mappings, while parseRef normalizes input formats.
  • Element resolution occurs through CDP methods DOM.getBoxModel and DOM.resolveNode, providing direct access without DOM querying.
  • Automatic fallback to role/name-based lookup occurs when backendNodeId values become stale, ensuring robust test execution.
  • References remain valid across multiple actions until the DOM node is recreated or removed, offering superior performance compared to CSS selectors.

Frequently Asked Questions

What is a backendNodeId in Chrome DevTools Protocol?

The backendNodeId is a numeric identifier assigned to each node in Chrome's internal accessibility (AX) tree representation. Unlike DOM IDs, these integers are stable for the lifetime of the node and allow direct addressing via CDP methods like DOM.resolveNode and DOM.getBoxModel without requiring CSS selector evaluation or JavaScript execution in the target page.

How does Ego Lite handle stale snapshot references?

When a stored backendNodeId no longer resolves (typically due to DOM removal or re-creation), Ego Lite triggers fallback resolution via findBackendNodeIdByRoleName in src/element-resolver.ts (lines 96-124). This function re-queries the full AX tree using Accessibility.getFullAXTree, then matches nodes by their accessibility role and name to locate the new backendDOMNodeId, effectively refreshing the reference mapping.

What performance benefits do snapshot references provide?

Snapshot references eliminate the runtime overhead of CSS selector parsing, DOM traversal, and repeated accessibility tree queries during multi-step interactions. Because backendNodeId values provide direct handles to internal browser structures, subsequent actions like clicks and coordinate lookups execute via lightweight CDP calls rather than expensive DOM operations, significantly reducing latency in automation sequences.

Can snapshot references persist across page navigations?

No, backendNodeId values are tied to specific document lifecycles and invalidate when navigating to new pages. Ego Lite provides the waitForRef helper to regenerate snapshots after navigation, ensuring fresh references for the new document context. The framework automatically detects stale identifiers and can refresh them using role/name matching when possible, though best practice involves re-capturing snapshots after significant DOM changes.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →