Error decoded
“node with id X not found in a11y tree”
You are not looking for an element that does not exist. You are looking for an element by an id that expired. That id was only ever valid inside one snapshot of the accessibility tree, and the tree moved before your action ran.
Direct answer · verified 2026-06-21
The error means an automation captured a node by its numeric id from an earlier accessibility-tree snapshot, then tried to act on that id after the tree had changed (a re-render, a navigation, a modal, or a focus shift). The id no longer maps to any node, so the lookup fails. Recover by taking a fresh snapshot immediately before each action and re-querying the element. The durable fix is to stop acting on ids: bind to a role plus name selector that is re-resolved against the live tree at action time, so there is no id to go stale.
The id is an index into a snapshot, not a handle on the element
Every accessibility-tree tool gives you a flattened view of the live tree at one instant. To make that view addressable it tags each node with an id: a number, a ref like @e7, a backend node id from the Chrome DevTools Protocol, or a UIA RuntimeId. That id is meaningful only inside the snapshot it was minted in. It is not a stable pointer to the button on screen; it is a row number in a table that gets thrown away and rebuilt.
So the moment the tree is rebuilt, the table is regenerated and the row numbers shuffle. Your saved id now points at nothing, or worse, at a different element. The tool detects the first case and raises node with id X not found in a11y tree. The same class of error shows up worded slightly differently across stacks: “Could not find node with id N in commit tree” in React DevTools, “Ref not found: @eN” in snapshot-ref browser agents, “Unregistered node” in some UI frameworks. Different words, one cause.
The exact moment it goes stale
Here is the lifecycle of a single click that fails. The id is born valid and dies between two steps you did not think changed anything.
snapshot id lifecycle
Nothing is broken in the app. The button is still on screen. The only thing that is wrong is that you are holding a number from a snapshot that no longer exists.
Recover right now, with the tool you already have
If you are on a snapshot-id API and just need the run to pass, the fix is mechanical. Shorten the distance between capture and action so the id has no time to expire.
Re-snapshot immediately before acting
Move the snapshot call to the line right above the action. Do not snapshot once at the top of a function and reuse ids ten lines down.
Re-query, never reuse the id
After any click that navigates or mutates, after any wait, and after any dialog, throw the old ids away and look the element up again in the fresh snapshot.
Clear the overlay first
If a consent banner or modal opened on top, that rebuild is what invalidated your id. Dismiss or interact with the covering element, then re-snapshot before retrying the original target.
If it is still missing, it is a timing problem
A fresh snapshot that still lacks the element means it has not rendered yet. Wait on a condition (visible, enabled) instead of sleeping a fixed number of milliseconds, then snapshot again.
The durable fix: bind to a selector, re-resolve at action time
Re-snapshotting treats the symptom. The cause is that your action is coupled to an ephemeral id. Remove the id from the contract entirely: describe the element by what it is (its role and name), and let the framework re-find it against the live tree at the instant you act. There is then no value to carry across a state change, so nothing can go stale.
# the stale-id pattern (every snapshot-id tool)
snapshot = agent.snapshot() # node 42 = the Save button, right now
save_id = find(snapshot, "Save").id # 42
agent.click(modal_appeared_then()) # tree rebuilds, ids reassigned
agent.click_by_id(save_id) # 42 is gone
# -> "node with id 42 not found in a11y tree"// the durable-selector pattern (Terminator)
let app = desktop.application("notepad")?;
// the selector is a *description*, not an index into a past snapshot
app.locator("role:Button|name:Save")?
.click(Some(Duration::from_secs(5))) // re-resolved against the LIVE tree here
.await?;
// no id is ever held across an action, so none can go staleThis is the design Terminator ships with. The selector is parsed into a semantic query, not a snapshot index. Its variants are role-and-name, automation id, name, text, attributes, and even relational ones like RightOf and Below an anchor element. None of them is an id from a past capture.
Where this lives in the source
In crates/terminator/src/locator.rs, the locator’s wait() method does not cache a node. Every time you act, it runs engine.find_element(&selector, root, timeout) on a blocking-safe thread, re-resolving the selector against the current tree and polling up to the timeout. The selector itself is defined as an enum in crates/terminator/src/selector.rs: Role { role, name }, Name, NativeId, Attributes, and relational variants. There is no snapshot-index variant in the enum, which is precisely why there is no id that can go missing from the tree.
The same selector string flows through the MCP server too. The click tool in crates/terminator-mcp-agent takes a selector such as role:Button|name:Save and resolves it at call time, so an AI agent driving a real app through Terminator never holds a stale id between tool calls either.
This is not only a browser problem
Most writing about this error assumes you are inside a browser, because that is where snapshot-ref agents are most common. But the failure mode is a property of any accessibility tree, and native desktop trees churn just as hard: a Windows UIA RuntimeId is invalidated when the control is re-created, an AX element reference on macOS dies when the app rebuilds a view or switches tabs, and a line-of-business app that redraws a grid throws away every node you were holding.
Terminator targets the whole OS rather than a single tab, so the stale-binding question matters across every app on the desktop, not just the page in front of you. The answer is the same everywhere: do not hold a reference into a tree that moves. Hold a description and re-resolve it. That is why the same role:Button|name:Save selector is portable across the Windows UIA adapter and the macOS AX adapter without you ever touching a platform-specific id.
Building an agent that keeps losing its element references?
Talk through how to drive native apps with durable selectors instead of snapshot ids that expire mid-run.
Frequently asked questions
Frequently asked questions
What does "node with id X not found in a11y tree" actually mean?
It means your automation tool captured a node from one accessibility-tree snapshot, recorded that node's numeric id, and then tried to act on that id later. Between the capture and the action, the tree changed, so the id no longer points to anything. The id was never a stable handle on the element; it was an index into a frozen snapshot that is now out of date.
Why does the tree change between snapshot and action?
Common triggers: the page or app re-rendered (React/Vue re-mount, virtualized list scroll), you navigated or submitted a form, a modal or consent banner opened on top, focus moved to a new window, or an async load swapped the content. Any of these rebuilds part of the tree and reassigns ids, so a previously valid id falls out.
What is the quickest way to recover from this error?
Take a fresh snapshot of the accessibility tree right before you act, then look up the element again in the new snapshot and use the new id. Never reuse an id across a navigation, a click that mutates the page, or a wait. If you re-snapshot and the element still is not present, you are waiting on something that has not rendered yet, which is a timing problem, not an id problem.
Is re-snapshotting the real fix or just a workaround?
Re-snapshotting is the correct recovery for any tool whose API is built around snapshot ids. It is a workaround for the underlying design: numeric ids are ephemeral. The structural fix is to stop binding to ids at all and bind to a durable selector (role plus name, automation id, or a relational query) that gets re-resolved against the live tree every time you act.
Does this happen with desktop apps too, or only browsers?
Both. Browser tools (CDP accessibility snapshots, Playwright a11y, agent-browser refs) hit it most visibly, but native desktop trees go stale the same way. A Windows UIA RuntimeId or an element pointer from one AX query becomes invalid when the app rebuilds its view, switches tabs, or opens a dialog. The cause is identical: you held a reference into a tree that moved.
How does Terminator avoid the stale-id problem?
Terminator never hands you a snapshot id to act on. You describe the element with a selector like role:Button|name:Save, and at action time its locator calls find_element against the live tree with a timeout, re-resolving the selector each time. Because the binding is semantic (role and name) rather than an index into a past capture, there is no id to go stale. See crates/terminator/src/locator.rs and crates/terminator/src/selector.rs.
When should I still expect a failure even with a selector?
If the element genuinely is not in the live tree (it has not rendered, it is in a different window you have not attached to, or it is drawn as raw pixels with no accessibility node), a selector returns a timeout rather than a stale-id error. That is the honest signal: the element is not there yet or is not exposed, which is a different problem from an id that expired.
Terminator is an open-source desktop automation framework for Windows and macOS that drives apps through native accessibility APIs, with a Playwright-shaped API for the whole OS and an MCP server for AI agents. The source is on GitHub. For more on how snapshot refs go stale in browser agents, the agent-browser docs describe the same re-snapshot recovery for the browser case.
Keep reading
Accessibility API desktop automation
Why driving apps through the accessibility tree beats screenshots and pixel matching for native desktop control.
RPA with accessibility-tree selectors
Selectors that survive layout changes, theme switches, and localization because they bind to role and name.
Verifying cross-platform desktop automation
How to confirm an action landed when the same selector has to work on Windows UIA and macOS AX.
Comments (••)
Leave a comment to see what others are saying.Public and anonymous. No signup.