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:
$ 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:
- Wait for the specific element you expect to appear:
wait @reforwait --text "...". --textmatches an exact, case-sensitive substring of the page's visible text.--text "Saved"does not match a page readingDelivery saved.— read the page and match what it actually renders.- A result you can already see is the answer. A receipt or confirmation
already on screen does not need a
waitto confirm it a second time. Wait timed out after …means the condition was not observed in time. A failing probe is retried until the deadline, so the message reads the same whether the page answered every poll or none of them — it says nothing about the connection either way. Check the page and the condition first; do not reconnect on the strength of it alone.- Wait for the URL to change:
wait --url "**/new-page". - Wait for the network to go idle (the catch-all for SPA navigations):
wait --load networkidle.
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:
- the DOM has gone 100ms without a mutation,
- no finite CSS transition or animation is still running (a spinner that loops forever is ignored — it never ends),
- and no request fired by the action you just ran is still in flight.
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).
$ chrome-use snapshot -i --settle-ms 3000 # raise the ceiling for a slow page
$ chrome-use snapshot -i --no-settle # capture now, mid-flight
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.
$ 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:
- Element state:
visible|hidden|gone|present; - Count:
count <css> <op> <n>; - Text/value/attribute:
text|value|attr … equals|contains|matches; - URL:
url …; - Network request:
request <substr> [--status 2xx]; - Console:
no-errors.
--not inverts the check, and --no-wait checks once without polling.
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}.
$ chrome-use click @e8 --observe # read changes and bounded request context after the action
--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.
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.
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).
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.