Skip to main content
Each recipe is a situation, the call that verifies it, and what came back. Every response on this page was captured from a running app on 2026-08-16. Refs like e605 are session-specific: take your own from reticle_look { action: "find" } before copying a call.

”Did the login actually work?”

One call, three conditions. The signal fired, exactly one request went out, and nothing was logged.
Each child predicate reports its own evidence, in order. count: 1 is the part that catches double-submit, and it costs nothing to ask for.

”Did navigating actually change the view?”

The signal carries the destination in its payload, so one predicate proves both that navigation happened and that it went to the right place.

”Did the modal open?” and the lesson in getting it wrong

Here is a real failure, kept because it teaches more than another pass.
Read the summary. The modal did open. newDeployOpen went false to true and the app fired modal:opened. capsule.blastRadius says the same thing as a list: the click moved the app store and fired modal:opened, and nothing else. The assertion was wrong, not the app. That modal is a div with no role="dialog", so a role query could never match it. (Which is also an accessibility bug worth fixing, found for free.) The right predicate was sitting in the response the whole time:
When an assertion fails, read summary.signals and summary.stateDiffs before you assume the feature is broken. They tell you what actually happened, and usually hand you the predicate you should have written.

”Nothing matched, and I do not know why”

An empty reticle_look { action: "find" } result tells you what is there:
The testid existed, on another route. hint.route says where you actually are and presentTestids says what is here instead, for 101 tokens and no snapshot. presentRegions is the other half: a list with childCount: 0 is an empty state, which is a different bug from a list that failed to render at all.

”Is this button destructive?”

Reticle refuses controls that look destructive, before acting:
Confirm it explicitly when you mean it:
The matcher reads the control’s label. It stops a control whose words destroy something (delete, remove, revoke, reset…) or move money (pay, buy, transfer, refund…). It can stop a harmless control that uses one of those words, for example “Remove filter”. That is the trade: a false stop costs you one argument, and a false start costs you a deleted record.

”Did my change break anything else?”

Give it the files you edited, or a git ref. It works out which saved flows cover them and replays only those.

”What is even testable in this app?”

The best first call on an unfamiliar codebase:
That is the app stating what it considers testable, which beats snapshotting the DOM and inferring it from element names.

”The error state has never been tested”

Most error states have never run. Force one:
Then drive the flow and assert the error UI appears. Pass clear: true to turn mocking off.
This one needs a browser that Reticle controls: a leased tab (reticle_lease) or a driven browser (reticle drive <url>, or RETICLE_CDP_URL). Mocks go through CDP, and the always-on SDK in your own tab cannot apply them. Against your own tab the call returns
On a lease or a driven browser the same call answers { "applied": true, "count": 1 }. reticle_viewport follows the same rule and also reports no-cdp-provider against your own tab. reticle_screenshot and reticle_visual_diff also work on a lease or a driven browser, and report no-visual-provider against your own tab.

”I want this to run on every PR”

Record it once, replay it forever:
Omit names to replay every saved flow. Either way you get one consolidated verdict, and only the failures carry detail:
It also returns flaky, the flows it has seen both pass and fail on unchanged code. Treat that list as a bug in the flow, not in the app.

The predicate grammar

Every kind, every field, and which ones actually prove something.

Best practices

The habits behind these recipes, and the mistakes that produce a confident wrong pass.
Last modified on October 25, 2026