Browser
Automate websites that have no API. A browser step drives a local Chrome with natural-language instructions, extracts structured data into step outputs, checks page state, and can pause for a person, for example to enter a one-time code, then continue in the same browser.
Browser steps are powered by the Stagehand Go SDK. Every model request goes through Dagu's own LLM providers, so the step uses the same llm configuration as chat.completion, and API keys never enter the browser.
Requirements
- Dagu v2.18.0 or later.
- Google Chrome or Chromium installed on the host that runs the step. Dagu uses
browser.executable, thenCHROME_PATH, then a standard install location. Of the container images, onlydevincludes Chromium, on amd64 and arm64. - In a container, Chrome's sandbox needs a seccomp profile that allows user namespaces. Run the
devimage with its Compose setup, which applies the bundled profile. Where the sandbox cannot start at all,browser.sandbox: falsein the Dagu config orDAGU_BROWSER_SANDBOX=falseturns it off for every browser on the host. Without the sandbox, a page that exploits the browser runs with the permissions of the Dagu process, which in the Dagu images hassudo, so use it only for sites you trust. A DAG cannot change this setting. With the sandbox on, a browser step fails whenCIis set or Dagu runs as root on Linux, because the browser would run without the sandbox there; setDAGU_BROWSER_SANDBOX=falseto allow it. - A model configured with a DAG-level
llmblock orwith.llm. Use a model that follows tool-call schemas reliably.
Quick Start
browser.extract opens a page and returns structured data:
secrets:
- name: ANTHROPIC_API_KEY
provider: env
key: ANTHROPIC_API_KEY
llm:
provider: anthropic
model: claude-sonnet-5
steps:
- id: hn
action: browser.extract
with:
url: https://news.ycombinator.com
instruction: The top 5 stories with their points
schema:
type: object
properties:
stories:
type: array
items:
type: object
properties:
title: { type: string }
points: { type: integer }
- id: report
depends: hn
run: echo '${steps.hn.outputs.stories}'browser.run runs several operations in one browser session:
secrets:
- name: VENDOR_USER
provider: env
key: VENDOR_USER
- name: VENDOR_PASSWORD
provider: env
key: VENDOR_PASSWORD
llm:
provider: anthropic
model: claude-sonnet-5
steps:
- id: invoice
action: browser.run
with:
url: https://portal.vendor.com/billing
browser:
# Every host the site loads from, including CDNs and sign-in pages.
allowed_domains: ["*.vendor.com"]
variables:
user: ${VENDOR_USER}
password: ${VENDOR_PASSWORD}
do:
- act: Type %user% into the email field
- act: Type %password% into the password field
- act: Click the Sign in button
- expect: {text: Invoices}
- extract:
instruction: The most recent invoice
schema:
type: object
properties:
invoice_number: { type: string }
total: { type: number }
- act: Download the most recent invoice PDF
- id: book
depends: invoice
run: ./book.sh "${steps.invoice.outputs.invoice_number}"Operations
with.do lists operations that run in order. Each item sets exactly one operation:
| Operation | Value | Effect |
|---|---|---|
goto | URL | Navigate the current tab. |
act | Instruction, or {instruction, cache} | Perform one action described in natural language: click, type, select, scroll, press a key. |
extract | {instruction, schema} | Extract data from the page. The schema must be a JSON Schema with type: object. |
expect | Condition | Fail the step unless the condition holds. |
wait | {selector} or {duration} | Wait until a CSS selector is visible, or pause, such as 2s. |
screenshot | Name | Save a PNG of the page as a run artifact. |
ask | {prompt, as, timeout} | Wait for a person's answer. See Human Input. |
Any operation can also set:
| Field | Description |
|---|---|
when | A condition checked once before the operation. The operation is skipped unless it holds. |
timeout | Maximum time for the operation, such as 30s. Defaults to 2m. |
An act performs a single action. "Sign in with %user% and %password%" types into one field and stops, so write one act per field and one for the button.
A browser that stops responding fails the step instead of hanging it: an operation is abandoned a few seconds after its timeout, and a screenshot or page check after 30 seconds.
with.url is opened before the first operation.
Conditions
expect and when take either kind of condition:
| Form | Example | Checked by |
|---|---|---|
| Statement | expect: The cart shows two items | The model. Results can vary between runs. |
text | expect: {text: Order confirmed} | The page's visible text contains it. |
selector | when: {selector: "#cookie-banner"} | A CSS selector matches a visible element. |
url | expect: {url: /billing} | The current URL contains it. |
Fixed checks (text, selector, url) make no model call and give the same answer on every run, so prefer them for monitoring. A selector check holds when any matching element is visible.
A fixed when reads the page once, right after the previous operation. When the page may still be loading, add within so the check keeps reading until the condition holds or the window ends:
- ask: {prompt: Enter the code, as: otp}
when: {text: Verification code, within: 10s}A fixed expect keeps reading until within, or the operation timeout when within is not set.
Model
Browser steps use the DAG-level llm block. with.llm replaces it entirely for one step:
steps:
- id: prices
action: browser.extract
with:
llm:
provider: openrouter
model: deepseek/deepseek-v4-flash
url: https://shop.example.com
instruction: All product names and prices
schema:
type: object
properties:
products: { type: array }A list of models under model is tried in order for each request. Every request asks the model for a tool call whose parameters are the expected JSON. A provider or model without tool calling works only if it replies with plain JSON text, so choose a model that follows tool-call schemas reliably. Small local models often pick the wrong element.
If an act fails with "the model (...) answered that no element on the page matches the instruction" while the element is plainly on the page, the model is answering "no element" for every request. The schema allows null for "no match", and some models always choose it; google/gemini-2.5-flash through OpenRouter does, while newer Gemini Flash models, gpt-4.1-mini, and claude-haiku-4.5 pick the element. Try another model. See Troubleshooting.
Secrets and Variables
Declare secrets under secrets:, put them in with.variables, and reference them as %name% in act instructions. The browser types the value; the model sees only the name.
secrets:
- name: PORTAL_PASSWORD
provider: env
key: PORTAL_PASSWORD
llm:
provider: anthropic
model: claude-sonnet-5
steps:
- id: login
action: browser.run
with:
url: https://portal.example.com/login
variables:
password: ${PORTAL_PASSWORD}
do:
- act: Type %password% into the password field
- act: Click the Sign in button- Do not write
${PORTAL_PASSWORD}inside an instruction. The step fails before starting a browser when a model-bound text contains the value of a declared secret of four or more characters. Values that only come fromenv:are not treated as secrets. - A
%name%must be awith.variableskey or theasof an earlierask; anything else fails validation. - Declared secrets and
askanswers of four or more characters are masked in text sent to the model, in the step log, and in the timeline. Plain variables are not masked.
What Is Sent to the Model
Each model request carries:
- the operation's instruction, or the statement a model-judged condition checks;
- a list of the page's elements with their roles and visible text;
- the
extractschema, and for anact, the names of the available variables.
Requests never carry:
- the values of
with.variablesoraskanswers; the model sees%name%; - what the browser typed into fields;
- screenshots.
Declared secrets and ask answers of four or more characters are replaced with ******* wherever they appear in a request, including in the page text. Plain variables are not masked, so a value the page displays, such as a signed-in user name, reaches the model unless it comes from a declared secret. Secrets can come from any secret provider. The model provider's API key only authenticates the request.
Outputs
The top-level properties each extract schema lists become step outputs, readable as ${steps.<id>.outputs.<name>} and checked when the DAG loads. Fields a schema does not list are dropped. Two extracts in one step cannot list the same property.
When the step succeeds with outputs, stdout is one JSON object of them, so output: also works. Operation progress is written to the step's stderr log:
[start] goto "https://portal.vendor.com/billing" (completed, 0 tokens, 800ms)
[1/6] act "Type %user% into the email field" → fill xpath=/html[1]/body[1]/form[1]/input[1] %user% (cache-hit, 0 tokens, 300ms)
[4/6] expect "text \"Invoices\"" → the page text contains "Invoices" (completed, 0 tokens, 0s)
[5/6] extract "The most recent invoice" → {"invoice_number":"INV-8812","total":412.5} (completed, 1204 tokens, 2.1s)
[6/6] download "invoice-8812.pdf" → browser/invoice/downloads/invoice-8812.pdf (completed, 0 tokens, 0s)Screenshots and Downloads
Browser steps store files as run artifacts under browser/<step id>/. A DAG with a browser step enables artifact storage automatically. These files are not masked and can show signed-in pages with personal data; use browser.screenshots: never or artifacts.enabled: false to keep them out of run history.
browser.screenshots | Automatic screenshots |
|---|---|
on_failure (default) | When the step fails. |
final | When the step fails, and at the end of a successful step. |
each | After every operation, plus the final ones. |
never | None. screenshot operations still save. |
Files the page downloads are saved under browser/<step id>/downloads/ with the name the site suggests. Only act and goto start downloads. Once one has run, the step waits for running downloads after every operation, and before it ends or pauses for an ask it waits a few seconds for a late download to begin. A download may run for the longest timeout of the acts and gotos run so far; give a large export's act a long timeout. A canceled download, or one still running at that timeout, fails the step. Downloaded files appear in the timeline.
With artifacts.enabled: false, no screenshots are saved, a screenshot operation fails, and the browser refuses downloads.
Dialogs
The step accepts every JavaScript dialog a page opens, so a dialog never blocks the page: alert, confirm, and beforeunload are accepted, and a prompt is answered with its default text. An act that raises "Are you sure?" therefore goes through. Each accepted dialog appears in the step log and the timeline after the operation that opened it:
[2/3] dialog "Delete the row?" → accepted confirm (completed, 0 tokens, 0s)Replay Cache
An act records the actions it performed, and the recordings are kept when the step succeeds. The next run of the same step on the same host replays them without asking the model when the operation's position, its instruction, and the page URL (without query or fragment) match. When a replay fails because the page changed, the step asks the model again, records the new actions, and marks the operation healed in the log. A recorded element that is gone, or still on the page but hidden, such as a field in a closed dialog, fails the replay.
- Recordings are kept only when the whole step succeeds. When an operation fails on the page after a replay, the step drops the recordings it replayed, so the next run asks the model again. A failure of the model, the browser, a download, or an
ask, or a canceled run, leaves them. - The cache covers
actonly.extractand statement conditions call the model on every run. With fixed conditions, a rerun calls the model only forextract. - A replay clicks the recorded element location. After a layout change it can hit a different element without failing, so follow important acts with an
expect, preferably a fixed one; when theexpectfails, the replay is dropped. - Disable the cache with
with.cache: false, or for one operation withact: {instruction: ..., cache: false}. dagu browser session exportwrites the recordings of the acts a session ran for a step before its first run; see Building a Step in a Browser Session.dagu browser cache clear <dag>removes the recordings of every step of a DAG, or of one step with--step <id>. The REST API does the same withDELETE /api/v1/dags/{fileName}/browser-cache, optionally with?step=<id>. Removing all of a DAG's history withdagu rm --historyalso clears them. Each clears only the cache on the host that handles it.
Building a Step in a Browser Session
Before writing a step for a page you have not seen, work the page in a browser session with the same operations the step will run, and look at the page after each instead of guessing its fields, buttons, or addresses. A session keeps the browser open between commands; each command runs one operation and reports what it did and what the page shows next.
dagu browser session open https://portal.example.com/login --provider openai --model gpt-5-mini
# {"id": "ab2cd3ef4g", "outline": "form\n textbox \"Username\"\n textbox \"Password\"\n button \"Sign in\"", ...}
echo '{"act": "Type %user% into the Username field", "variables": {"user": "alice"}}' | dagu browser session do ab2cd3ef4g
echo '{"act": "Type %password% into the Password field", "variables": {"password": {"env": "PORTAL_PASSWORD"}}}' | dagu browser session do ab2cd3ef4g
echo '{"act": "Click the Sign in button"}' | dagu browser session do ab2cd3ef4g
dagu browser session describe ab2cd3ef4g --find Orders --format textWhen the operations do what the step should, export them:
dagu browser session export ab2cd3ef4g --dag orders --step fetch --format yaml- id: fetch
action: browser.run
with:
url: https://portal.example.com/login
variables:
password: ${PORTAL_PASSWORD}
user: ${user}
do:
- act: Type %user% into the Username field
- act: Type %password% into the Password field
- act: Click the Sign in buttonThe step holds the operations that succeeded, in the order they ran. Export also writes their act recordings to the step's replay cache, so the step's first run on this host replays them without asking the model, and heals as usual when the page changed. Declare PORTAL_PASSWORD under the DAG's secrets and give it a parameter user.
doreads one operation from stdin, written as an item ofwith.do;askis not available. Variable values go undervariables, never on the command line:{"env": NAME}is read from the environment on every command and masked like a secret, and a plain string serves one command.describeoutlines the page without a model request: headings, fields with their labels, selects with their options, buttons, links with their addresses, and tables and lists, with long runs of alike rows collapsed. Text typed into fields is never shown.- A session closes its browser when no command arrives for
--idle-timeout(default30m), even with no Dagu server running. Its history can still be exported for a day;closeremoves it at once. - A session opened with
--profileholds the profile: a step naming it fails until the session ends. With--headed, the window is visible, and a person can sign in or solve a CAPTCHA in it between commands; the browser's DevTools address in the result lets an application show the page itself. - With a profile, the sign-in operations of the exported step run on every run; guard them with
whenas shown in Profiles.
See browser session for every option.
Trying One Step
While you reword act instructions or adjust expect conditions, run the browser step on its own instead of the whole workflow:
dagu start --only checkout workflow.yamlThe other steps are recorded as skipped, and the run keeps the workflow's name, so the replay cache and browser.profile apply as in a full run. With a profile, a site stays signed in, so the step does not need the sign-in steps before it. When the step reads an earlier step's output, take it from a finished run with --outputs-from <run-id>, or set it with --output <step>.<name>=<value>. See Running One Step.
Profiles
browser.profile keeps cookies and storage across runs, so a site stays signed in:
with:
browser:
profile: vendor
do:
- act: Type %user% into the email field
when: {selector: "form#login"}
- act: Type %password% into the password field
when: {selector: "form#login"}
- act: Click the Sign in button
when: {selector: "form#login"}The sign-in acts run only while the login form is showing. Profiles are stored on the host that runs the step. Runs that use the same profile run one at a time. A run fails immediately when another run is waiting for input with the same profile open, or when a browser session holds it.
Human Input
ask pauses the step until someone answers in the Web UI:
do:
- act: Type %user% into the email field
- act: Type %password% into the password field
- act: Click the Sign in button
- ask:
prompt: Enter the 6-digit code sent to your phone
as: otp
timeout: 15m
when: {text: Verification code, within: 10s}
- act: Type %otp% into the code field and submit
when: {text: Verification code}The step enters Waiting with the question in the step's Agent tab. The browser stays open. Answering resumes the step in the same browser at the next operation, with the answer available as %otp%. If an act needs an answer whose ask was skipped, the step fails instead of typing %otp%. While other steps of the run are still running, answering is rejected; try again after they finish. Rejecting the question fails the step. The browser stays open for timeout (default 1h). After that, the step can no longer resume: an answer is refused, or fails the step on a distributed worker. Start clean session runs the step again from the beginning.
Answers are stored in the run's history, like other human input. Use ask for short-lived codes, not long-term secrets.
ask is not supported on Windows, where the browser cannot outlive the step process; such a step fails at the start.
Safety
- Page content is untrusted and is sent to the model. A hostile page, or content other users posted on an allowed site, can try to steer an
act, for example into typing%password%into the wrong field. Use variables only on pages you trust, keep instructions specific, and follow sensitive acts with anexpect. - The browser runtime applies
browser.allowed_domainsto the page's HTTP(S) requests, including scripts, images, and API calls, so list the CDN and sign-in hosts a site loads from. WebSocket connections are not covered, and the runtime's check has a known bypass.example.commatches only that host;*.example.commatches its subdomains but notexample.com. An entry is a host name with at least two labels, without a scheme, port, or path, and*may appear only as a leading*.. - Blocked requests are logged after the operation that made them, such as
allowed_domains blocked 3 requests: cdn.example.net (3), and a failed step's error lists them. A page that breaks for no clear reason often needs a host added. - Dagu itself checks only the page URL: it rejects a
gotoorurloutside the list, and after every operation fails the step if a redirect or a click left the allowed domains. - Dialogs are accepted automatically, so an act that clicks the wrong button also confirms it. Keep instructions for destructive actions specific and check the result with an
expect. - The browser runs with the permissions of the Dagu process.
Distributed Mode
Browser steps run on the worker that picks them up, which needs Chrome. Profiles and the replay cache are stored on that worker, so pin DAGs that rely on them with a DAG-level worker_selector and worker labels. An answered ask resumes on the worker that holds the browser.
Web UI
The step's Agent tab shows each operation with its status, token use, screenshot thumbnails, downloads, and accepted dialogs, and holds pending questions.
Options
browser.run takes:
| Field | Description |
|---|---|
do | Operations to run in order. Required. |
url | Page opened before the first operation. |
variables | Values referenced as %name% in act instructions. |
cache | Record and replay acts. Defaults to true. See Replay Cache. |
browser | Browser options. |
llm | Model configuration that replaces the DAG-level llm block. |
browser.extract takes url, instruction, and schema, with optional timeout, browser, variables, and llm, and behaves as browser.run with one extract operation.
Browser Options
| Field | Description |
|---|---|
browser.headless | Run without a window. Defaults to true. |
browser.executable | Chrome or Chromium binary. |
browser.viewport | {width, height} in pixels. |
browser.proxy | Proxy server URL. Authenticated proxies are not supported. |
browser.allowed_domains | Hosts the page's HTTP(S) requests may reach. |
browser.screenshots | on_failure, final, each, or never. |
browser.profile | Persistent profile name. |
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
the model (...) answered that no element on the page matches the instruction | The element is not on the page, or the model answers "no match" for every request. | Look at the failure screenshot. If the element is there, try another model; otherwise reword the instruction or open the right page first. |
act did not complete: ... | The browser runtime could not perform the action. The message gives its reason. | Look at the failure screenshot; make the instruction more specific, or add a wait for the page to settle. |
expectation not met: ... | An expect condition did not hold. | Look at the failure screenshot; fix the act before it, or add within when the page needs time to change. |
selector "..." did not appear within ... | A wait selector did not become visible. | Check the selector, or raise the operation's timeout. |
browser actions need a model | No llm block applies to the step. | Add a DAG-level llm block or with.llm. |
Chrome installation not found; set CHROME_PATH | No Chrome or Chromium on the host that runs the step. | Install Chrome, set CHROME_PATH or browser.executable, or use the dev image. |
the browser sandbox is on, but the browser runtime turns it off because CI is set (or when running as root) | The browser would run without its sandbox. | Run Dagu as a non-root user without CI, or set DAGU_BROWSER_SANDBOX=false. |
Chrome exited before its debugging port was ready | On Linux, usually a sandbox that cannot start, as under Docker's default seccomp profile. | Run the dev image with its Compose setup, or set DAGU_BROWSER_SANDBOX=false. |
... is outside browser.allowed_domains or the page navigated away | A goto, redirect, or click left the allowed hosts. | Add the host, including sign-in and CDN hosts, or fix the instruction. |
contains the value of secret ... | A declared secret is written in an instruction. | Pass it in with.variables and reference it as %name%. |
act references %name%, which is not in with.variables or an earlier ask | A misspelled or missing variable. | Add it to with.variables or fix the name. |
the instruction uses %name%, but the ask that sets it did not run | The ask was skipped because its when did not hold. | Give the act the same when as the ask. |
download of ... did not finish within ... | The download ran longer than the longest act or goto timeout. | Raise the timeout of the act that starts it. |
the browser did not respond within ... | The page stopped responding. | Check the page; raise timeout for slow pages. |
browser profile "..." is held by DAG run ..., which is waiting for input | Another run holds the profile while it waits for an answer. | Answer or cancel that run. |
ask operations are not supported on Windows | The browser cannot outlive the step process on Windows. | Run the step on Linux or macOS. |
Not Supported Yet
- Attaching to an already running browser.
- Hosted browsers, CAPTCHA solving, and stealth fingerprints.
- Screenshot-based extraction.
askon Windows.

