# Best Practices for Using ego-lite: Reliable Browser Automation for AI Agents

> Master ego-lite browser automation for AI agents. Learn best practices for stable selectors, state verification, and isolated task spaces to enhance reliability and efficiency. Optimize your automation workflows.

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

---

**The best practices for using ego-lite center on isolating work in dedicated task spaces, preferring stable `loc=` selectors over ephemeral `@N` references, and always verifying state changes with `snapshotText()` or `captureScreenshot()` before proceeding.**

ego-lite is a Chromium-based browser built from the ground up to let AI agents work side-by-side with human users. Its architecture revolves around isolated task spaces, a thin Node.js runtime (`ego-browser`) that injects a rich set of helpers, and a highly-optimized snapshot system. Understanding these core concepts is essential for writing reliable, token-efficient automation scripts that handle complex web interactions without brittle selectors or failed handoffs.

## Core Architecture Components

Understanding the five core components of ego-lite is the foundation for applying best practices effectively.

### Task Spaces

Task spaces are isolated browsing contexts that inherit the user’s login state. Agents own a space, the user can take control, and spaces can be handed off or closed. According to the canonical documentation in [`skills/ego-browser/SKILL.md`](https://github.com/citrolabs/ego-lite/blob/main/skills/ego-browser/SKILL.md), proper task-space management prevents duplicate contexts and ensures persistence across heredoc script rounds.

### Helper Context

The `HelperContext` singleton registers all public helpers (`click`, `snapshotText`, `js`, etc.) and injects them into the heredoc script at runtime. This registration logic and ownership enforcement live in [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts), which serves as the primary interface between your automation code and the browser runtime.

### Snapshot Engine

The snapshot engine produces a semantic tree via `snapshotText()` with stable `@N` refs and `loc=` selectors, enabling fast, token-efficient observations. The implementation in [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) builds this accessibility tree and manages the mapping between semantic elements and their CDP-based selectors.

### Driver Layer

Low-level browser interaction is handled by the driver layer, including CDP transport in [`src/cdp-eval.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/cdp-eval.ts) (which implements `js()` and `cdp()` wrappers), along with navigation, pointer, keyboard, and file-upload drivers under `src/driver/`.

### Learning Subsystem

Site-specific "learnings" enrich the skill set with custom tools for complex applications like Notion or Google Docs. This subsystem is managed by [`src/learning/index.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/learning/index.ts), which injects domain-specific helpers when recognized sites are detected.

## Seven Key Design Principles

Following these principles ensures your ego-lite automations remain stable, efficient, and maintainable.

### 1. Isolation First

Always start a script with `useOrCreateTaskSpace(name)` (or `takeOverTaskSpace` after a hand-off). This guarantees that the agent re-uses the same tabs across heredoc rounds and prevents accidental creation of duplicate spaces that waste resources and fragment state.

### 2. Stable Locators

Prefer `loc=` selectors (e.g., `loc=css:button.primary`) or robust CSS/XPath strings over raw `@N` refs. Because `@N` references are refreshed with each `snapshotText()` call, they are ephemeral and prone to invalidation during DOM updates, whereas `loc=` selectors derive from the accessibility tree and remain valid across snapshots.

### 3. Workflow Selection

Choose the most appropriate interaction pattern for your target:

- **Semantic** – `snapshotText()` plus refs/locators (default for normal pages with standard DOM).
- **Visual** – `captureScreenshot()` plus coordinate actions (for canvas-heavy editors where DOM elements are hidden).
- **Direct CDP** – `js()` or `cdp()` for low-level operations requiring custom Chrome DevTools Protocol commands.

### 4. Control Handoff

When a step requires user interaction (e.g., CAPTCHA), call `handOffTaskSpace()` and wait for the user’s confirmation before proceeding. Never retry a failed operation that requires human input; instead, surface a clear Ask and block with `waitForAgentControl()`.

### 5. Explicit Cleanup

End every successful automation with `completeTaskSpace(id, { keep: false })`. Use `{ keep: true }` only when the user explicitly wants the page to remain open for further manual inspection.

### 6. Verification Loop

After any navigation, click, or input, re-run `snapshotText()`, `pageInfo()`, or `captureScreenshot()` before assuming success. This guards against transient failures, delayed DOM re-renders, and race conditions in single-page applications.

### 7. Minimal Token Use

Because the agent sends code as plain text, keep each heredoc focused: observe → act → verify → `cliLog` the result. Large monolithic scripts waste tokens, increase latency, and are harder to debug when failures occur.

## Implementation Patterns and Code Examples

These concrete patterns demonstrate the best practices in action.

### Creating and Reusing Task Spaces

Always establish your execution context before performing any browser actions.

```js
// Start (or resume) a dedicated space for the whole task
const task = await useOrCreateTaskSpace('search github issues');

// Open a new tab or reuse an existing one
await openOrReuseTab('https://github.com/citrolabs/ego-lite/issues', {
  wait: true,
  timeout: 20,
});

```

*Why this works*: The space persists across subsequent heredocs, allowing the agent to keep the same tabs and avoid re-logins.

### Semantic Workflow with Stable Selectors

Use `loc=` selectors for reliable element interaction on standard web pages.

```js
// Grab a full-page snapshot (default scope is fine)
const snap = await snapshotText();

// Use a stable selector from the snapshot output
await click('loc=css:a[href*="issue"]', { label: 'open first issue' });

// Verify navigation succeeded
cliLog(await pageInfo());        // prints { url, title, … }

```

*Why this works*: `loc=` selectors survive across snapshots; they are derived from the accessibility tree in [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) and are less brittle than raw `@N` refs.

### Visual Workflow for Rich Editors

When the DOM is opaque or canvas-based, switch to coordinate-based interaction.

```js
// Capture a screenshot, then click by coordinates
await captureScreenshot();      // optional visual debug
await click({ x: 420, y: 260 }, { label: 'place cursor in canvas' });
await typeText('Hello world!');

// Confirm the change with another screenshot
cliLog('Result screenshot saved');
await captureScreenshot();

```

*Why this works*: Rich editors often hide DOM elements; coordinate actions coupled with visual verification are more reliable than attempting to target non-existent accessibility nodes.

### Direct CDP for Custom Logic

Execute complex page-side logic in a single round-trip to minimize overhead.

```js
// Run a single-shot script inside the page
const titles = await js(String.raw`(() => {
  return [...document.querySelectorAll('h1')].map(el => el.innerText);
})()`);

cliLog('Page titles: ' + JSON.stringify(titles));

```

*Why this works*: Keeps all browser-side logic inside one `js()` call via the wrapper in [`src/cdp-eval.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/cdp-eval.ts), avoiding the overhead of multiple evaluations and reducing token consumption.

### Handling Human-in-the-Loop Interactions

Properly manage ownership when users must intervene.

```js
// Ask the user to solve a captcha
await handOffTaskSpace(task.id);
await waitForAgentControl(task.id);   // block until user resumes

// After user confirms, take back control and continue
await takeOverTaskSpace(task.id);
await click('button.submit', { label: 'submit after captcha' });

```

*Why this works*: Respects the ownership model defined in the task-space lifecycle and prevents the agent from hard-failing when the user is in control.

### Resource Cleanup

Always terminate sessions cleanly to prevent resource leaks.

```js
// Close everything once the job is done
await completeTaskSpace(task.id, { keep: false });
cliLog('Task completed and space closed.');

```

*Why this works*: Guarantees no stray tabs or spaces linger, keeping the browser tidy for future tasks and preventing the accumulation of orphaned CDP sessions.

## Essential Source Files

These files embody the design decisions that make ego-lite a fast, token-efficient, and agent-friendly platform:

| File | Purpose |
|------|---------|
| [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) | Registers all public helpers (`click`, `snapshotText`, `js`, …) and enforces ownership checks. |
| [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) | Core snapshot and CDP transport layer; builds the semantic tree used by `snapshotText`. |
| [`src/cdp-eval.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/cdp-eval.ts) | Implements `js()` and `cdp()` wrappers for evaluating code inside the page. |
| `src/driver/*` | Low-level drivers for navigation, pointer, keyboard, file upload, and event handling. |
| [`skills/ego-browser/SKILL.md`](https://github.com/citrolabs/ego-lite/blob/main/skills/ego-browser/SKILL.md) | Canonical user-facing documentation; defines the public helper surface and workflow recommendations. |

## Summary

- **Isolate every task** in a dedicated space using `useOrCreateTaskSpace()` to maintain persistence across script rounds.
- **Prefer `loc=` selectors** over `@N` references for stability across accessibility tree snapshots.
- **Verify every action** with `snapshotText()` or `captureScreenshot()` to guard against DOM changes and navigation failures.
- **Hand off control** to the user via `handOffTaskSpace()` for interactive steps like CAPTCHAs, and never retry failed human-required steps.
- **Always clean up** completed tasks with `completeTaskSpace(id, { keep: false })` to prevent resource leaks and session fragmentation.
- **Keep heredocs minimal**: observe, act, verify, then log results to optimize token usage and debuggability.

## Frequently Asked Questions

### What is a task space in ego-lite?

A task space is an isolated browsing context that inherits the user’s login state and is owned by either the agent or the human user. According to [`skills/ego-browser/SKILL.md`](https://github.com/citrolabs/ego-lite/blob/main/skills/ego-browser/SKILL.md), task spaces allow agents to persist tabs and cookies across multiple heredoc script executions while preventing interference with the user’s main browsing session or other concurrent automations.

### How do `loc=` selectors differ from `@N` references?

`loc=` selectors (e.g., `loc=css:button.primary`) are stable CSS or XPath-based locators derived from the accessibility tree that remain valid across multiple `snapshotText()` calls. In contrast, `@N` references (like `@1`, `@2`) are ephemeral numeric IDs assigned to elements during a single snapshot capture, as implemented in [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts), and become invalid when the DOM updates or a new snapshot is taken.

### When should I use the visual workflow instead of the semantic workflow?

Use the **semantic workflow** (`snapshotText()` with locators) for standard web pages with accessible DOM elements. Switch to the **visual workflow** (`captureScreenshot()` with coordinate-based clicking) when automating rich text editors, canvas-based applications, or other interfaces where the DOM is hidden or non-semantic, making traditional selectors unreliable.

### How do I properly handle user interactions like CAPTCHAs?

When encountering a CAPTCHA or any step requiring human judgment, call `handOffTaskSpace(task.id)` to transfer ownership to the user, then block execution with `waitForAgentControl(task.id)`. Once the user completes the interaction and control returns, resume automation with `takeOverTaskSpace(task.id)`. Never implement retry loops for human-required steps; instead, surface clear asks and wait for explicit confirmation.