If you have spent any time using Playwright CLI with a coding agent, you know the pattern. The agent navigates the page, finds the right element, does something useful, and then the session ends. You are left staring at a terminal full of ephemeral refs like e7 and e21 that expired the moment the page changed, trying to remember which element was which so you can write the actual test. There is a command built into Playwright CLI that closes this gap completely, and almost nobody is talking about it.
When Playwright CLI takes a snapshot, every interactive element on the page gets assigned a ref. These look like e7, e21, or f0e2 for elements inside frames. They are short, clean, and easy to work with inside an agent session. They are also completely ephemeral.
A ref is a session-scoped handle, not a locator. It maps to an element in the accessibility tree that was captured at a specific moment in time. The moment the page re-renders, navigates, or receives any update, the tree is rebuilt and the old refs are gone. There is no way to take a ref from a terminal snapshot and use it in a @playwright/test spec file. It will never resolve to anything.
The instinct most engineers reach for at this point is to reverse-engineer the locator manually. They look at the snapshot output, see something like button "Delete invoice 1042" [ref=e7], and write page.getByRole('button', { name: 'Delete invoice 1042' }) by hand. That works, but it only works because in this case the element has a clear accessible name that is obvious from the snapshot text. On a more complex page, with nested components, dynamic text, or icon-only buttons, the snapshot output does not give you enough information to confidently reconstruct the strongest locator. You are guessing, and Playwright's locator engine knows things about the accessibility tree that a terminal printout does not show you.
The other common fallback is re-running codegen. That means opening another browser session, repeating the interaction the agent already performed, and generating a script just to extract one locator from it. It works, but it is the long way around a problem that has a one-command solution.
Once you have a snapshot and a ref you care about, the command is straightforward:
playwright-cli generate-locator e7
That's it. The CLI talks to the daemon, inspects the live accessibility tree for the element behind that ref, and returns a locator string:
getByRole('button', { name: 'Delete invoice 1042' })
What makes this more useful than writing the locator by hand is what is happening under the hood. generate-locator uses the same locator resolution strategy as Playwright's built-in element picker in UI mode. It does not just read the visible text from the snapshot and wrap it in a getByRole call. It walks the accessibility tree, evaluates what semantic information is available for that specific element, and produces the strongest locator it can justify given what is actually there. ARIA role, accessible name, label association, test ID — it checks in priority order and returns the result that is least likely to break under normal application changes.
The practical difference is that you are getting Playwright's own opinion about the best way to target that element, not yours. On a straightforward button with a clear label, those two opinions will match. On anything more complex, Playwright's will be better.
By default, generate-locator returns its output wrapped in some surrounding context — the page URL, a snapshot reference, some formatting. That is fine for reading in the terminal, but it gets in the way if you want to use the output programmatically.
The --raw flag strips all of that:
playwright-cli generate-locator --raw e7
Output:
getByRole('button', { name: 'Delete invoice 1042' })
Just the locator string, nothing else. That makes it pipeable. On macOS and Linux you can feed it straight into your clipboard:
playwright-cli generate-locator --raw e7 | pbcopy
Or append it directly into a test file:
playwright-cli generate-locator --raw e7 >> test.spec.cjs
On Windows with PowerShell:
playwright-cli generate-locator --raw e7 | Set-Clipboard
None of these are workflows you would build around a locator you wrote by hand or extracted from codegen output. The --raw flag is a small detail that turns generate-locator from a convenience into something you can actually script around, and it is easy to overlook how much that one flag changes what the command is good for.
To make this concrete, here is what the same task looks like before and after.
Before: The agent navigates to a page during an exploratory session and interacts with an element you want to assert on in a test. The session ends. You open the spec file, try to remember what the element looked like, write a locator by hand, run the test, watch it fail because you got the role or the name slightly wrong, adjust, run it again. If the element was complex you might open codegen, repeat the entire interaction, and extract the locator from the generated script.
After: During the agent session, while the browser is still open and the ref is still valid, you run one command:
playwright-cli generate-locator --raw e7 | pbcopy
You switch to your spec file and paste. The test passes on the first run.
The key shift is timing. The agent session is not just an exploration step that happens before the real work starts. It is the moment when you have the most information about the page: the browser is open, the accessibility tree is live, and Playwright can inspect the exact element the agent just touched. generate-locator makes that the moment you capture the locator, not some reconstruction attempt that happens afterward from incomplete information.
The workflow also composes naturally with how CLI sessions already work. You are not adding a new tool or a new step. You are adding one command at the point in the session when you find something worth keeping.
Run generate-locator on a button with a clear accessible name and you get this:
getByRole('button', { name: 'Delete invoice 1042' })
Now run it on an icon-only button that has no aria-label and no visible text on the button itself:
getByRole('listitem').filter({ hasText: 'Q3 Financial Report' }).getByRole('button')
The tool did not fail. It did not return a CSS class selector. It walked up the accessibility tree, found the nearest element it could use as a stable anchor, which in this case was the list item containing the text "Q3 Financial Report", and built a locator that works by filtering down from there.
But look at what each locator is actually doing. The first one describes what the button is. The second one describes where the button lives. That distinction matters more than it might seem.
A self-contained locator like getByRole('button', { name: 'Delete invoice 1042' }) survives layout changes, component refactors, and DOM restructuring, as long as a button with that accessible name exists on the page, the locator resolves. A positional locator like the one generated for the icon button is coupled to its surroundings. Rename the "Q3 Financial Report" text, move the button to a different list item, or add a second unlabelled button to the same row, and the locator either breaks or silently resolves to the wrong element.
The deeper point is that generate-locator output is a direct readout of how well your application describes its own interface to the accessibility tree. A clean getByRole with a name means the element is properly described and any user, agent, or assistive technology can identify it unambiguously. A chained filter means the element has no identity of its own and something nearby is doing the work it should be doing itself.
This makes generate-locator useful beyond the agent workflow. Run it across the interactive elements on a page you are about to write tests for and look at the output. Every chained filter, every nth(), every CSS fallback is a signal. Not a bug, not a test failure, just a signal that an element is harder to target than it needs to be, and that the fix is one aria-label away.
generate-locator is a small command with an outsized effect on how useful a Playwright CLI session actually is. It changes the agent session from something you do before writing tests into something that produces test artifacts directly. The locator you get is not an approximation based on what you remember the element looked like. It is Playwright's own assessment of the best way to target that element, produced at the moment when the browser is open and the information is most complete.
The accessibility angle is worth sitting with. Most teams find out about their accessibility debt when an audit fails or a screen reader user files a complaint. generate-locator surfaces the same information earlier, as a side effect of work you were already doing. A locator that chains through parent elements to find an unlabelled button is not just a brittle test artifact. It is a specific, actionable signal about a specific element that needs an accessible name. That is a more useful output than a general accessibility score.
If you are already using Playwright CLI with a coding agent, add generate-locator --raw to the points in your session where you find something worth asserting on. If you are not using it yet, that workflow is a reasonable place to start.