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

  1. Call browser_snapshot again on the same pageId and use the freshly returned refs for the next action.
  2. Verify the pageId passed to the action matches the pageId used for the snapshot that generated the ref.
  3. 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

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


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/c6e78a8dc50347df. Report an issue: GitHub.