Core Usage

Waiting & Asserting

When an agent goes off the rails, it's usually because it waited wrong, not because it picked the wrong element. After a click or a fill, the page is still moving — what you wait for and how you confirm the action really landed decides whether your script is solid or brittle. This page spells out how wait waits, how expect asserts, how --observe shows you what changed, and how --if-present keeps optional steps idempotent.

observed.status is complete, partial, or unavailable, independently of the action success value. Missing evidence can produce changed:null, never a fabricated empty page or URL. A missing baseline returns the available after-tree instead of a diff. Incomplete observations include errors and retryAction:false; inspect current state rather than replaying the action.

If only the post-action capture fails, observed.refs says what became of the session's refs. kept-unverified: the previous snapshot's refs are kept, but a later command may use one only after chrome-use confirms live that the tab, frame, document and element are unchanged; otherwise the command is refused before it acts, and a ref is never re-anchored by role or name. Refs from a DOM-walk snapshot (snapshot --dom) are always refused in this state. dropped: the page may be a different document now. Either way, run snapshot -i for current refs, and do not repeat the action.

Pick the right wait condition — wait

After every action that changes the page, wait for a specific condition instead of blindly sleeping a fixed number of milliseconds. wait supports these forms:

bash
$ chrome-use wait @e1                     # until an element appears
$ chrome-use wait 2000                    # dumb sleep, milliseconds (last resort)
$ chrome-use wait --text "Success"        # until this text shows up on the page
$ chrome-use wait --url "**/dashboard"    # until the URL matches the pattern (glob)
$ chrome-use wait --load networkidle       # until the network goes idle (post-navigation settle)
$ chrome-use wait --load domcontentloaded  # until DOMContentLoaded
$ chrome-use wait --fn "window.myApp.ready === true"  # until a JS condition holds

After any action that changes the page, pick one of these three:

⚠️ Avoid bare wait 2000
Outside of debugging, don't use a fixed-millisecond wait 2000 — it makes scripts slow and brittle. The timeout defaults to 25 seconds, so waiting on a real condition never costs much, and it continues the instant the condition is met.

The read waits for you — settle

An observation is only worth what the page was doing when it was taken, so snapshot and --observe wait for the page to stop changing before they capture. You do not have to guess a sleep in front of them:

Whichever takes longest wins, bounded by a 1 second ceiling. A static page costs about 100ms; a click that fires an XHR waits for the response instead of returning the pre-response tree as though it were the result.

They differ in one way. A plain snapshot has no action to react to, so a still page is its answer and it returns as soon as everything is quiet. After a mutating action, --observe keeps watching for a first reaction for half the ceiling (500ms by default) before it is willing to report changed:false — otherwise a control that renders on a 300ms timer reads as "nothing happened". Only actions that really change nothing pay that; anything that reacts ends the window at once, including a re-render the action's own handler made while it was being dispatched (a click that redraws a list synchronously settles in about the 100ms quiet window, not 500ms).

observed.status: complete means the capture is complete, not the task: a page that shows Loading and renders its result on a timer is quiet at that moment. Follow with wait --text "Report ready" (or the page's own final signal).

bash
$ chrome-use snapshot -i --settle-ms 3000   # raise the ceiling for a slow page
$ chrome-use snapshot -i --no-settle        # capture now, mid-flight
⚠️ A ceiling that expires says so
When the page is still moving at the ceiling, the reply carries Page had not settled after 1000ms (request in flight still active) — this capture may be mid-transition. Re-read to confirm, or raise the ceiling with AGENT_BROWSER_SETTLE_MS. That means the tree may be mid-transition — re-read before trusting it. When the network held it, the line names up to three requests (host and path, never the query) and how long each has been in flight (settle.pendingRequests in JSON), as of the moment the wait stopped; state at the deadline unknown means the requests it waited on are no longer counted (finished, or past the stale cutoff); page did not answer the settle check means the page was busy or navigating, not that its DOM was changing. A mid-transition capture passed off as the final one is worse than being slow, so this line is never swallowed.

Since the wait already happened, the pixels can ride along with it: --with-screenshot <path> makes snapshot — or an action with --observe — leave both the structure and an image from that same settled moment (two separately-waited captures would describe two different states, which is worse than not combining them). The tree still goes to stdout and the image to disk: a screenshot is an output here, not the way an agent reads a page.

AGENT_BROWSER_SETTLE_MS sets the ceiling globally (0 disables the wait) and AGENT_BROWSER_SETTLE_QUIET_MS sets the quiet window. Waiting for something specific is still wait's job: the settle knows the page stopped, not that what you wanted appeared.


Confirm the action landed — expect

Once an action is done, confirm the result with an assertion instead of pulling a snapshot and eyeballing it. expect is a pass/fail verb with an exit code (0 pass / 1 false / 2 can't tell), so it composes with && and chrome-use batch, and it costs ~1 line — no reading a whole snapshot.

bash — expect assertions
$ chrome-use click @e8 && chrome-use expect "#toast" visible      # did the toast pop up?
$ chrome-use expect count ".result" ">=" 1                        # did results load?
$ chrome-use expect text @e3 contains "Saved"                    # success message?
$ chrome-use expect url contains /dashboard                       # did navigation land?
$ chrome-use expect "#spinner" gone                               # finished loading?
$ chrome-use requests --clear && chrome-use click @save \
    && chrome-use expect request /api/save --status 2xx           # POST fired and 2xx?
$ chrome-use expect no-errors                                     # no console errors?

expect waits for you

expect polls until the condition holds within the timeout window, so you often don't need a separate wait. The supported conditions are:

--not inverts the check, and --no-wait checks once without polling.

ℹ️ Prerequisites for requests and console
expect request can only see requests captured after tracking started — run requests --clear first; no-errors needs console capture — run console first. See Troubleshooting for more.

Repeated unchanged attempts

observed.noProgress appears from the third identical observed click, dblclick, or press on the same target and screen, with at most 60 seconds between attempts. Each observation must be complete and quiet, with settling enabled (waitedMs > 0), changed:false, sawChange:false, no pending work, and no detected request, resource, new-frame or human-check activity. Activity or incomplete evidence breaks the streak.

The hint carries retryAction:false. It advises inspecting current state or waiting for a task-specific condition; it never changes action success or runs another action. No observed accessibility or request change does not prove a server write failed. Do not blindly replay a send, submit, purchase or delete.

Ordinary CLI/MCP connection preparation preserves this streak only on successful reuse of the same browser connection, target and session, without loading storage state. A new browser, rebind, failed preparation or storage-state load still clears the count.

Scripts retain these hints in advisories (top-level in CLI script --json; data.advisories in the daemon envelope) (at most 20), including nested scripts and runs that later fail. JSON op-list scripts also keep noProgress on the relevant steps entry; text output prints the aggregate advisories once. A failed JS script retains ok:false, return:null, error, logs, and advisories. Nested script daemon-envelope data.ok:false fails the parent script even when transport success is true; dispatch success does not establish program success.

See what an action changed — --observe

Requests are summarized to at most 20 entries of 256 UTF-8 bytes each. Data URLs include their media header and encoded payload size, not the payload. JSON includes requestsTotal, requestsOmitted, and requestsShortened. Use network requests --json for full captured details. Request summaries still appear with changed:false, which describes tree and URL changes only.

Add --observe to any action that changes the page (click / fill / type / select / check / press / eval / hover, which often opens a menu or a caption) and you skip the manual "action → wait → snapshot → diff" dance: the result carries an observed delta — interactive rows added/removed (new toasts and validation hints come through the alert channel too), URL changes, and requests fired; if nothing moved, it returns {changed:false}.

bash
$ chrome-use click @e8 --observe   # read changes and bounded request context after the action
ℹ️ On no change, read the why: line under it first
When nothing changed after an action, the daemon probes the target and reports the first decisive finding: the control is disabled, not rendered, outside the viewport, or covered by another element (named). Fix that one thing and retry the same semantic action. When the note says none of those apply, the action reached a control that genuinely changes nothing visible: do not read an empty delta as failure, and do not repeat the action.
ℹ️ When a click replaced the page, you get the new tree
For a link click or a submit that navigated, --observe no longer emits the old tree as removals plus the new tree as additions. It prints the new page's tree under observed snapshot:, with one line saying how many lines of the old page are gone. The rule: both trees are page-sized (20+ lines) and at least 80% of each changed. Refs in that tree are live; use them directly.
ℹ️ A small page comes back whole
When the post-action tree is at most 4 KB and 60 lines, observed.snapshot is that whole current tree, the same capture, refs registered, and observed.changes lists only the +/- lines, with no context. A snapshot straight after it adds nothing. Larger pages keep the unified delta. An incomplete capture still says partial or unavailable in observed.status.
ℹ️ Static text changes are in observed.text
The interactive tree leaves plain static text out, so an iframe's receipt, an action log line or a "Page 2 of 3" counter used to be invisible and the click read as no change. observed.text lists the visible text lines, frame by frame, that appeared (+) or went away (-); a child frame's lines start with [frame name]. A text change alone sets changed: true. At most 20 lines, 1 KB and 200 bytes a line; textOmitted counts the rest and textTruncated: true says so. The text is read from the rendered DOM, not innerText: <input>, <textarea>, <select> and every editable element (the editable host, all its content, a document in designMode) are skipped with their subtrees, inside open shadow roots too. When any password field in any frame (open shadow roots included) holds a value before or after the action, no text lines are returned and textStatus is redacted: a page can copy a password into any frame's text. The values never leave the page. Each frame is read in its own isolated world and checked against the frame tree; the tree read again afterwards is the inventory. One capture reads at most 32 frames across all sessions within 2.5 s. A frame not read (over budget, or one that appeared during the read) is partial; one whose read failed or whose document changed during the read is unavailable. Any such frame withholds every text line, is listed in textFrames with its reason, and makes the observation incomplete: textStatus is unavailable, status is at best partial, an unchanged tree no longer reads as changed: false, and no noProgress hint is given. A frame the final inventory confirms is gone, and a frame whose document changed (frameDocumentChanged, such as a same-URL reload), count as changes. The small-page changes list is capped at 60 lines, 4 KB and 240 bytes a line (changesOmitted, changesShortened).
The interactive tree leaves plain static text out, so an iframe's receipt, an action log line or a "Page 2 of 3" counter used to be invisible and the click read as no change. observed.text lists the visible text lines, frame by frame, that appeared (+) or went away (-); a child frame's lines start with [frame name]. A text change alone sets changed: true. At most 20 lines and 1 KB; textOmitted counts the rest. What was typed into inputs and textareas, editable-region content and any line holding a password value never appear. textStatus: "unavailable" means the text could not be read, so a text-only change is unknown, not absent.
✅ expect or --observe?
For a hard pass/fail gate (with exit codes, &&, batches) use expect; to see what actually happened and get a structured diff, use --observe.

Fill a whole form at once — form fill --map

Instead of writing N separate fill / select / check steps, pass a {label or selector: value} map: it resolves each field by <label>, aria-label, placeholder, name, then CSS, and auto-dispatches to the right control type (string → text/dropdown/radio, true/false → checkbox). It can also submit for you (--submit "<text|selector>") and returns {filled, submitted, errors} — where errors are the inline validation messages, so a rejected submit tells you why in the same call.

bash — form fill --map
$ chrome-use form fill --map \
    '{"Email":"a@b.com","Country":"US","Subscribe":true}' \
    --submit "Sign up"
ℹ️ Rich-text editors are the exception
For rich-text editors like DraftJS / Monaco / CodeMirror, fill them individually with fill — it handles them; form fill covers standard controls. More input detail in Interacting.

Optional steps — --if-present

Add --if-present (aliased --optional) to any selector-based action, and when the target doesn't exist it becomes a no-op that still succeeds (↷ skipped, exit code 0) instead of erroring — no need to read the page first to decide, and the flow can be re-run over and over.

bash
$ chrome-use click ".cookie-accept" --if-present   # dismiss a cookie banner that may not be there
$ chrome-use check "#opt-in" --if-present
⚠️ Only skips "element not present"
--if-present only lets a missing target through; a real failure (say, the element is there but won't click) still errors and is not silently swallowed.

Resources and human-check blocks in observe

--observe results carry resources: what the page fetched during the action, from resource timing (so it works with the Network domain off), scripts and fetches first, up to 10, with resourcesTotal.

When an action changed nothing but loaded a known human-check script (OpenAI Sentinel, hCaptcha, Cloudflare Turnstile, reCAPTCHA, Arkose, DataDome, HUMAN, GeeTest), observed.humanCheck names the vendor and the result carries a blocked_by_human_check warning. This establishes a script load and an unchanged page, not that personal presence is required. Do not repeat the original submit. Inspect the current challenge and use core/captcha to attempt ordinary controls within the authorized task, then verify the result. Hand off only when attempts fail or a person is required.


Next

🖱️ Interacting

click, type, fill, select drive a real browser — move first, then talk about waiting and asserting.

🛠️ Troubleshooting

Pre-capture for requests / console, timeouts, brittle waits — come here to diagnose when things break.