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.
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 "...". - 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.
$ 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.
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.
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)
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.
&&, 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.
$ chrome-use form fill --map \
'{"Email":"a@b.com","Country":"US","Subscribe":true}' \
--submit "Sign up"
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.
$ chrome-use click ".cookie-accept" --if-present # dismiss a cookie banner that may not be there
$ chrome-use check "#opt-in" --if-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.