Skip to content

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, then CHROME_PATH, then a standard install location. Of the container images, only dev includes Chromium, on amd64 and arm64.
  • In a container, Chrome's sandbox needs a seccomp profile that allows user namespaces. Run the dev image with its Compose setup, which applies the bundled profile. Where the sandbox cannot start at all, browser.sandbox: false in the Dagu config or DAGU_BROWSER_SANDBOX=false turns 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 has sudo, so use it only for sites you trust. A DAG cannot change this setting. With the sandbox on, a browser step fails when CI is set or Dagu runs as root on Linux, because the browser would run without the sandbox there; set DAGU_BROWSER_SANDBOX=false to allow it.
  • A model configured with a DAG-level llm block or with.llm. Use a model that follows tool-call schemas reliably.

Quick Start ​

browser.extract opens a page and returns structured data:

yaml
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:

yaml
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:

OperationValueEffect
gotoURLNavigate the current tab.
actInstruction, 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.
expectConditionFail the step unless the condition holds.
wait{selector} or {duration}Wait until a CSS selector is visible, or pause, such as 2s.
screenshotNameSave 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:

FieldDescription
whenA condition checked once before the operation. The operation is skipped unless it holds.
timeoutMaximum 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:

FormExampleChecked by
Statementexpect: The cart shows two itemsThe model. Results can vary between runs.
textexpect: {text: Order confirmed}The page's visible text contains it.
selectorwhen: {selector: "#cookie-banner"}A CSS selector matches a visible element.
urlexpect: {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:

yaml
- 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:

yaml
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.

yaml
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 from env: are not treated as secrets.
  • A %name% must be a with.variables key or the as of an earlier ask; anything else fails validation.
  • Declared secrets and ask answers 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 extract schema, and for an act, the names of the available variables.

Requests never carry:

  • the values of with.variables or ask answers; 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:

text
[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.screenshotsAutomatic screenshots
on_failure (default)When the step fails.
finalWhen the step fails, and at the end of a successful step.
eachAfter every operation, plus the final ones.
neverNone. 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:

text
[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 act only. extract and statement conditions call the model on every run. With fixed conditions, a rerun calls the model only for extract.
  • 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 the expect fails, the replay is dropped.
  • Disable the cache with with.cache: false, or for one operation with act: {instruction: ..., cache: false}.
  • dagu browser session export writes 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 with DELETE /api/v1/dags/{fileName}/browser-cache, optionally with ?step=<id>. Removing all of a DAG's history with dagu rm --history also 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.

bash
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 text

When the operations do what the step should, export them:

bash
dagu browser session export ab2cd3ef4g --dag orders --step fetch --format yaml
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 button

The 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.

  • do reads one operation from stdin, written as an item of with.do; ask is not available. Variable values go under variables, 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.
  • describe outlines 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 (default 30m), even with no Dagu server running. Its history can still be exported for a day; close removes it at once.
  • A session opened with --profile holds 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 when as 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:

bash
dagu start --only checkout workflow.yaml

The 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:

yaml
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:

yaml
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 an expect.
  • The browser runtime applies browser.allowed_domains to 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.com matches only that host; *.example.com matches its subdomains but not example.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 goto or url outside 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:

FieldDescription
doOperations to run in order. Required.
urlPage opened before the first operation.
variablesValues referenced as %name% in act instructions.
cacheRecord and replay acts. Defaults to true. See Replay Cache.
browserBrowser options.
llmModel 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 ​

FieldDescription
browser.headlessRun without a window. Defaults to true.
browser.executableChrome or Chromium binary.
browser.viewport{width, height} in pixels.
browser.proxyProxy server URL. Authenticated proxies are not supported.
browser.allowed_domainsHosts the page's HTTP(S) requests may reach.
browser.screenshotson_failure, final, each, or never.
browser.profilePersistent profile name.

Troubleshooting ​

ErrorCauseFix
the model (...) answered that no element on the page matches the instructionThe 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 modelNo llm block applies to the step.Add a DAG-level llm block or with.llm.
Chrome installation not found; set CHROME_PATHNo 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 readyOn 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 awayA 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 askA 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 runThe 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 inputAnother run holds the profile while it waits for an answer.Answer or cancel that run.
ask operations are not supported on WindowsThe 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.
  • ask on Windows.

Dagu is open source under the GNU General Public License v3.0.