n8n-io/n8n · error · StaleRefError
Stale element ref: ${ref}
Error message
Stale element ref: ${ref} What it means
StaleRefError is thrown by PlaywrightAdapter.resolveRef when an aria-ref selector resolves to zero elements on the page. The adapter builds a locator with `aria-ref=${ref}` and calls locator.count(); a count of 0 means the ref no longer points to a live DOM node. Refs are only valid against the snapshot that produced them, so any DOM mutation between snapshot and action invalidates them.
Source
Thrown at packages/@n8n/mcp-browser/src/adapters/playwright.ts:885
const html = await locator.evaluate((el) => el.outerHTML);
return { html, url: page.url() };
}
return { html: await page.content(), url: page.url() };
}
// =========================================================================
// Ref resolution — uses Playwright's built-in aria-ref selector engine
// =========================================================================
async resolveRef(pageId: string, ref: string): Promise<unknown> {
const { page } = await this.ensurePage(pageId);
const locator = page.locator(`aria-ref=${ref}`);
// Verify the element exists
try {
const count = await locator.count();
if (count === 0) throw new StaleRefError(ref);
} catch (error) {
if (error instanceof StaleRefError) throw error;
throw new StaleRefError(ref);
}
return locator;
}
// =========================================================================
// Private helpers
// =========================================================================
private requireContext(): BrowserContext {
if (!this.context) throw new Error('Browser context not initialized');
return this.context;
}
private requirePage(pageId: string): PageState {View on GitHub (pinned to 5ac6606e81)
Solutions
- Call browser_snapshot again on the same pageId and use the freshly returned refs for the next action.
- Verify the pageId passed to the action matches the pageId used for the snapshot that generated the ref.
- After any tool call that can navigate, re-snapshot before issuing element-targeted actions.
Example fix
// before — ref is stale after navigation
await adapter.click('page-1', { ref: 'e12' }); // throws StaleRefError
// after — refresh snapshot, use new ref
const snap = await snapshot('page-1');
await adapter.click('page-1', { ref: snap.refs[0] }); Defensive patterns
Strategy: retry
Validate before calling
// Before acting on a ref, verify it still resolves.
const count = await page.locator(`aria-ref=${ref}`).count();
if (count === 0) { const snap = await snapshot(pageId); ref = pickFreshRef(snap); } Type guard
function isFreshRef(snap: Snapshot, ref: string): boolean {
return snap.elements.some((e) => e.ref === ref);
} Try / catch
try {
await adapter.click(pageId, { ref });
} catch (e) {
if (e instanceof StaleRefError) {
const snap = await snapshot(pageId);
await adapter.click(pageId, { ref: pickFreshRef(snap) });
} else throw e;
} Prevention
- Always re-snapshot after any tool call that may navigate or mutate the DOM.
- Keep refs scoped to a single pageId — never reuse across tabs.
- Treat refs as single-use; capture, act, discard.
When it happens
Trigger: Calling any browser interaction tool (click, type, fill) with a ref obtained from an older browser_snapshot after the page has navigated, reloaded, or been mutated by JS. Also triggered when a ref was copied from a different page/tab than the active one.
Common situations: Agent takes a snapshot, performs an action that triggers navigation (form submit, SPA route change), then tries to reuse the old ref. Refs are also invalidated by lazy-loaded content replacing nodes, or by switching activePageId without refreshing refs.
Related errors
- No executable path for ${browser}
- No file chooser pending and no element target provided
- Page not found: ${pageId}
- The marker "${redactedKey}" was not found.
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/c6e78a8dc50347df.
Report an issue: GitHub.