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

  • 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.
  • Where Chrome's sandbox is unavailable, such as a container that runs Dagu as a non-root user, add flags for every browser on the host with browser.args in the Dagu config or DAGU_BROWSER_ARGS, for example --no-sandbox,--disable-dev-shm-usage. The dev image sets these. Without the sandbox, a page that exploits the browser runs with the permissions of the Dagu process, so keep it to containers. A DAG cannot set these flags.
  • 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

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 every act fails with No action found while the element is plainly on the page, the model is likely 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.

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

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.

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

A successful act records the actions it performed. 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.

  • 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.
  • Disable the cache with with.cache: false, or for one operation with act: {instruction: ..., cache: false}.

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.

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%. Rejecting the question fails the step. The browser stays open for timeout (default 1h); after that, the answer fails the step and 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.
  • 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 steps that rely on them with worker_selector. 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.

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.

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.