Ctrl K
ComunidadGuías…

llms.txt

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Dagu

Dagu is a self-contained workflow orchestration engine for running DAGs defined in YAML. It runs as a single binary without requiring an external database or message broker. It stores state locally by default and supports local, queued, and distributed execution modes.

Use this compact reference with an AI agent when authoring, validating, or troubleshooting Dagu workflows, or when operating Dagu through its CLI. It summarizes repository-local workflow and CLI references.

---

# DAG Authoring

Load only the reference file that matches the task.

## Default Approach

- Prefer `type: graph` for new DAGs. It supports both sequential flow via `depends:` and parallel flow.
- Use an Agent DAG (`type: agent`) only when the step order cannot be written down in advance and an LLM must choose it. It requires `llm:` and a `tasks:` list stating when the run is finished. The model may select multiple distinct, independent actions in one turn; Dagu runs them concurrently up to `max_active_steps` (`0` is unlimited, `1` is serial). Same-batch actions cannot consume sibling outputs, while failures do not cancel siblings. `set_task_status` and `ask_user` must be called alone.
- Use `type: build` only for local regular-file pipelines whose unchanged transformations should be reused across runs.
- Prefer `id` on every step. Omit `name` unless the display label must differ from the step ID.
- Prefer `dagu schema ...` and `dagu validate ...` over guessing field names or shapes.
- Prefer `action: template.render` when generating text files, prompts, or artifacts instead of assembling them with shell `echo` or heredocs.
- Prefer `action: js.run` for transforming data between steps (parsing, filtering, reshaping, extracting with regular expressions, computing values) instead of `python -c`, Python or Node heredocs, or `awk`, `sed`, and `jq` pipelines in `run:`. It needs no interpreter on the host or worker, Dagu passes its script as written so `${...}` and `$` need no escaping, syntax errors fail validation before a run, and its returned value is clean stdout for `output:`. Pass data in with `with.input` or `with.input_file`. Keep `run:` for invoking CLIs and for work that needs the network, the filesystem, environment variables, or a library; `js.run` has none of these. A single jq path such as `.items[].id` is fine in `jq.filter`.
- Prefer `file.*` actions for local file operations such as stat, read, write, copy, move, delete, mkdir, and list instead of shelling out to `cp`, `mv`, `rm`, or `mkdir`.
- Prefer `xlsx.*` actions for `.xlsx` workbooks: `xlsx.read` for rows, `xlsx.validate` before acting on them, `xlsx.write` or `xlsx.append` for reports, `xlsx.update_rows` to write per-row results back, `xlsx.write_cells` to fill a template, `xlsx.sheet` to manage sheets, `xlsx.convert` to hand a sheet to a tool that reads CSV, and `xlsx.extract` to read named fields out of a form-like sheet whose layout differs by sender, instead of a Python or PowerShell script. Inspect a workbook first with `dagu xlsx inspect` or the MCP `workbook` read target.
- Prefer `git.worktree.add` and `git.worktree.remove` when steps need isolated branches inside an existing local Git repository. Add an explicit remove step when the workflow should delete the worktree.
- Prefer `stdout.artifact` / `stderr.artifact` when a command stream should become a DAG-run artifact, especially for large reports, JSON, Markdown, logs, or generated files.
- Prefer `artifact.*` actions for explicit artifact reads/writes/lists. Use `DAG_RUN_ARTIFACTS_DIR` only when a tool truly needs a filesystem path inside the step.
- Prefer string-form `output: VAR_NAME` for capturing small stdout values into flat variables.
- Prefer object-form `output:` when downstream steps need structured values via `${step_id.output.*}`.
- Prefer declared step `outputs:` with `$DAGU_OUTPUT_FILE` when a step must publish explicit values for `${steps.<step_id>.outputs.<name>}`.
- Use `action: human.task` when an operator must provide typed input before downstream steps continue. Human task form outputs use `${steps.<step_id>.outputs.<name>}` without an authored `outputs:` field.
- Prefer `stdout.outputs` or `action: outputs.write` when a DAG or remote action needs to return caller-visible values via `${step_id.outputs.*}`.
- Prefer `state.*` actions for small persistent JSON state across DAG runs, such as cursors, checkpoints, and previous-value comparisons.
- Prefer temporary files in the artifacts dir only when downstream steps need file paths; otherwise let commands write large artifact content to stdout and attach it with `stdout.artifact`.
- Prefer scoped Dagu references for named values: `${consts.NAME}`, `${params.NAME}`, and `${env.NAME}`. Avoid unscoped braced names in examples unless the example is intentionally showing shell syntax.
- Declare portable external CLI dependencies in top-level `tools` using aqua shorthand when the binary version affects reproducibility, for example `tools: ["jqlang/jq@jq-1.7.1"]`. Append `#sha256:<64 hex>` to also pin the downloaded artifact content for the run platform, for example `jqlang/jq@jq-1.7.1#sha256:<hex>`.
- For remote actions, put `tools` in the referenced action DAG file, not in `dagu-action.yaml`; caller DAG tools are not inherited across the action boundary.
- For worker-local aqua registries, set `tools.registry.type: local` and `tools.registry.path` to a path absolute for the worker's OS. The worker captures a regular file of at most 16 MiB and installs from that snapshot, hashing its contents into the toolset cache key.
- Each distinct local registry content hash retains a full private package set and aqua-proxy on every worker, with no automatic cache pruning. Use package digests to pin downloaded bytes even when a registry or asset URL changes.
- Declare step-level `dependencies` when a run needs files from the DAG working directory. Use literal working-directory-relative paths or glob patterns; do not use value references.
- Use remote action packages (`dagu-action.yaml`) when reusable logic needs helper files, its own DAG, versioning, or an input/output schema contract.

## High-Signal Rules

- `output:` has two modes:
  - string form captures trimmed stdout, including any log lines, into an env-scope variable such as `${env.VERSION}`
  - object form publishes structured step-scoped output for `${step_id.output.*}` access
- Value declarations in step `outputs:` publish explicit values through `${steps.<step_id>.outputs.<name>}`. Write those values to `$DAGU_OUTPUT_FILE`; Dagu captures them only after the command succeeds. Build path declarations publish the final materialization path after commit or reuse.
- `human.task` is a processless root-DAG step with an explicit `id`, a required `with.prompt`, and an optional flat scalar form. A root DAG containing one can run locally or on a distributed worker. Every declared form property is a step output, published when submitted or defaulted, and available as `${steps.<step_id>.outputs.<name>}`. Optional `with.push_back` (`rewind_to` plus an optional feedback `form`) lets the operator send the work back to an upstream step instead of completing it.
- `stdout.artifact` / `stderr.artifact` store command stdout/stderr directly as relative artifact paths, for example `stdout: {artifact: reports/report.md}`. Artifact outputs auto-enable artifacts unless `artifacts.enabled: false` is explicitly set, which is invalid.
- `${step_id.stdout}` is a log file path, not stdout content.
- Step `stdin:` pipes a file to the command's standard input, so `stdin: ${step_id.stdout}` replaces `cat "${step_id.stdout}" | command`. Only command and shell steps accept it.
- Use `${context.*}` for run metadata in DAG YAML, for example `${context.dag.name}`, `${context.run.id}`, or `${context.paths.artifacts_dir}`. Unavailable context values remain unresolved text instead of becoming empty strings.
- In a build step, `${inputs.<name>}` is the final input path and `${outputs.<name>}` is a fresh attempt staging path. Write file results only to the staging path; dependencies read the committed path as `${steps.<step_id>.outputs.<name>}`.
- Do not read attempt-only `${step_id.stdout}`, `${step_id.stderr}`, or `${step_id.exit_code}` from potentially reusable producers. This also applies to `${step_id.output.<name>}` and `${step_id.outputs.<name>}`, including their whole-value `${step_id.output}` and `${step_id.outputs}` forms. Path-output steps cannot use `continue_on.mark_success`.
- Build workflows are local-only. Path declarations are supported only on host command or shell steps without containers, and stream redirects cannot target declared build inputs or outputs.
- Use `${consts.NAME}`, `${params.NAME}`, and `${env.NAME}` for Dagu-side named values. Use shell `$NAME` or `printenv NAME` only when the target shell or process should read the variable at execution time.
- `consts:` must use list form with one key per item, for example `consts: [{service: api}]`. Const values are resolved while loading the DAG and can reference inherited or earlier consts.
- `env:` should use list-of-maps when values depend on earlier env vars.
- `params:` values arrive as strings. The `params:` field supports JSON schema-like types and validation, check for schema to see how to specify types and validation rules.
- Single-line `run:` values are command-form entries. Array-form `run:` entries run one by one. Multi-line `run:` values are scripts. Dagu does not split pipes, redirects, `&&`, or `;` into separate commands; those stay with the selected shell.
- Do not assume `bash` for `run:` steps. If a script depends on a specific interpreter, add a shebang such as `#!/bin/sh` or `#!/usr/bin/env bash` only after checking that shell exists on the target host or container. Otherwise keep the script portable or set `with.shell:` explicitly.
- `parallel:` currently requires `action: dag.run` or `action: dag.enqueue` to a child DAG.
- Sub-DAGs do not inherit parent env vars; pass what you need via `params:`.
- For arbitrary text inside shell steps, prefer `printenv VAR_NAME` or `action: template.render` over Dagu interpolation such as `${env.VAR_NAME}`.
- `harness.run` supports built-in provider adapters (`aider`, `amp`, `claude`, `cline`, `codex`, `copilot`, `cursor`, `deepseek`, `droid`, `gemini`, `goose`, `kilo`, `kiro`, `opencode`, `pi`, `qwen`) and custom top-level `harnesses:` entries. Built-in OpenCode uses managed sessions on long-lived Dagu hosts by default; containers and unsupported options use the CLI path. It can use top-level `container:` or step-level `container:`.
- Container runtime selection is service-level, not a DAG YAML field. Set `DAGU_CONTAINER_RUNTIME=podman` to use Podman, and set `DAGU_PODMAN_HOST` only when the Podman Docker-compatible socket is not the default.
- DAG/action outputs are collected from string-form `output: VAR_NAME`, `stdout.outputs`, and `action: outputs.write`. Object-form `output:` stays step-scoped for `${step_id.output.*}` unless the workflow explicitly republishes values through `stdout.outputs` or `outputs.write`.
- `state.get`, `state.set`, `state.delete`, `state.list`, and `state.diff` persist small JSON values across DAG runs. State scopes are `dag`, `root_dag`, `global`, and `custom`; use artifacts or external storage for large payloads.
- Git worktree actions discover the repository from the step `working_dir`. Relative worktree paths resolve from the repository root, and the actions never fetch or push.
- Remote action packages define `dagu-action.yaml` with `apiVersion: v1alpha1`, `name`, `dag`, and optional `inputs`/`outputs` JSON Schemas. `inputs` validates caller `with:` before the action DAG starts; `outputs` validates the final action output object after the action DAG returns.
- Remote action manifests do not support `tools`. Declare external CLI tools in the action DAG itself so local and distributed workers prepare the right binaries for that action run.
- `dependencies` snapshots files from the DAG working directory for both local and distributed execution. The materialized bundle is available through `DAG_RUN_WORK_DIR` and `${context.paths.work_dir}` and becomes the DAG process working directory.
- In remote action examples, prefer `dag: workflow.yaml` for the action DAG filename. The `dag` field accepts any safe relative file path, but `workflow.yaml` avoids confusing the executable DAG with the `dagu-action.yaml` manifest.
- Object-form `output:` with `decode: json` or `decode: yaml` can act as lightweight runtime validation. Malformed data or an unresolved `select:` path fails the step, so normal `retry_policy` applies.
- Use DAG-level `shell` and `shell_args` only when every inherited `run:` step should use the same shell invocation. Use step-level `with.shell` and `with.shell_args` for a single step.
- A DAG-level `watch:` block (`path`, optional `pattern` such as `*.pdf`, optional `interval`, 10s by default) starts one run for each file that arrives in a folder, with the file's path as `${WATCH_FILE}` and its name as `${WATCH_NAME}`. A file counts as arrived once its size and time hold still between two readings; files already in the folder when the watch starts are left alone, and a file runs again only when it is replaced. Prefer it to a schedule that polls a folder. A file's name is chosen by whoever put it in the folder, so in a `run:` step read `WATCH_FILE` and `WATCH_NAME` with `printenv` or through `action: template.render` rather than interpolating `${WATCH_NAME}` into the command.
- Use `dagu schema dag` to check the full list of available fields and their shapes.
- Use `dagu example` to see different DAG patterns and how to express them in YAML.

## Example of Params, template step, and artifacts

```yaml
params:
  type: object
  properties:
    name:
      type: string
      maxLength: 50
    age:
      type: integer
      minimum: 0
      maximum: 120
    favorite_color:
      type: string
  required: [name, age]

steps:
  - id: render
    action: template.render
    with:
      data:
        name: ${params.name}
        age: ${params.age}
        favorite_color: ${params.favorite_color}
      template: |
        Hello, {{ .name }}!
        You are {{ .age }} years old.
        {{- if .favorite_color }}
        Your favorite color is {{ .favorite_color }}.
        {{- end }}
    stdout:
      artifact: greeting.txt
```

## Example of Large Command Output as Artifact

```yaml
steps:
  - id: report
    run: ./generate-report --format markdown
    stdout:
      artifact: reports/report.md
```

## Example of Reproducible External CLI

```yaml
tools:
  - jqlang/jq@jq-1.7.1

steps:
  - id: inspect
    run: jq --version
```

## Example of Object-Form Output

```yaml
steps:
  - id: inspect_build
    run: echo '{"version":"v1.2.3","artifact":{"url":"https://example.test/app.tgz"}}'
    output:
      # decode + select act as a lightweight contract check:
      # malformed JSON or a missing selected field fails the step.
      version:
        from: stdout
        decode: json
        select: .version
      artifact:
        from: stdout
        decode: json
        select: .artifact

  - id: publish
    depends: [inspect_build]
    output:
      versionLabel: "ver - ${inspect_build.output.version}"
      artifactUrl: "${inspect_build.output.artifact.url}"
```

## Example of Action Outputs

```yaml
steps:
  - id: classify
    run: ./classify.sh "${params.INPUT}"
    stdout:
      outputs:
        fields:
          label:
            decode: json
            select: .label
          confidence:
            decode: json
            select: .confidence

  - id: publish
    depends: [classify]
    action: outputs.write
    with:
      values:
        label: ${classify.outputs.label}
        reviewed: false
```

## Example of Human Input

```yaml
steps:
  - id: review
    action: human.task
    with:
      prompt: Choose the deployment target
      form:
        type: object
        properties:
          environment:
            type: string
            enum: [staging, production]
          note:
            type: string
            default: ""
        required: [environment]

  - id: deploy
    depends: [review]
    run: ./deploy --environment '${steps.review.outputs.environment}' --note '${steps.review.outputs.note}'
```

Complete the task from a local CLI context with `dagu human-task complete --run-id=<run-id> --step=review --input environment=production <dag-name>`. A form is optional for acknowledgement-only tasks. Human tasks cannot be used in sub-DAGs; a distributed root run is re-queued through the scheduler after completion.

## Reference Guide

Load only the file you need:

- `references/steptypes.md` when choosing an action or checking action-specific behavior such as `human.task`, `dag.run`, `parallel`, `git.worktree.*`, `jq.filter`, `js.run`, `file.*`, `xlsx.*`, `state.*`, `template.render`, `chat.completion`, `browser.*`, or `computer.*`
- `references/dagu-action.md` when creating a reusable `dagu-action.yaml` package or checking action input/output schema behavior
- `references/cli.md` when choosing or using Dagu CLI commands, including workflow inspection, execution, and cleanup operations, and browser sessions for building a browser step on a site you have not seen
- `references/context.md` when using `${context.*}` metadata references or declared step `outputs:`
- `references/build.md` when creating or troubleshooting a `type: build` file workflow, path references, reuse decisions, or `--no-reuse`
- `references/file-dependencies.md` when a DAG needs scripts, configuration, or other files from its working directory
- `references/harnesses.md` only when the DAG invokes external CLI harnesses through `harness.run`

---

# Actions

## run: Shell Commands And Scripts

Use top-level `run:` for local shell commands and scripts.

```yaml
steps:
  - id: hello
    run: echo "hello"

  - id: multi_line
    run: |
      echo "step 1"
      echo "step 2"

  - id: ordered
    run:
      - echo "first"
      - echo "second"

  - id: custom_shell
    run: |
      set -euo pipefail
      echo "running in bash"
    with:
      shell: /bin/bash
```

Fields:

- `run` - command string or multi-line shell script
- `stdin` - optional file path piped to the command's standard input, for example `stdin: ${fetch.stdout}`
- `with.shell` - shell interpreter, for example `/bin/bash`
- `with.shell_args` - shell interpreter arguments
- `with.shell_packages` - optional packages to install before execution

Notes:

- Single-line `run:` values are command-form entries.
- Array-form `run:` entries run one by one and stop on the first failing entry.
- Multi-line `run:` values are scripts.
- Dagu sends pipes, redirects, `&&`, and `;` to the selected shell. It does not split that shell syntax into separate Dagu commands.
- DAG-level `shell` and `shell_args` provide defaults for inherited `run` steps. Use `with.shell` and `with.shell_args` when one step needs a different shell invocation.
- Dagu resolves `${...}` references before the shell runs. For large or arbitrary text, prefer `printenv VAR_NAME`, reading `${step_id.stdout}` as a file, or `action: template.render`.
- `stdin` resolves `${...}` references before the command runs; `${step_id.stdout}` gives the path to that step's captured stdout file. A leading `~` expands, relative paths resolve against the step working directory, and the host process environment is not a fallback for `$NAME`.
- A `stdin` path that resolves to nothing, or that still carries a reference, fails the step rather than silently leaving standard input empty.
- `stdin` needs an action that can consume standard input. A DAG-level `container:` makes every inherited step a container step, and `ssh` feeds the remote shell its script over standard input, so neither accepts `stdin`.
- Use scoped Dagu references for named values: `${consts.NAME}`, `${params.NAME}`, and `${env.NAME}`. Use shell `$NAME` only when the target shell should read the variable at execution time.
- When large command output should become an artifact, write it to stdout/stderr and attach the stream directly instead of redirecting inside shell:

```yaml
steps:
  - id: report
    run: ./generate-report --format markdown
    stdout:
      artifact: reports/report.md
```

- Use string-form `output: VAR_NAME` only for small stdout values. Large reports, JSON dumps, Markdown summaries, and logs belong in `stdout.artifact` / `stderr.artifact`.

## docker.run / container.run

Run commands in Docker containers.

```yaml
steps:
  - id: build
    action: docker.run
    with:
      image: golang:1.23
      pull: always
      auto_remove: true
      working_dir: /app
      volumes:
        - /local/src:/app
      command: go build ./...
```

`with` fields: `image`, `container_name`, `pull`, `auto_remove`, `working_dir`, `volumes`, `network`, `platform`, `command`.

Dagu can drive Docker or Podman through a Docker-compatible API. Runtime selection is service-level, not a DAG YAML field. Set `DAGU_CONTAINER_RUNTIME=podman` for Podman. Set `DAGU_PODMAN_HOST` only when the Podman socket is not the default.

## git.worktree.add / git.worktree.remove

Create isolated working directories for branches in an existing local Git repository. The actions discover the repository from the step `working_dir`; they do not clone, fetch, or push.

This example creates a generated branch, runs tests inside its worktree, and then removes the worktree explicitly:

```yaml
working_dir: ./repo

steps:
  - id: worktree
    action: git.worktree.add

  - id: test
    depends: worktree
    working_dir: "${steps.worktree.outputs.path}"
    run: go test ./...

  - id: remove_worktree
    depends: test
    action: git.worktree.remove
    with:
      path: "${steps.worktree.outputs.path}"
```

When `branch` is omitted, Dagu generates a stable branch name for that step and DAG run. The default path is `<repository-root>.worktrees/<branch>`.

To create an explicit branch from a local commit, branch, `origin` remote-tracking branch, or tag:

```yaml
working_dir: ./repo

steps:
  - id: worktree
    action: git.worktree.add
    with:
      branch: feature/api
      create_branch: true
      base: main
      path: ../worktrees/feature-api
```

`git.worktree.add` fields:

- `branch` - local branch to check out. Omit it to let Dagu generate one.
- `path` - worktree directory. Relative paths resolve from the repository root.
- `create_branch` - allow creation of an explicitly named branch. Defaults to `false`.
- `base` - local commit, branch, remote-tracking branch, or tag used when creating the branch. Defaults to repository `HEAD`.

The add action is idempotent. It reuses a matching registered worktree without resetting its branch or discarding local changes. Worktrees remain registered until an explicit remove action or an external Git command removes them.

Use `git.worktree.remove` for explicit removal:

```yaml
working_dir: ./repo

steps:
  - id: worktree
    action: git.worktree.add

  - id: remove_worktree
    depends: worktree
    action: git.worktree.remove
    with:
      path: "${steps.worktree.outputs.path}"
      branch: "${steps.worktree.outputs.branch}"
      delete_branch: true
```

`git.worktree.remove` fields:

- `branch` and `path` - provide either selector or both. When both resolve to a worktree, they must identify the same registration.
- `force` - remove a dirty worktree. Defaults to `false`.
- `delete_branch` - delete the local branch after removing the worktree. Requires `branch`.
- `force_delete_branch` - allow deletion of an unmerged branch. Requires `delete_branch: true`.

`force` and `force_delete_branch` protect different data: `force` permits removal of local worktree changes, while `force_delete_branch` permits deletion of unmerged commits.

Both actions publish fixed outputs. Do not add `output`, `outputs`, or `stdout.outputs` to these steps. Read results through `${steps.<id>.outputs.<field>}`.

- Add outputs: `path`, `branch`, `commit`, `worktree_created`, `branch_created`.
- Remove outputs: `path`, `branch`, `worktree_removed`, `branch_deleted`.

Dagu refuses to remove the primary working tree. Worktree mutations against the same repository are serialized, but Git changes made outside Dagu are not covered by that lock.

## dag.run

Execute another DAG as a child DAG.

```yaml
steps:
  - id: child
    action: dag.run
    with:
      dag: child-workflow
      params:
        input: /data/file.csv
```

Sub-DAGs do not inherit parent env vars. Pass values explicitly via `with.params`.

## Approval Gates

An `approval:` gate pauses a step after its execution until an operator approves, rejects, or pushes it back. In a root run, approving a gate resumes ready dependent steps while independent manual steps remain waiting, provided no failed, aborted, rejected, or retrying step would be re-run. Steps with build inputs do not by themselves trigger a resume while other manual steps wait.

Root approvals, push-backs, agent interaction responses, and agent-session restarts resume directly unless the run belongs to an enabled global queue. Global queues require a running scheduler. For queued local managed OpenCode sessions, use `dagu start-all` so the server and scheduler share the session host. Standalone API servers resume unqueued local runs using their own session host; distributed runs retain their owning worker. The API's `resumed: true` indicates an accepted resume, not that execution has started.

Approvals remain saved if resume admission fails. The approval endpoint returns HTTP 503 with `approvalStored: true` and `resumePending: true` in the error details. Use **Retry resume** in the run view or `POST /api/v1/dag-runs/{name}/{dagRunId}/resume` to retry without approving again. This recovery endpoint accepts no approval inputs and is limited to root runs with saved approvals and ready work. Repeated requests while queued or running do not create another execution.

Child approvals and agent interaction responses resume only after every waiting step in the child is resolved. Independent branch resumption is supported only for root runs.

## human.task

Pause a root DAG run until an operator completes a processless step. A human task does not execute a command and is distinct from an approval gate: completion always succeeds the step, and there is no reject operation. With `with.push_back`, the operator can instead send the work back to an upstream step with feedback.

```yaml
params:
  RELEASE: v1.2.3

steps:
  - id: review
    action: human.task
    with:
      prompt: Select a deployment window for ${params.RELEASE}
      form:
        type: object
        title: Deployment review
        properties:
          window:
            type: string
            enum: [morning, evening]
          ticket:
            type: string
            pattern: '^CHG-[0-9]+$'
          notify:
            type: boolean
            default: true
        required: [window, ticket]

  - id: deploy
    depends: [review]
    run: ./deploy --window '${steps.review.outputs.window}' --ticket '${steps.review.outputs.ticket}'
```

`with.prompt` is required and supports normal Dagu value references. `with.form` is optional; omit it for an acknowledgement-only task that accepts no input. `with.artifacts` is optional and lists artifact-relative paths to show the operator as review context:

```yaml
steps:
  - id: test
    run: ./run-tests.sh
    stdout:
      artifact: reports/test-report.html

  - id: review
    depends: [test]
    action: human.task
    with:
      prompt: The test report is attached. Approve the release?
      artifacts:
        - reports/test-report.html
```

Artifact paths reject absolute paths, `~`, and `..` segments. They are value-resolved when the task opens, like the prompt, and the resolved path is re-checked under the same rules, so a parameter cannot inject `..`. Referencing an artifact does not enable artifact storage; a step must still write it. A missing artifact never blocks completion.

The form is a flat object JSON Schema:

- `type` must be `object`.
- Property names must start with a letter and contain only letters, digits, or `_`.
- Property types are `string`, `integer`, `number`, and `boolean`.
- Supported property constraints include `default`, `enum`, `oneOf` choices, `minimum`, `maximum`, `minLength`, `maxLength`, and `pattern`.
- `additionalProperties` defaults to `false`. Set it explicitly to `true` only when undeclared completion fields are intended.

Dagu derives outputs from form properties; do not add an `outputs:` field to the human task. Every declared property is a step output, published when submitted or defaulted, and available as `${steps.<step_id>.outputs.<name>}`.

Human tasks require an explicit `id` and cannot be used in sub-DAGs, lifecycle handlers, or `foreach.steps`. A root DAG containing human tasks can run locally or on a distributed worker selected by its DAG-level `worker_selector`. Executor, retry, repeat, timeout, container, step-level worker selector, output capture, and approval fields are not supported on the same step.

Complete a waiting task from a local CLI context:

```sh
dagu human-task complete --run-id=<run-id> --step=review --input window=morning --input ticket=CHG-123 <dag-name>
```

Use `--inputs-json` instead of repeated `--input` flags when input types must be preserved exactly.

Completing a human task resumes the run when it unblocks a step or no other step is waiting. A distributed run is re-queued, so its scheduler must be running.

Add `with.push_back` when the reviewer may request changes instead of completing:

```yaml
steps:
  - id: implement
    action: harness.run
    with:
      provider: codex
      prompt: Implement the requested change

  - id: review
    depends: [implement]
    action: human.task
    with:
      prompt: Review the implementation
      push_back:
        rewind_to: implement
        form:
          type: object
          required: [feedback]
          properties:
            feedback:
              type: string
```

- `rewind_to` is required and must name a step the task depends on directly or transitively, by `id` or `name`. It cannot be the task itself.
- `form` is optional and follows the `with.form` rules, but `additionalProperties` must stay `false`. Feedback properties never become step outputs.
- A push-back resets `rewind_to` and every step that depends on it directly or transitively, including the task and, in `type: build` DAGs, steps that consume its output paths. The run is queued again when no other step is waiting, or when the rewind target can run, declares no build inputs, and no step is failed, aborted, rejected, or retrying. Otherwise it keeps waiting until those steps are resolved.
- Rewound steps receive `DAG_PUSHBACK`, `DAG_PUSHBACK_ITERATION`, `${context.pushback.iteration}`, and one environment variable per feedback property, even approval steps whose `approval.input` does not list it. `harness.run` and `chat.completion` also get the feedback in their prompt.
- Feedback is limited to 16 KiB as JSON because it is passed as environment variables. `DAG_PUSHBACK` keeps the most recent history entries that fit in 30 KiB.
- The task then opens again with its prompt re-resolved. It can be completed or pushed back again.
- `push_back` is invalid in `type: agent` DAGs.

Push back from a local CLI context, passing the iteration that was reviewed:

```sh
dagu human-task push-back --run-id=<run-id> --step=review --input feedback="Add tests" --expected-iteration 0 <dag-name>
```

## Declared Value Outputs

Declare value-form `outputs:` when a step should publish named values for later steps as `${steps.<step_id>.outputs.<name>}`. Build file outputs use `path` instead; see `references/build.md`.

```yaml
steps:
  - id: build
    run: |
      printf 'image_tag=v1.2.3\n' >> "$DAGU_OUTPUT_FILE"
      {
        printf 'metadata<<JSON\n'
        printf '{"commit":"abc123"}\n'
        printf 'JSON\n'
      } >> "$DAGU_OUTPUT_FILE"
    outputs:
      - name: image_tag
      - name: metadata
        type: json

  - id: deploy
    depends: [build]
    run: ./deploy.sh '${steps.build.outputs.image_tag}'
```

Rules:

- The step must have an `id`.
- `outputs:` must be a non-empty sequence.
- Each output requires `name`.
- `type` can be `string` or `json`. The default is `string`.
- The step writes output records to `$DAGU_OUTPUT_FILE`.
- Output records use `name=value` or heredoc form: `name<<DELIMITER`, value lines, matching `DELIMITER`.
- The output file must be valid UTF-8.
- Every declared output must be written exactly once.
- Undeclared, duplicate, missing, or invalid JSON outputs fail the step.
- Dagu captures declared outputs only after the command succeeds.

## outputs.write

Publish DAG or remote action outputs assembled from literals, parameters, or prior step values.

```yaml
steps:
  - id: send
    run: ./scripts/notify.sh "${params.text}"
    output:
      response:
        from: stdout
        decode: json

  - id: publish
    depends: [send]
    action: outputs.write
    with:
      values:
        messageId: ${send.output.response.id}
        status: sent
```

Published values are available as `${publish.outputs.messageId}` in the same DAG. When the step runs inside a remote action DAG, the parent action caller reads the final action outputs as `${action_step.outputs.messageId}`.

Notes:

- `values` must be a non-empty object.
- Keep values small and JSON-compatible; use artifacts for files, reports, logs, screenshots, or large JSON payloads.
- If the remote action manifest declares an `outputs` schema, Dagu validates the final collected action output object after the action DAG returns. `outputs.write` itself does not validate the manifest.

## state.get / state.set / state.delete / state.list / state.diff

Read and write persistent JSON state that survives across DAG runs. Use state actions for cursors, checkpoints, and comparing the current result with the previous run. Use artifacts or external storage for large files.

```yaml
steps:
  - id: load_cursor
    action: state.get
    with:
      key: cursors/feed
      default: null

  - id: save_cursor
    action: state.set
    with:
      key: cursors/feed
      value: ${fetch.output.nextCursor}

  - id: detect_change
    action: state.diff
    with:
      key: snapshots/feed
      value: ${fetch.output.items}
      update: true
```

Scope fields:

- `scope` - state scope: `dag` (default), `root_dag`, `global`, or `custom`
- `namespace` - namespace override. For `custom` scope, this is required.

Default namespaces:

- `dag` - current DAG name
- `root_dag` - root DAG name for nested DAG runs
- `global` - `_`
- `custom` - no default; set `namespace`

Operation fields:

- `state.get`: `key`, optional `default`, `required`
- `state.set`: `key`, `value`, optional `expected_version`, `create_only`
- `state.delete`: `key`
- `state.list`: optional `prefix`, `limit`, `include_values`
- `state.diff`: `key`, `value`, optional `expected_version`, `update`

All state actions write JSON to stdout. Common output fields include `operation`, `scope`, `namespace`, and key or prefix information.

- `state.get` returns `found`, and when found, `value`, `version`, and `hash`. If not found and `default` is set, `value` contains the default.
- `state.set` returns `version`, `hash`, and `created`.
- `state.delete` returns `deleted`.
- `state.list` returns `entries`; entry values are omitted unless `include_values` is true.
- `state.diff` returns `changed`, `foundPrevious`, `current`, optional `previous`, and `version` / `hash` when the stored value was written or already exists.

Values must be JSON-serializable. Dagu normalizes state values before storing them and enforces the state payload size limit after normalization.

## parallel

`parallel:` currently works only with `action: dag.run` or `action: dag.enqueue`.

```yaml
steps:
  - id: fan_out
    action: dag.run
    with:
      dag: process-item
    parallel:
      items:
        - item1
        - item2
        - item3
      max_concurrent: 5

  - id: fan_out_dynamic
    action: dag.run
    with:
      dag: process-item
    parallel: ${params.ITEMS}
```

Each child invocation receives the current item as `ITEM`.

A string item source is parsed as a JSON array when it is one, and otherwise split on a detected separator: line breaks when the string has several lines, else the most frequent of `,` `;` `|` and tab, else whitespace. Detection cannot tell one item containing `|` or `,` from several items on one line, so set `separator` (`newline`, `comma`, `semicolon`, `pipe`, `tab`, or `space`) when items may contain delimiter characters. An explicit separator splits on that separator alone and skips JSON detection; it requires a string `items` value.

```yaml
  - id: restore
    action: dag.run
    with:
      dag: restore-pvc
      params:
        PVC_DETAILS: ${ITEM}
    parallel:
      items: ${get_pvcs.outputs.ALL_PVCS}
      separator: newline
```

`max_concurrent` defaults to 10. Set `defaults.max_concurrent` in a DAG or base config to change it for every `parallel` and `foreach` step that omits `max_concurrent`:

```yaml
defaults:
  max_concurrent: 50
```

A completed `parallel` step publishes a JSON array of each successful child run's outputs on its step outputs channel, so a later step in the same DAG can read `${fan_out.outputs}` or `${fan_out.outputs[0].NAME}`. Entries follow `parallel.items` order with failed children removed, so index `N` is the Nth successful child rather than the Nth item, and a successful child that published nothing contributes an empty object. An entry merges the child's output variables with its declared outputs, so values published through `outputs:`, `stdout.outputs` or `outputs.write` are included. The array does not merge into the run's collected outputs or into a parent run's `${step.outputs}`.

## ssh.run / sftp.upload / sftp.download

Remote command execution and file transfer over SSH.

```yaml
steps:
  - id: remote
    action: ssh.run
    with:
      user: deploy
      host: server.example.com
      key: ~/.ssh/id_rsa
      timeout: 60s
      command: systemctl restart app

  - id: upload
    action: sftp.upload
    with:
      user: deploy
      host: server.example.com
      key: ~/.ssh/id_rsa
      source: /local/file.tar.gz
      destination: /remote/file.tar.gz
```

Shared SSH fields: `user`, `host`, `port`, `key`, `password`, `timeout`, `strict_host_key`, `known_host_file`, `bastion`. `ssh.run` also accepts `shell` and `shell_args`.

## http.request

HTTP requests.

```yaml
steps:
  - id: api_call
    action: http.request
    with:
      method: POST
      url: https://api.example.com/data
      headers:
        Authorization: "Bearer ${env.TOKEN}"
        Content-Type: application/json
      body: '{"key": "value"}'
      json: true
      timeout: 30
```

Upload files as multipart form data by mapping server field names to local paths. Relative paths resolve from the step working directory:

```yaml
steps:
  - id: upload
    action: http.request
    with:
      method: POST
      url: https://api.example.com/uploads
      form:
        description: nightly-report
      files:
        document: ./report.pdf
```

Do not set the multipart `Content-Type` header manually. `body` cannot be combined with `form` or `files`.

`with` fields: `method`, `url`, `timeout`, `headers`, `query`, `body`, `form`, `files`, `silent`, `debug`, `format`, `output`, `json`, `skip_tls_verify`.

## jq.filter

JSON processing.

```yaml
steps:
  - id: transform
    action: jq.filter
    with:
      filter: ".items[] | {name: .name, count: .quantity}"
      data:
        items:
          - name: a
            quantity: 1

  - id: transform_file
    action: jq.filter
    with:
      filter: .name
      input: ${fetch_json.stdout}
```

Use `with.data` for inline JSON or `with.input` for a JSON file path. Do not set both.

`with.args` binds named variables available as `$<key>` in the filter. Values keep their YAML type; string values, including nested strings, resolve Dagu references once through executor-config resolution. Reference-derived numeric strings need `tonumber` for numeric comparisons.

Supplying `args`, including `{}`, makes the entire filter literal jq source. Pass Dagu references through `args`; steps without `args` retain their existing filter interpolation. Keys `name` and `$name` both bind `$name` and cannot be supplied together.

```yaml
params:
  - MIN_PRICE: '4'
steps:
  - id: filter
    action: jq.filter
    with:
      filter: '.items[] | select(.price > ($min_price | tonumber)) | .name'
      data:
        items:
          - {name: apple, price: 5}
          - {name: pear, price: 3}
      raw: true
      args:
        min_price: ${params.MIN_PRICE}
```

Output: `apple`

## js.run

Run a JavaScript function body in an embedded sandbox. No Node.js or other interpreter is needed on the host.

Use it for data transformation between steps instead of inline Python, Node, or `awk`/`sed`/`jq` pipelines. Use `run:` when the work needs a CLI, the network, the filesystem, or a library.

```yaml
steps:
  - id: fetch
    action: http.request
    with:
      method: GET
      url: https://example.com
    output: HTML

  - id: links
    depends: [fetch]
    action: js.run
    with:
      input: ${HTML}
      script: |
        const urls = new Set();
        for (const m of input.matchAll(/href="([^"]+)"/g)) {
          urls.add(new URL(m[1], "https://example.com").href);
        }
        return [...urls];
    output: LINKS
```

`with.script` is a function body; `return` a value to publish it. The text is used as written: Dagu does not resolve `${...}` inside the script, so JavaScript template literals keep working. Pass workflow values through `with.input`.

`with.input` is any YAML value bound to `input`; objects and lists arrive as native objects and arrays. `with.input_file` binds a file's contents instead. A string input that is a JSON object or array is parsed by default, so a captured output from an earlier step arrives as an object; set `format: text` to keep the string, or `format: json` to fail on invalid JSON:

```yaml
  - id: summarize
    depends: [links]
    action: js.run
    with:
      input: ${LINKS}
      script: |
        return {count: input.length, first: input[0]};
```

Output rules: `undefined` writes nothing, a string is written as-is, any other value is written as JSON. Capture it with `output:`.

The sandbox exposes the ECMAScript builtins, `console.*` (written to step stderr), `URL`, and `URLSearchParams`. The language level is ES2022 (classes, destructuring, generators, `async`/`await`, optional chaining, `BigInt`, regex named groups and lookbehind) plus `toSorted` and the ES2025 `Set` methods; absent are ES modules, `Object.groupBy`, `WeakRef`, and host APIs such as `Intl`, `structuredClone`, `TextEncoder`, and `atob`. `await` works for promises that resolve synchronously; there is no `require`, `fetch`, filesystem, `process`, or timers. `with.timeout` takes seconds or a duration and defaults to the step timeout, or `60s` without one. `dagu validate` reports syntax errors with the script line. A thrown error fails the step with the exception and script line in the error, and the stack trace on stderr. A script that returns `undefined` leaves stdout empty and notes that on stderr. Use `node-script@v1` when a script needs real Node.js.

`with` fields: `script`, `input`, `input_file`, `format`, `timeout`.

## template.render

Render text using Go `text/template`.

```yaml
steps:
  - id: render
    action: template.render
    with:
      data:
        name: Alice
      template: |
        Hello, {{ .name }}!
    output: RESULT
```

Reference `with.data` keys with a leading dot: `{{ .name }}`. A bare `{{ name }}` is a function call in Go templates; `dagu validate` rejects it in inline `with.template` text, and a `with.template_ref` template fails the same way when the step runs. Set exactly one of `with.template` or `with.template_ref`. `with.template` is literal template text. `with.template_ref` must be one complete canonical Dagu reference such as `${env.TEMPLATE}` or `${steps.fetch.outputs.template}`; it resolves once to a non-empty string, and references inside the resulting template remain literal. The selected text is rendered as a template, not executed as shell. `with.output` writes rendered content to a file; top-level `output:` captures or publishes step output.

## file.stat / file.read / file.write / file.copy / file.move / file.delete / file.mkdir / file.list

Local filesystem operations.

```yaml
steps:
  - id: ensure_output_dir
    action: file.mkdir
    with:
      path: ${context.paths.artifacts_dir}/reports

  - id: write_report
    action: file.write
    with:
      path: ${context.paths.artifacts_dir}/reports/summary.txt
      content: "status=ok\n"
      overwrite: true

  - id: copy_report
    action: file.copy
    with:
      source: ${context.paths.artifacts_dir}/reports/summary.txt
      destination: ${context.paths.artifacts_dir}/reports/latest.txt
      overwrite: true

  - id: list_reports
    action: file.list
    with:
      path: ${context.paths.artifacts_dir}/reports
      pattern: "*.txt"
```

Use `path` for `file.stat`, `file.read`, `file.write`, `file.delete`, `file.mkdir`, and `file.list`. Use `source` and `destination` for `file.copy` and `file.move`. `file.write` also requires `content`.

`with` fields: `path`, `source`, `destination`, `content`, `mode`, `format`, `pattern`, `overwrite`, `create_dirs`, `atomic`, `recursive`, `missing_ok`, `dry_run`, `include_dirs`, `follow_symlinks`, `max_bytes`.

Safety defaults:

- `overwrite` defaults to false for write, copy, and move.
- `atomic` defaults to true for file writes.
- `recursive` is required for directory copy and directory delete.
- `file.delete` refuses to delete the filesystem root.
- Copy and move reject the same source and destination, and directory copy rejects destinations inside the source tree.

## postgres.query / sqlite.query / postgres.import / sqlite.import

SQL database queries and imports.

```yaml
steps:
  - id: query
    action: postgres.query
    with:
      dsn: "postgres://user:pass@localhost:5432/db"
      query: "SELECT * FROM users WHERE active = true"
      output_format: json
      timeout: 120
      transaction: true
```

`with` fields include `dsn`, `query`, `params`, `timeout`, `transaction`, `isolation_level`, `output_format`, `headers`, `null_string`, `max_rows`, `streaming`, `output_file`, and `import`.

## clickhouse.query

Run SQL on ClickHouse over its HTTP interface. No ClickHouse client library is needed.

```yaml
steps:
  - id: rollup
    action: clickhouse.query
    with:
      url: "https://clickhouse.example.com:8443"
      user: etl
      password: ${CLICKHOUSE_PASSWORD}
      database: analytics
      query: |
        INSERT INTO daily SELECT * FROM raw WHERE day = {day:Date};
        SELECT count() AS rows FROM daily WHERE day = {day:Date}
      params:
        day: "2024-01-02"
      settings:
        max_execution_time: 300
      output_format: jsonl
```

`with` fields: `url` (required, `http://` or `https://`, no credentials in it), `user`, `password` (sent only as request headers), `database`, `query` or `file` (a `.sql` file, relative to the step working directory), `params`, `settings`, `output_format`, `headers`, `timeout`, `tls_skip_verify`, `tls_ca` (PEM bundle of extra root CAs).

- The password travels in a request header, so use an `https://` URL unless the server is on a trusted network.
- `query` may hold several statements. They run in order in one session (so `SET`, `USE`, and temporary tables carry over) and the step stops at the first error. `database` is the default database; a script selects it once with `USE`, so a `USE` in the script takes effect for later statements. A `SET x` in a script is overridden when `x` is also in `settings`. A multi-replica service behind a load balancer needs session stickiness for scripts.
- `params` are server-side parameters: write `{name:Type}` in the SQL, for example `WHERE day = {day:Date}`. Values are never interpolated into the SQL.
- `settings` are ClickHouse settings applied to every statement; the executor reserves the names it manages itself (query and session ids, formats, compression, and exception reporting). Nothing is read-only by default; set `readonly: 1` for a read-only step. A user whose profile sets `readonly = 1` cannot change settings: on ClickHouse 25.11 and later such a user can run `jsonl` and `csv` steps but not `json`, and on earlier versions every statement fails because the executor turns off `http_write_exception_in_output_format`. A profile with `readonly = 2` works on all versions.
- `output_format` applies to statements without their own `FORMAT` clause: `jsonl` (default, one JSON object per row), `json` (a JSON array of objects), or `csv` (`headers: true` adds a header row). The response is streamed to stdout as it arrives.
- On timeout or cancellation the running statement is stopped on the server with `KILL QUERY`.
- Data import is not part of this action. Inline data after `INSERT ... FORMAT` is not supported; use `INSERT ... VALUES` or `INSERT ... SELECT`.

## redis.<operation>

Redis operations use the operation in the action name.

```yaml
steps:
  - id: cache_set
    action: redis.set
    with:
      url: "redis://localhost:6379"
      key: mykey
      value: myvalue
      ttl: 3600
```

Connection fields: `url`, `host`, `port`, `password`, `username`, `db`, TLS fields, `mode`, `timeout`, `max_retries`.

## s3.upload / s3.download / s3.list / s3.delete

S3 object operations.

```yaml
steps:
  - id: upload
    action: s3.upload
    with:
      region: us-east-1
      bucket: my-bucket
      key: data/output.csv
      source: /local/output.csv
```

Connection fields: `region`, `endpoint`, `access_key_id`, `secret_access_key`, `session_token`, `profile`, `force_path_style`.

Custom endpoints may include a path prefix. Dagu preserves the prefix when constructing and signing S3 requests. For example, Supabase can be configured at the DAG level:

```yaml
s3:
  endpoint: "${env.SUPABASE_API_URL}/storage/v1/s3"
  region: "${env.SUPABASE_S3_REGION}"
  access_key_id: "${env.SUPABASE_S3_ACCESS_KEY_ID}"
  secret_access_key: "${env.SUPABASE_S3_SECRET_ACCESS_KEY}"
  force_path_style: true

steps:
  - id: upload
    action: s3.upload
    with:
      bucket: reports
      key: daily/output.csv
      source: /local/output.csv
```

## mail.send

Send email.

```yaml
steps:
  - id: notify
    action: mail.send
    with:
      from: noreply@example.com
      to: team@example.com
      subject: "Build Complete"
      message: "The build finished successfully."
```

SMTP server settings come from global configuration. With `mailbox`, the message
goes through that entry of `mail_accounts` instead, and `from` defaults to its
address.

With `mailbox`, `in_reply_to` answers one found email (its `id`, or the email
from `mail.search`): `to` defaults to its Reply-To or sender, `subject` to
`Re: <subject>`, and the reply is threaded under it.

```yaml
- action: mail.send
  with:
    mailbox: support@example.com
    in_reply_to: ${foreach.email.id}
    message: Thanks, we are on it.
```

## mail.search / mail.organize

Read and organize a mailbox over IMAP or, for a Google account signed in with
OAuth, the Gmail API. Accounts live in the DAG-level (or base
config) `mail_accounts` map, keyed by email address. `provider: google` or
`provider: microsoft` fills in the servers; any other server sets `imap.host`.
Authenticate with `password` or with `oauth` (`google_refresh` or
`microsoft_refresh` and a refresh token). `microsoft_refresh` accepts optional
`scopes` to request instead of `https://outlook.office.com/.default`. A
`provider: google` account with `oauth` uses the Gmail API instead of IMAP and
SMTP: it takes no `imap`, `smtp`, or `username`, folders are Gmail labels, and
its grant needs `https://www.googleapis.com/auth/gmail.modify` or
`https://mail.google.com/`, and the Google Cloud project of `client_id` must have
the Gmail API enabled.

```yaml
mail_accounts:
  support@example.com:
    provider: microsoft
    oauth:
      provider: microsoft_refresh
      client_id: ${MS_CLIENT_ID}
      refresh_token: ${SUPPORT_TOKEN}

steps:
  - id: find
    action: mail.search
    with:
      mailbox: support@example.com
      unread: true
  - id: each
    depends: find
    foreach:
      items: ${steps.find.outputs.messages}
      as: email
      steps:
        - id: ticket
          run: ./create-ticket.sh "${foreach.email.subject}"
        - id: done
          depends: ticket
          action: mail.organize
          with:
            mailbox: support@example.com
            emails: ${foreach.email.id}
            mark: read
```

`mail.search` `with` fields: `mailbox`, `folder` (default `INBOX`), `unread`,
`from`, `subject`, `within` (such as `24h` or `7d`), `has_attachments`,
`save_attachments`, `limit` (1-50, default 20). It publishes `messages` (oldest
first, each with `id`, `message_id`, `folder`, `from_name`, `from_address`, `to`, `cc`,
`subject`, `date`, `unread`, `flagged`, `text`, `attachments`), `count`, and
`truncated`. Searching never marks email read.

`mail.organize` `with` fields: `mailbox`, `emails` (an ID, an email from
`mail.search`, an `{id, move_to}` object, or a list), `mark` (`read`, `unread`,
`flagged`, `unflagged`), `move` (`folder`, `archive`, `trash`), `folder`,
`dry_run`. It publishes `changed` and `missing`. To process each email once,
mark it read inside the loop right after its work, so a failed email stays
unread for the next run.

## xlsx.read / xlsx.info / xlsx.list_sheets / xlsx.write / xlsx.append / xlsx.update_rows / xlsx.validate / xlsx.write_cells / xlsx.sheet / xlsx.convert / xlsx.extract

Read and write `.xlsx` workbooks without a spreadsheet application, on any
platform. The pattern a per-row job needs is: read the rows still to do, act on
each, and write each row's result back.

```yaml
steps:
  - id: read
    action: xlsx.read
    with:
      path: ~/Inbox/orders.xlsx
      sheet: Orders
      where: {Status: ""}
  - id: each
    depends: read
    foreach:
      items: ${steps.read.outputs.rows}
      key: ${foreach.item.order_id}
      steps:
        - id: submit
          action: http.request
          with:
            method: POST
            url: https://erp.example.com/orders
            body: ${foreach.item}
            format: json
      collect:
        order_id: ${foreach.item.order_id}
        status: ${steps.submit.outputs.status_code}
    output: RESULTS
  - id: mark
    depends: each
    action: xlsx.update_rows
    with:
      path: ~/Inbox/orders.xlsx
      sheet: Orders
      key: order_id
      rows: ${RESULTS}
      set: {Status: status}
      wait_for_unlock: 5m
```

The loop's `collect` builds one object per row with the key and the result
fields, and `rows` accepts the foreach aggregate directly: a string-form
`output: RESULTS` is the variable `${RESULTS}`, and its `outputs` list is
what gets written back. `http.request` fails on a response outside 2xx, so a
rejected order fails its item body; the loop is then partially succeeded,
the aggregate lists the failed items and holds only the successful bodies in
`outputs`, and the write-back marks the rows that succeeded. The rejected
rows keep an empty Status, so the next run submits only them; the run ends
partially succeeded.

`xlsx.read` `with` fields: `path`, `password`, `sheet` (first sheet by default,
matched case-insensitively), `range` (`A2:F`, `Sheet1!A2:F`, a named range, or a
table; without it the data block is detected, skipping leading empty rows and
columns), `header` (`true`, `false` for `A`, `B`, `C`, a row number, or `[3, 4]`
to join two header rows), `columns` (names or `{name: alias}`), `merged` (`fill`
or `first`), `stop_at_blank`, `keep_empty_rows`, `trim`, `formulas` (`cached`,
`text`, `calculate`), `types` (`{amount: number, due: date}`; a pinned column
also reads Japanese forms: `¥123,000`, `12万3,500円`, `▲1,000`, `(1,000)`,
`10%`, `税込1,000`, `金壱拾弐万参千円也`, `2026年10月3日`, `令和8年10月3日`,
`R8.10.3`, `2026年10月` as its first day, `10月3日` in the current year,
`令和8年10月3日(金) 午後2時30分`, and under `boolean` `はい`/`いいえ`, `有`/`無`,
`済`/`未`, `○`/`×`), `on_type_error`
(`fail`, `warn` to read the cell as null, or `keep` to keep it as read), `where` (`{Status: ""}`, `{Status: {ne: Done}}`,
`{Status: {in: [A, B]}}`), `max_rows` (default 5000), `skip_hidden` (`true`
leaves out rows hidden by a filter or by hand; they are read by default). It
publishes `rows` (objects keyed by header, each with `_row`), `count`, `headers`, `sheet`, `range`,
`warnings`, and `truncated`; do not declare outputs on the step. Numbers stay
numbers, dates become ISO 8601 text, text keeps leading zeros, and errors name
the cell: `orders.xlsx Orders!D17: expected number, found "N/A"`.

`xlsx.info` publishes `path`, `sheets` (each with `name`, `hidden`, `used_range`,
`range`, `header_row`, `headers`, `types`, `row_count`, `hidden_rows`, `columns`,
`profile_truncated`, `tables`), `named_ranges`, `date_system`, and `warnings`;
`xlsx.list_sheets` publishes `sheets` and `count`. `hidden` marks a hidden or
very hidden sheet, and `hidden_rows` counts the data rows a filter or a person
hid; pass `skip_hidden: true` to read only the rows shown. `types` and `columns` cover every data row of
the detected table, up to 5000 (`profile_truncated` when there are more). Each
column has `name`, `type`, `filled`, `blank`, `distinct` (counted up to 1000),
`values` (the distinct values when 12 or fewer repeat), `min` and `max` (number
and date columns), `odd` (cells the type cannot read even when pinned, such as
`未定` in a number column; `12` or `令和8年10月3日` are not odd), and
`odd_cells` (the first three as `{cell, text}`). Before writing a workflow for a
workbook, read it with `dagu xlsx inspect <path>` or the MCP `workbook` target
to learn its sheets, headers, and types. A protected workbook takes `--password`
or `DAGU_XLSX_PASSWORD` on the command, and `password` on the workbook target.
Use the profile to choose `where`
values, `types`, and `on_type_error`.

`xlsx.write` `with` fields: `path`, `sheet` (created when missing), `rows` (a
list of objects or arrays, usually `${steps.<id>.outputs.rows}`) or `input` (a
`.json`, `.jsonl`, or `.csv` file), `columns` (order and selection; rows from a
step output arrive with keys in alphabetical order, so pass
`columns: ${steps.<id>.outputs.headers}` to keep the sheet's order), `header`,
`mode` (`replace` the sheet, the default, or `append`), `style` (`table`, the
default: bold frozen header, fitted widths, number formats by column; or
`none`), `types`, `formats` (a number format per column, such as
`{Total: "#,##0"}`, for every cell written to it; also on `xlsx.append`),
`atomic` (default `true`), `dry_run`, `wait_for_unlock`, `artifact`. A
replaced sheet is cleared in place, so its position, the defined
names scoped to it, and formulas elsewhere that refer to it stay valid; its
merged regions and tables are removed. Other sheets, widths, styles, and
defined names are untouched. ISO date strings become real dates. `xlsx.append` adds rows below the last used row,
copying the style of the cell above, and writes a header only when the sheet is
empty. Appended fields are placed under the header cell of the same exact name:
a loose match fails with `did you mean`, a name the header lacks adds a column
at the right, and `header: false` writes rows by position onto a sheet with no
header row. An append fails when a cell it would write, or the header cell of
a new column, lies in a merged cell. `output` on `xlsx.append` writes the
result to another workbook and leaves `path` untouched.

`xlsx.update_rows` `with` fields: `path`, `sheet`, `key` (the column that
identifies a row, or `_row`), `rows`, `set` (`{Status: status}` takes a field of
each row, `{Reviewed: {value: yes}}` writes one literal, and a literal that is
text in the canonical form of a number such as `"100"` is written as a number
unless `type: string` pins it; omitted, every field
other than the key and `_row` goes to the column of the same name), `missing`
(`fail`, `skip`, or `append`), `output` (a different workbook to write the
result to, leaving `path` untouched), `atomic`, `dry_run`, `wait_for_unlock`,
`artifact`. Columns the sheet lacks are added at the right. Two checks run
before any cell is written and cannot be turned off: the key column and every
existing `set` column must still be in the header row by name, and a row
addressed by `_row` must still hold its key there. Either failure fails the
step with the file untouched. A merged cell, such as one Status over the
lines of an order, is written once at its top-left cell: rows may give it
the same value, different values fail the step, and a merged cell reaching
outside its column's data rows is refused. A row appended with `missing:
append`, or a new column, that would land in a merged cell fails the step.

Every writer publishes `path`, `sheet`, `changes` (`{sheet, range, rows_updated,
rows_appended, columns_added, cells_changed}`), `dry_run`, and `warnings`.
By default, saves go through a temporary file renamed over the workbook;
`atomic: false` saves in place. A workbook that Excel holds open on Windows
fails with `is open in another program; close it and retry`; a `~$` lock file
nobody holds is only a warning; `wait_for_unlock: 5m` retries with backoff
instead of failing. Writes to one workbook from the same Dagu process, such as
concurrent foreach items, run one at a time; writes from another process or a
distributed worker are not coordinated, so give each workbook one writer there.
`artifact: true` keeps a copy of the saved workbook with
the run's artifacts; a copy that fails after the save is a warning, not a
failed step.

`xlsx.validate` checks a sheet before a workflow acts on it. `with` fields:
`path`, `password`, `sheet`, `range`, `header`, `columns`, `merged`,
`skip_hidden`, `trim`, `formulas`, `max_problems` (default 1000: `count` reports every problem found
while `problems` keeps that many), and the rules `required` (columns the
header row must have), `not_blank` (columns no non-empty row may leave empty;
rows whose cells are all empty are skipped), `unique` (columns whose values may not repeat), `types`
(`{Amount: number}`), and `allowed` (`{Status: [Open, Done]}`); at least one
rule is needed. It publishes `ok`, `problems` (each with `code`:
`missing_column`, `blank`, `type`, `duplicate`, or `not_allowed`; `sheet`,
`cell` and `row` (absent for `missing_column`), `column`, `message`), `count`
(the number of problems), `rows` (the number of rows checked, not the rows
themselves), `headers`, `sheet`, `range`, `warnings`, and `truncated`, and
lists each problem on stderr. By
default the step succeeds with the problems published (`on_problem: warn`);
`on_problem: fail` fails it after listing them, and a failed step publishes
nothing. To stop and ask someone, follow it with a `human.task` step whose
precondition is `${steps.<id>.outputs.count}` with `expected: "num:>0"` and
`continue_on: {skipped: true}`, so a clean workbook skips the task and the
run goes on.

`xlsx.write_cells` fills a template: `cells` maps addresses (`B2`,
`Sheet1!B2`, `'My Sheet'!B2`, or a defined name for one cell; an address
inside a merged cell, or a name over its whole area, writes the merged cell,
and two addresses in one merged cell fail the step) to a value, to
`{value: v, type: t}` to pin the type, to `{formula: "=SUM(B2:B9)"}`, or to
`null` to empty the cell. `merge` lists ranges to merge first (`[A1:D1]`), so
a title can span the template; a range over a filled cell or another merged
region fails the step, and `changes.merged` counts the ranges merged. An ISO
date string becomes a date, and text in the
canonical form of a number (`100`, `-12.5`, not `007` or `1,234`) becomes a
number, so `${foreach.item.amount}`, which arrives as text, lands as a number;
`type: string` keeps such text. Every cell keeps its style, and a date written into
a plain cell gains a date format. `output` writes the result to a new
workbook, which must be a different file, and leaves `path` untouched, so one
template serves many fills; the workbook at `path` must exist. `changes.cells_changed` counts cells that
really changed. `dry_run`, `atomic`, `wait_for_unlock`, and `artifact` apply.

`xlsx.sheet` takes `operation` (`add`, `copy`, `rename`, `delete`), `sheet`
(the new sheet for `add`, the source for `copy`, the sheet to rename or
delete otherwise), `to` (the new name for `copy` and `rename`), `if_exists`
(`fail`, `skip`, or `replace` when the sheet to create exists), `missing`
(`fail` or `skip` when the source is absent), and `position` (1-based; by
default an added sheet goes last and a copy right after its source). It
publishes the writer outputs plus `sheets`, the names afterwards. Rerun
safety comes from `if_exists: skip`. A rename does not rewrite formulas on
other sheets, a delete leaves references to the sheet dangling, and a copy
does not carry tables, images, or charts.

`xlsx.convert` exports a sheet: `path`, `output` (the file; `.csv`, `.json`,
`.jsonl`, or `format`), the read options `sheet`, `range`, `header`,
`columns`, `types`, `trim`, `skip_hidden`, and for csv `encoding` (`utf-8`, `utf-8-bom`,
`shift_jis`; `cp932` is accepted) and `delimiter`. Every row is written, with
no cap; `_row` is not. It publishes `path`, `format`, `count`, `sheet`,
`range`, `warnings`, and with `artifact: true` the file's copy. The reverse
direction is `xlsx.write` with `input`, whose csv also takes `encoding` and
`delimiter`.

`xlsx.extract` reads fields out of a sheet laid out as a form rather than a
table, such as a supplier's quote whose layout differs by sender. `path`,
`instruction` (what to find), and `schema` (`type: object`, one property per
field, each optionally `description`-ed in the sheet's own language and typed
`string`, `number`, `integer`, `boolean`, or `string` with `format: date` or
`date-time`), plus a model through the DAG-level `llm` block or `with.llm`.
Optional `sheet`, `range`, `send_values` (default true; false shows non-text
cells as their kind only), `cache` (default true), `trim`, `formulas`. A
model is shown the sheet's non-empty cells as `B3 [text,bold]: 見積番号` and
answers only the address of each field's cell, or null; the engine reads the
typed value from that cell, so no value is invented. It publishes each
property, `cells` (property to the `Sheet1!B7` it came from), `sheet`,
`warnings`, and `source` (`model` or `cache`); do not declare outputs. The
model also names the cells that are labels, and the addresses are cached by
the sheet's shape with a digest of each label, so a layout seen before makes
no model request as long as the instruction, the schema, and the labels are
unchanged; a form of the same template with a box left blank is read from
the cache too, while a label renamed in place asks again. `dagu xlsx cache
clear <dag>` drops the cache. A pinned type reads Japanese text as
`xlsx.read` does: amounts such as `¥123,000`, `12万3,500円`, `▲1,000`, or
`10%`, dates such as `2026年10月3日`, `令和8年10月3日 午後2時30分`, or `R8.10.3`,
and `済`/`未` or `○`/`×` under `boolean`.

```yaml
llm:
  provider: anthropic
  model: claude-sonnet-5
  api_key_name: ANTHROPIC_API_KEY
steps:
  - id: fields
    action: xlsx.extract
    with:
      path: inbox/quote.xlsx
      instruction: A supplier's quote. Find the quote number, the delivery date, and the total amount.
      schema:
        type: object
        properties:
          quote_no: {type: string, description: 見積番号}
          delivery: {type: string, format: date, description: 納期}
          total: {type: number, description: 合計金額}
```

`dagu dry` warns, without failing, when the workbook, sheet, or a column
named by a reading, validating, converting, updating, cell-writing, or sheet
step does not exist, and when the `input` file of `xlsx.write` or
`xlsx.append` is missing (those two may create their workbook, so it is not
checked); fields still holding a step-output reference are skipped.


## archive.create / archive.extract / archive.list

Archive operations.

```yaml
steps:
  - id: compress
    action: archive.create
    with:
      source: /data/output
      destination: /data/output.tar.gz
      format: tar.gz
      exclude:
        - "*.tmp"
```

`with` fields: `source`, `destination`, `format`, `compression_level`, `password`, `overwrite`, `strip_components`, `include`, `exclude`.

## chat.completion

Send a prompt to a model and print the reply. `with.prompt` (or `with.messages`) is required; every other `with` key (`provider`, `model`, `system`, `tools`, `max_tool_iterations`, `stream`, `thinking`, `web_search`, `files`) configures the model and replaces the DAG-level `llm` block entirely. `with.model` can be a list of `{provider, name}` entries tried in order.

With `provider: openrouter`, `provider_order` lists the upstream providers OpenRouter tries first, such as `[azure/eu]`; a `with.model` entry can set its own. OpenRouter can pass over an endpoint, such as one with a regional surcharge, unless the request names it, so an account limited to zero-data-retention endpoints may need it to reach some models. Other providers refuse `provider_order`. Every `llm` block takes it, including the DAG-level one and `with.llm` of `browser.run` and `computer.run`.

`with.files` gives the model files to read with the prompt: a path or glob pattern, a list of them, or a list an earlier step published, such as the emails `mail.search` found with `save_attachments: true`. PDF, Word (`.docx`), Excel (`.xlsx`) and text files go as text ahead of the prompt, each in a `<file name="…">` block; images go as images, scaled down to fit a model; and a PDF page without text, such as a scan, goes as an image too. Up to 50 MB a file, 1,000,000 characters of text and 50 images a step. Read a document into typed values with `output_schema`:

```yaml
steps:
  - id: find
    action: mail.search
    with:
      mailbox: billing@example.com
      unread: true
      has_attachments: true
      save_attachments: true

  - id: read
    action: chat.completion
    depends: find
    with:
      provider: anthropic
      model: claude-sonnet-4-6
      prompt: Read the invoice and give its number, date, and total.
      files: ${steps.find.outputs.messages}
    output_schema:
      type: object
      properties:
        number: {type: string}
        date: {type: string}
        total: {type: number}
      required: [number]
```

Add a step-level `output_schema` when later steps need typed values instead of free text:

```yaml
steps:
  - id: classify
    action: chat.completion
    with:
      provider: openai
      model: gpt-5
      prompt: "Classify this note and extract the amount: ${foreach.item.note}"
    output_schema:
      type: object
      properties:
        category: {type: string, enum: [refund, complaint, question]}
        amount: {type: number}
      required: [category]

  - id: record
    depends: classify
    run: ./record.sh '${steps.classify.outputs.category}' '${steps.classify.outputs.amount}'
```

Structured output behavior:

- The model answers through a forced `respond` tool whose parameters are the schema. A plain-text JSON reply is also accepted.
- Unlisted properties are dropped, the rest is validated, and stdout is one JSON object. Each listed property becomes `${steps.<id>.outputs.<name>}`; references to other names are reported as unknown even for an open schema.
- The schema needs `type: object`, at least one property, and every `required` name listed under `properties`. It cannot be combined with `web_search` or a tool named `respond`.
- An invalid answer gets one correction, then the next `with.model` entry is tried. When none answers, the step fails; the error omits the answer and the step's stderr shows each rejected answer and why.
- Validation checks shape, not truth. For extraction, leave a field out of `required` when the text may not contain it; a required field makes the model invent a value.
- With `tools`, the tool DAGs are offered next to `respond`. Reaching `max_tool_iterations` without an answer fails that model like an unusable answer, so the next `with.model` entry starts over, running the tools again. The answer is never streamed.

## harness.run

Invoke external coding-agent CLIs through built-in provider adapters or custom harness definitions.

```yaml
harnesses:
  gemini-custom:
    binary: gemini
    prompt_mode: flag
    prompt_flag: --prompt

harness:
  provider: gemini-custom
  model: gemini-2.5-pro
  fallback:
    - provider: claude
      model: sonnet

steps:
  - id: generate_tests
    action: harness.run
    with:
      prompt: "Write unit tests for the auth module"
      yolo: true
    output: RESULT
```

`with.prompt` is required and is passed to the selected provider according to its built-in adapter or custom harness definition. `with.provider` can be a built-in provider adapter (`aider`, `amp`, `claude`, `cline`, `codex`, `copilot`, `cursor`, `deepseek`, `droid`, `gemini`, `goose`, `kilo`, `kiro`, `opencode`, `pi`, `qwen`) or a top-level `harnesses:` entry. For host subprocess runs, each built-in adapter either pipes `with.stdin` to stdin or folds it into the prompt; see `references/harnesses.md` for the provider table.

Harness behavior:

- Built-in provider adapters and custom providers pass non-reserved `with` keys as CLI flags. Built-in adapters normalize `snake_case` keys to kebab-case flags.
- A non-null custom definition shadows a built-in provider with the same name. Deleting the custom definition exposes the built-in again.
- `fallback` is an ordered list of provider configs. Nested fallback is not supported.
- Provider value references must resolve to a concrete provider string before execution. Unresolved `${...}` provider values fail at runtime.
- A harness step is named with `action: harness.run`. A top-level `harness:` config supplies defaults to those steps and does not set the type of any other step, so a step written with `run:`, `exec:`, or `script:` under one stays a local command.

Container support:

- Use root-level `container:` to run compatible harness steps inside the shared DAG-level container.
- Use step-level `container:` when only that step needs a container, or when it needs a different container from the root-level container.
- Step-level `container:` takes precedence for that step.
- The selected provider binary must exist inside the container that runs the step.
- `with.stdin` and custom `prompt_mode: stdin` are rejected for containerized harness steps.
- Do not set `container.name` for step-level image-mode harness steps. Use `container.exec` when the step must run inside an existing container.
- Docker or Podman is selected by the Dagu service process, not by a DAG YAML field.

## browser.extract / browser.run

Automate a website in a local Chrome with natural-language operations. The model comes from the DAG-level `llm` block, or `with.llm`, which replaces it entirely; use a model that follows tool-call schemas reliably. Chrome or Chromium must be installed on the host that runs the step (`browser.executable` or `CHROME_PATH` selects it). Of the container images, only `ghcr.io/dagucloud/dagu:dev` includes Chromium, on amd64 and arm64; run it with `deploy/docker/browser/compose.yaml` (`docker compose up -d`), which applies the bundled seccomp profile so the browser sandbox can start, and a 1 GB `/dev/shm`. The profile applies to every process in the container and lets DAG steps call `clone`, `setns`, and `unshare` without argument filters, so use it only when the container's DAG steps are trusted.

```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.example.com/billing
      browser:
        profile: vendor
        # Every host the site loads from, including CDNs and sign-in pages.
        allowed_domains: ["*.example.com"]
      variables:
        user: ${VENDOR_USER}
        password: ${VENDOR_PASSWORD}
      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"}
        - expect: {text: Invoices}
        - extract:
            instruction: The latest invoice
            schema:
              type: object
              properties:
                invoice_number: {type: string}
        - act: Download the latest invoice PDF

  - id: book
    depends: invoice
    run: ./book.sh "${steps.invoice.outputs.invoice_number}"
```

Browser behavior:

- Before writing a browser step for a page you have not seen, work the page in a browser session (`dagu browser session`, see `references/cli.md`) with the same operations, looking at its outline after each, instead of guessing at its fields, buttons, or addresses. Export what worked: the step comes out with its acts recorded, so its first run replays them. With `browser.profile`, sign-in operations run only while the site asks for them, so guard them with `when: {selector: ...}` on the sign-in form.
- Each `do` item sets exactly one of `goto`, `act`, `upload`, `extract`, `expect`, `wait` (`selector` or `duration`), `screenshot`, or `ask`, plus optional `when` (skip unless it holds) and `timeout`.
- An `act` performs one action: "Sign in with %user% and %password%" types into one field and stops. Write one act per field and one for the button. A dropdown that is not a native `<select>` takes two acts: one opens it, the next picks the option.
- `upload: {files, into}` gives local files to a file input; an `act` cannot. `files` is a path or glob pattern relative to the step's working directory, a list of them, or an earlier step's output holding a JSON list of paths, of objects with a `path`, or of `mail.search` emails, whose attachments saved with `save_attachments: true` are uploaded. Each file is at most 50 MB. `into` names the file input, or the button or drop area that opens the file picker, in plain words; the model finds it once and later runs replay it like an act. Leave `into` out when the page has one file input; no model is asked then. The element gets the files directly when it is or holds one file input; otherwise Dagu clicks it and hands the files to the file picker it opens, which never shows; when none opens, the file input nearest it gets them. A file input that takes one file fails when given several. The model never sees the files. Follow an upload with an `expect` for what the site shows once it has the files, such as `{text: "Uploaded", within: 1m}`.
- `expect` and `when` take a statement the model judges, or a fixed check `{text}`, `{selector}`, or `{url}` that reads the page without a model call. Text and selector checks, and `wait: {selector}`, cover the frames the page shows from its own origin, and a selector matches inside open shadow roots too; `wait` also takes a selector that hops into frames with `>>`, or an XPath, as written. Prefer fixed checks for monitoring; they give the same result on every run. A fixed `when` reads the page once; add `within: 10s` when the page may still be loading.
- Declare secrets under `secrets:`, pass them in `variables`, and reference them as `%name%`. The browser gets the value, the model only the name. An instruction containing a declared secret value (4+ characters) fails the step; do not write `${SECRET}` inside an instruction. A `%name%` that is not a variable or an earlier `ask.as` fails validation.
- Model requests carry the instruction, the page's elements and visible text, and the extract schema; never variable values, typed field text, or screenshots. Declared secrets shown on the page are masked; plain variables are not.
- "the model (...) answered that no element on the page matches the instruction" means the model chose no element. If the failure screenshot shows the element, the model answers that way for every request (for example `google/gemini-2.5-flash` via OpenRouter); switch models. Otherwise fix the instruction.
- The top-level properties of each `extract` schema become `${steps.<id>.outputs.<name>}`. The same property in two extracts is a validation error.
- An answer that breaks the `extract` schema gets one correction, then the next `with.model` entry is tried. When a page may have nothing to find, use a list the model can leave empty, or allow null with `type: [string, "null"]`; a required string has no way to say "nothing".
- A field that should hold the address a link points to needs `format: uri`, as in `link: {type: string, format: uri}`, and a place in `required`. Without the format the model returns the link's element id, such as `0-11` or `[0-11]`, and the step still succeeds; without `required` some models leave the field out. `dagu validate` warns about a string field named like an address (`url`, `link`, `href`, `detail_url`, ...) that has no `format: uri`.
- The `act` and `upload` operations of a step that succeeded are replayed on later runs on the same host without a model call; `extract` and model-judged conditions still call the model. A replay acts only on an element with the role and name it recorded, in the same row text when the page repeats it (such as a Delete button per row), and on a select offering the recorded option, or else asks the model again. A recording whose actions never use a variable the instruction names, as when the page already showed the value, replays only where that variable has the value it had then; another value asks the model, and fails under `never`. Unnamed elements are told apart by role only, so follow important acts with an `expect`: when a later operation fails on the page, the step's replays are dropped and the next run asks the model again.
- `with.ai` chooses how much the model decides for `act` and for `upload` with `into`, and `ai` on an `act` or an `upload` replaces it for that operation. `on_miss` (default): replay the recording, and let the model act and repair it when a replay fails. `every_run`: the model acts every run; a successful one still records. `never`: replay only; with no recording on the host, or a replay that fails, the step fails and says why, and the recording is kept for a run under `on_miss` to repair. A step whose operations never call the model needs no `llm`. An `extract` and a statement `expect` or `when` need the model, so a step-level `never` refuses them; fixed checks and an `upload` without `into` are allowed. `cache: false` means `every_run`; set `ai` or `cache`, not both. To run a step with no model, record it under `on_miss` first (or export a browser session), then set `ai: never`.
- The browser runtime applies `allowed_domains` to the page's HTTP(S) requests, so list CDN and sign-in hosts too; WebSockets are not covered. `example.com` matches only that host; `*.example.com` matches its subdomains. Dagu also fails the step when the page URL leaves the list. Blocked requests are counted per host in the timeline and appended to a failed step's error, so a missing CDN or sign-in host shows up there.
- For a site that requires a TLS client certificate, set `browser.client_certificates`: entries with `hosts` (a host, `*.example.com`, or `*`) and `pfx_file` with `passphrase: ${CERT_PASSPHRASE}` from a declared secret, or `cert_file` and `key_file` in PEM. The host setting `browser.client_certificates` in the Dagu config applies to every browser on that host, after the step's. Dagu presents the certificate through a local proxy, so the OS never asks for its password. While certificates are presented, other HTTPS requests go through the proxy too, presenting none, so a site that asks for one fails at once with "<host> asks for a TLS client certificate, and none is configured for it" instead of hanging on a system prompt; the host setting `browser.guard_client_certificates` turns that off. Requests follow the OS proxy settings. A rejected certificate fails with the site's reason, and a timed-out operation names the connections still waiting.
- `ask: {prompt, as}` puts the step in Waiting until someone answers in the Web UI; the answer becomes `%<as>%`. Not supported on Windows. Answers are stored in run history, so use it for short-lived codes.
- Downloads started by an act or goto are saved under `browser/<step id>/downloads/` in the run artifacts and awaited, up to the longest act or goto timeout, before the step ends. Screenshots are saved on failure by default (`screenshots: final` also keeps one of a successful end). Artifacts are not masked.
- JavaScript dialogs are accepted automatically (a `prompt` gets its default text) and listed in the timeline, so an act that raises "Are you sure?" goes through.
- Where the browser sandbox cannot start, the host setting `browser.sandbox: false` or `DAGU_BROWSER_SANDBOX=false` turns it off for every browser step; a compromised page then runs with the Dagu process's permissions, so prefer the seccomp profile. DAGs cannot change it. 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.
- The host setting `browser.max_open` (or `DAGU_BROWSER_MAX_OPEN`) bounds the browsers open at once on the host, across steps, DAG runs, and browser sessions; a browser past it waits for one to close within the step's timeout. Each browser holds a Chromium process, so set it where memory is tight; `0`, the default, sets no limit.
- Profiles and the replay cache live on the host that runs the step. Pin such steps with `worker_selector` in distributed mode.
- A browser without a window (`headless: true`, the default) sends the user agent the same Chrome sends with one and reports a desktop-sized screen, so showing the window does not change how a site sees the step. A page the site refused, with a bot check such as Cloudflare's or a 403 or 429, is logged as `page refused` and named at the end of the error of a step that fails on it; a bot check that passes on its own is waited out for up to 10 seconds.
- After a site redesign, clear recorded acts with `dagu browser cache clear <dag> [--step <id>]` on that host instead of editing the instruction or setting `cache: false`.

## computer.extract / computer.run

Automate desktop applications, such as an ERP client or a legacy Windows program, on the desktop of a macOS or Windows worker. The model looks at screenshots and clicks and types; it comes from the DAG-level `llm` block, or `with.llm`, which replaces it entirely. `anthropic`, `openai`, and `gemini` models use their native computer-use tools when they support them (Claude Opus 4.8 or Sonnet 5 and later, Gemini 3.5 and later); other providers, older models, or `mode: generic` use plain function tools with any tool-calling vision model. In generic mode a turn on a window whose elements are read in full goes with the element list alone, the model typing into fields and pressing controls by id, and the model asks for a screenshot when the list is not enough.

The Dagu process that runs the step must run in a logged-in user session: on Windows not as a service, with the screen unlocked, and with a Remote Desktop or child session kept connected so its display draws; on macOS with Screen Recording and Accessibility granted to the app that starts Dagu. Run `dagu computer check` on the host to verify. Route computer DAGs to such workers with a DAG-level `worker_selector`, or route one run with `workerSelector` on the start and enqueue API requests (`--worker-label` on `dagu start` and `dagu enqueue`); other systems fail the step.

```yaml
secrets:
  - name: ERP_PASSWORD
    provider: env
    key: ERP_PASSWORD

params:
  INVOICE_ID: INV-0001

llm:
  provider: anthropic
  model: claude-opus-5

worker_selector:
  desktop: finance

steps:
  - id: post
    action: computer.run
    with:
      variables:
        password: ${ERP_PASSWORD}
      do:
        - launch: C:\Program Files\ERP\client.exe
        - act: Log in as clerk with password %password%
        - act: Open the invoice entry form and post invoice ${params.INVOICE_ID}
        - expect: {statement: A document number is shown, within: 30s}
        - extract:
            instruction: The document number in the status bar
            schema:
              type: object
              properties:
                document_number: {type: string}

  - id: record
    depends: post
    run: echo "${steps.post.outputs.document_number}"
```

Computer behavior:

- Each `do` item sets exactly one of `launch` (a command, or `{command, args}`; on macOS use `{command: open, args: [-a, TextEdit]}`), `act`, `extract`, `expect`, `wait` (a duration), `screenshot`, `outline`, or `ask`, plus optional `when` and `timeout` (default 5m).
- A `launch` waits, up to 5 seconds or its `timeout` if shorter, until what it opened is in front (a window of the app `open -a` (by name or path) or a Windows `.exe` names, or any new window), so the next operation maximizes and reads that window rather than the one in front before. The app it names, or the one a launch naming none brought forward, becomes the one the step works in, which comes back after a person brings another app forward. Where the host cannot say which window was in front, or nothing comes forward in time, the step goes on with whatever is in front; follow such a launch with an exact `expect: {window: ...}` when the next operation must work that app.
- An `act` is a whole task: the model acts until it reports the task done, up to `max_actions` actions (default 50). It fails when the model reports it cannot finish.
- A combo box or pop-up is set to an option by name through the control itself, by the model (generic mode's `choose_option` tool) and by a pinned `choose` step alike: nothing when it shows the option already, else opening it and pressing the option, typing the option into it once it holds the keyboard focus (never Enter), or stepping down its list, each checked by what the control then shows. A control or option `with.refuse` names is not chosen. A choice is recorded with its option as a placeholder when it is a variable's value, so a loop's replay chooses each row's own.
- `expect` and `when` take a statement the model judges against a screenshot, or an exact check that reads the window's elements without a model: `{text}` (the name or value of a visible element contains it), `{element: {role, name | id | near, in, nth}}` (the selector matches exactly one element; several is a miss), or `{window}` (the title of the window read contains it; `*` wildcards). An exact check reads a window of the app its selector names or the step works in, where that window is, and the window in front only while the step works in no app yet. `{statement, within: 30s}` rechecks a false statement until it holds; an exact `expect` keeps looking until `within` or the operation timeout, and an exact `when` keeps looking for two seconds unless `within` is set. Prefer exact checks for steps that repeat; they give the same result on every run, need no model, and work under `ai: never`. `%name%` placeholders in an exact check are replaced from variables and ask answers. Exact checks need macOS, through the Accessibility permission a step already has, or 64-bit Windows.
- Declare secrets under `secrets:`, pass them in `variables`, and reference them as `%name%`. The model sees only the placeholder, which is replaced when typed. `with.visible` lists variables whose values are not secret and the model may see, such as an item a row chooses from a list: the model is told what each stands for, an element it chose by that value is recorded by the placeholder, so the next row chooses its own, and a visible value that holds a declared secret's value, however short, fails the step. Typed values can appear in later screenshots sent to the model and saved as artifacts. An instruction containing a declared secret value (4+ characters) fails the step.
- The top-level properties of each `extract` schema become `${steps.<id>.outputs.<name>}`.
- `outline: <name>` (a name starting with a letter) reads the window of the app the step works in, or the window in front while it works in none, with no model and takes no screenshot itself (the step's `screenshots` policy still captures its end or failure; `never` turns that off), and publishes its elements as text under `${steps.<id>.outputs.<name>}`: a header line with the window's role, title, and app, then one line per element, indented under the element that contains it, with its role, accessible name, and any `label`, `value`, and `id` that differ from the name, and `disabled` for a grayed-out element. Elements that show nothing and that a person does not act on, such as an unnamed group, are left out, while an unnamed field or button stays for a `near` selector to name; a window with more than 400 elements lists the first 400 and says so. Use it to learn an application's buttons, fields, and labels before writing acts, exact checks, and pinned selectors. Declared secrets in the text are masked. Once the step works in an app (one an act or a passing exact check found), an outline reads only that app's windows, where they are, and it fails rather than read another app's window, or one of an app the system does not name, without a failure screenshot of it. It needs macOS or 64-bit Windows, and fails on a window the step never operates.
- `with.ai` chooses how much the model decides, and `ai` on an `act` or an `extract` replaces it for that operation. `on_miss` (default): the acts of a step that succeeded are replayed on later runs on the same host without a model while every screen still matches what the model saw; when a screen differs, the model continues from there, told what the replay already carried out, and repairs the recording. `every_run`: the model decides every run; a successful act still records. `never`: replay only; when the screen differs or there is no recording on the host, the step fails and says why, and the recording is kept for a run under `on_miss` to repair. A step whose operations never call the model needs no `llm`. A statement `expect` or `when` needs the model, so a step-level `never` refuses it; exact checks are allowed. An `extract` binds each field it reads, a scalar to its element and a table to its grid, so under `never` it reads the bound elements with no model and fails only when a field's element or a column header is missing; an `extract` under `never` whose schema lists a plain object or a free array, which need the model, fails validation. `cache: false` means `every_run`; set `ai` or `cache`, not both. When a later operation fails, the step's replays are dropped and the next run asks the model again. Clear recordings with `dagu computer cache clear <dag> [--step <id>]`.
- A recording keeps the element each action landed on beside its pixels, so a replay finds the button or field where it is now: a moved window, another display, or a screen showing other data still replays, and the model is asked only when an element is missing. An element named by a variable's value is recorded as `%name%`, so one recording serves every row of a loop. A recording that never types, chooses, or finds an element by a variable its instruction names, as one made on a row whose field already showed the value, replays only where that variable has the value it had then; a row with another value is worked by the model, which records afresh, and under `never` the act fails. So name in an instruction only the variables its actions use. `with.find_within` (default `10s`; `act.find_within` for one act; `0` looks once) is how long a replayed action waits for its element before it counts as a miss. Names and labels are compared after folding full-width characters and whitespace, and without a label's trailing colon. `with.input: direct` (or `act.input`) has a replayed element act itself through its automation pattern instead of the pointer and keys: a click invokes a button, toggles a checkbox, selects a list item, or opens a combo box, and a typing sets the field's value at once when the recording showed the typed text became the value; an element without the pattern replays on the screen, and an application that reacts to each key or to the pointer alone sees nothing, so `screen` stays the default. A click the model carried out through the element itself, as it does on a button, replays the same way under either input; a click the model aims at a combo box lands on it as a person's click does, so one that takes typing gets the caret.
- A turn the model ran on the screen, rather than through an element, is recorded on the application window its first click landed on, or the window with the keyboard focus for a turn that only types. A replay compares only that window and clicks relative to its corner, so a window that moved, or a change elsewhere on the screen such as another window, a notification, or the clock, still replays; a window of another size is a miss. Where the system names no window, the replay compares the whole display, which then needs the size it was recorded on.
- `with.maximize` (default `true`; `act.maximize` or `extract.maximize` for one operation) maximizes the window in front before an operation that holds the desktop looks at the screen and whenever a new window comes to the front during an act, so the application is operated at one size every run, the model reads larger text, and recordings replay. On macOS the window fills the screen between the menu bar and the Dock, not full screen, and a window on another display moves to the main display. A dialog, a window of fixed size, and a window already maximized are left as they are, and each window is maximized at most once per run, so one a person resizes stays as they left it. A step never operates or maximizes the windows of applications named in the `DAGU_DESKTOP_PROTECTED_APPS` environment variable (executable names separated by commas), such as the app a person starts automation from, nor windows whose title matches one named in `DAGU_DESKTOP_PROTECTED_TITLES` (separated by commas), such as that app's pages in a browser.
- A successful `extract` binds each scalar field (`string`, `number`, `integer`) to the element its value was read from, when exactly one element in the window gives that value and the model read it from an element, not the screen. A later run reads the bound element and converts its value to the field's type (a string trimmed, a number without grouping or currency, full-width digits folded), so `on_miss` reads it with no model and asks the model only when a field's element is missing, and `never` reads it alone. A boolean field binds to a checkbox's state. A table field, an array of objects of scalars, binds to a grid, table, or list: the model names the container and each column's header, and a later run reads every row by header (up to `with.max_rows`, default 10000; `extract.max_rows` for one extract), so a moved column still reads and a missing header is a miss.
- A learned step can be pinned into the step itself so every device of a system runs it with no first-time learning: an `act` takes `recorded` and an `extract` takes `fields`, keyed by system (`windows` or `macos`). `recorded` lists steps, each one of `click`/`double_click`/`right_click` (a selector), `type` (`{into, text}`), `key` (a chord like `ctrl+s`, with an optional `focus` selector to focus first), `scroll` (`{on, dy}`), `choose` (`{from: <combo box or pop-up>, option: <text>}`), or `wait`; a device of the matching system runs them ahead of its own recording, under `never` failing on a miss and under `on_miss` letting the model finish. `fields` maps each output to a selector, or to `{container, columns}` for a table, read with no model. Pins travel with the step; the per-device cache does not. `dagu computer cache show <dag> --step <id>` prints a device's recordings in this form to copy into the step.
- Once a step works in an app (one it launches, the one the run's previous step worked in, or one an exact check or a replay finds in front), it never operates the windows other apps had on the screen when it began, which are the person's, their menus and status items and the Dock or taskbar included: a click or key that would reach one fails with the reason, naming the app to work in, and when one comes forward with nobody at the desktop the step brings its own app back before the model looks. A window that opens while the step runs, such as a save panel or a page a link opens, is the step's to work in. To work in an app that is already open, launch it by name, which brings it forward.
- Each operation's timeline event carries `via` (`element` for a replay or extract read by the window's elements, `screen` for a replay by pixels, `model` for a model request, `exact` for an exact check), `durationMs`, and `tokens`.
- When a model provider asks for confirmation before sensitive actions, the step fails unless `on_confirmation: allow`. Put an `ask` before such an act instead.
- `with.refuse` lists words the step never presses an element named with, such as `[保存, Save, Submit]` to keep a run from pressing the buttons that save or send. It limits presses only, not a read-only mode: the step still types into fields and sends other keys. A click on such an element or on what it holds, such as its label, whether the model aims it or a replay does, fails with the reason, as do a button press with the pointer on one, a drag that starts on one, and Enter, pressed, held, typed as a line break, or held with a click, while one is in the window in front, since Enter may press it as the default button. A Latin word matches a whole word, ignoring case and character width; any other word matches wherever a name holds it. A host that cannot read elements cannot tell what a click lands on, so the words hold only where elements are read.
- `ask: {prompt, as}` puts the step in Waiting and leaves the desktop as it is; the answer becomes `%<as>%` and the step resumes at the next operation.
- One computer step at a time holds a user's desktop, across Dagu processes with different data directories; others wait. A step holds it only from the look its input is based on to the end of that operation: a launch, a model-driven act, a replayed click, key, or typing, a model-read extract, a statement check, a screenshot, and focusing an element, bringing a window forward, or restoring a minimized one, and, on Windows, every element pattern, since UI Automation brings the window forward to perform one. Element patterns under `input: direct` on macOS, exact checks, extracts read from bindings, outlines, and waits run without it, beside a person and beside steps working other apps, in the step's app's window wherever it is, and wait for no person. Two steps working the same app take turns; the one that waits logs an `app` event. While a step holds the desktop, the display stays awake. A step that starts on a locked screen, or while nobody is logged in, waits for it to be unlocked, up to its timeout or 24 hours; a screen that locks while a step runs fails it. Screenshots are saved under `computer/<step id>/` in the run artifacts on failure by default and are not masked.
- Before sending input, a step waits until nobody has touched the desktop for `with.idle` (default `5s`). Actions the model chose on a screen a person has since used are not run; the act brings back the application it was working in, never maximizes the window the person left in front, and the model gets the new screen instead. `idle: 0` turns this off, for example on a dedicated host.
- While a step runs on the console, a mark shows a person at the desktop where each action lands, just before it acts: a frame around the element the step presses, toggles, selects, opens, sets, or focuses through the element itself, a square around the point of a click, scroll, or move, a frame from one end of a drag to the other, and for typing and keys a frame around the field the step focused, or else around the window that takes them. Each mark is labelled with the step's name and the action, never the text typed, in a color of the step's own. A mark takes no focus or input, never moves the pointer, and lets clicks through; Windows and macOS leave it out of every screenshot the step takes, so neither the model nor a replay sees it. `with.marks: false` shows none; a remote session shows none unless `with.marks: true`.

## router.route

Conditional routing based on expression value. Routes reference existing step IDs.

```yaml
steps:
  - id: check_status
    run: "curl -s -o /dev/null -w '%{http_code}' https://example.com"
    output: STATUS

  - id: route
    action: router.route
    with:
      value: ${env.STATUS}
      routes:
        "200":
          - handle_ok
        "re:5\\d{2}":
          - handle_error
          - send_alert
    depends: [check_status]

  - id: handle_ok
    run: echo "success"

  - id: handle_error
    run: echo "server error occurred"

  - id: send_alert
    run: echo "alerting on-call"
```

Routes are evaluated in priority order: exact matches first, then regex, then catch-all.
A step listed under several routes runs once when any of their patterns matches.

---

# Remote Action Packages

Use this reference when creating a reusable package-style action with `dagu-action.yaml`.

Remote actions are different from DAG-local `actions:` templates:

- DAG-local `actions:` are inline wrappers around built-in actions.
- Remote actions are directories or Git repositories that contain a manifest, a DAG entrypoint, and any helper files the action needs.
- Callers use them with `action: owner/repo@version`, `action: name@version`, or `action: source:target@version`.

## Package Layout

```text
dagu-action-notify/
├── dagu-action.yaml
├── workflow.yaml
└── scripts/
    └── notify.sh
```

This reference uses `workflow.yaml` as the recommended entrypoint DAG filename to keep it visually distinct from the `dagu-action.yaml` manifest. The `dag` field can point to any safe relative file path inside the package.

`dagu-action.yaml` supports exactly these fields:

- `apiVersion` - required, currently `v1alpha1`
- `name` - required action name
- `dag` - required relative path to the action DAG file
- `inputs` - optional JSON Schema object for the caller's `with:`
- `outputs` - optional JSON Schema object for the action output object

Unknown manifest keys are rejected. The `dag` path must resolve to a file inside the package.

## Manifest Example

```yaml
apiVersion: v1alpha1
name: notify
dag: workflow.yaml
inputs:
  type: object
  additionalProperties: false
  required: [text]
  properties:
    text:
      type: string
outputs:
  type: object
  additionalProperties: false
  required: [messageId]
  properties:
    messageId:
      type: string
    status:
      type: string
```

`inputs` validates the caller's `with:` object before the action DAG starts. JSON Schema `default` values are validated as schema defaults, but they are not applied to the caller's `with:` object before parameters are passed.

## Action DAG

The action DAG is a normal Dagu workflow. Do not set `working_dir` in the action DAG or local sub-DAGs inside the package; Dagu runs them in the materialized action workspace so relative package files are available.

```yaml
tools:
  - jqlang/jq@jq-1.7.1

params:
  - text
steps:
  - id: send
    run: ./scripts/notify.sh "${params.text}"
    stdout:
      outputs:
        fields:
          messageId:
            decode: json
            select: .id
          status:
            decode: json
            select: .status
```

Scalar `with:` fields are passed as runtime parameters and can be read as `${params.text}`. For structured input, pass an explicit JSON string and decode it in the action DAG; do not assume nested YAML/JSON input objects arrive as structured params.

## Tools

If the action DAG invokes portable external CLIs, declare them with top-level `tools` in the action DAG file. Do not put `tools` in `dagu-action.yaml`; unknown manifest keys are rejected.

Caller DAG tools are not inherited by remote actions. The action DAG is a separate DAG run, and the worker running it prepares that DAG's tools in the worker-local tools cache. Built-in-only actions do not need `tools`, but reusable action packages that call binaries such as `jq`, `yq`, or release helpers should pin those dependencies inside the action DAG.

## Returning Outputs

Use `stdout.outputs` when a command emits the action result on stdout:

```yaml
steps:
  - id: classify
    run: ./classify.sh "${params.text}"
    stdout:
      outputs:
        fields:
          label:
            decode: json
            select: .label
          confidence:
            decode: json
            select: .confidence
```

Use `outputs.write` when the result is assembled from parameters, previous step output, or literals:

```yaml
steps:
  - id: send
    run: ./scripts/notify.sh "${params.text}"
    output:
      response:
        from: stdout
        decode: json

  - id: publish
    depends: [send]
    action: outputs.write
    with:
      values:
        messageId: ${send.output.response.id}
        status: sent
```

Do not use object-form `output:` to return data to the parent DAG. Object-form `output:` is step-scoped inside the action DAG and is read as `${step_id.output.*}`. To cross the action boundary, republish values with `stdout.outputs` or `outputs.write`.

If the manifest declares `outputs`, Dagu validates the final collected action output object after the action DAG returns a run result. Validation failure fails the parent action step. The action executor also writes compact output JSON to the action step stdout for compatibility, but callers should read structured values through `${step.outputs.<field>}`.

Compatibility note: if an action DAG publishes no typed outputs, legacy string-form run outputs from `output: NAME` can be carried as action outputs. New action packages should prefer `stdout.outputs` or `outputs.write` because those define the action boundary explicitly.

## Caller Example

```yaml
params:
  - BUILD_ID: ""

steps:
  - id: notify
    action: acme/dagu-action-notify@v1.2.0
    with:
      text: "Build ${params.BUILD_ID} finished"

  - id: audit
    depends: [notify]
    run: echo "Message ID: ${notify.outputs.messageId}"
```

## References And Workers

Reference formats:

- `name@version` - official Dagu action, resolved as `dagucloud/name`
- `owner/repo@version` - GitHub repository
- `source:target@version` - explicit local path, `file://` path, or Git source

Use immutable tags or commit SHAs for production. Local `source:` paths are useful for development only when the worker executing the action can read the same path. For distributed or heterogeneous workers, prefer GitHub or explicit Git `source:` refs. After resolution, Dagu packages the action workspace and can send that bundle to the worker running the child action DAG.

---

# Dagu CLI Reference

Global flags on all commands: `--config/-c`, `--dagu-home`, `--quiet/-q`, `--cpu-profile`

Advanced and deprecated flags below remain implemented in `internal/cmd/start.go`, `internal/cmd/enqueue.go`, and `internal/cmd/exec.go`, so this reference keeps them documented even when they are mainly used by automation or backward-compatibility paths.

## Core Commands

### dagu start

Execute a DAG.

```sh
dagu start [flags] <dag> [-- params...]
```

Flags:

- `--params/-p` — Parameters (key=value or positional)
- `--name/-N` — Override DAG name
- `--run-id/-r` — Custom run ID
- `--from-run-id` — Historic dag-run ID to use as the template for a new run
- `--only` — Run only this step (name or ID) in a new run of the current definition; repeatable. Other steps are recorded as skipped. Not with `--from-run-id`
- `--outputs-from` — Finished run of the same DAG whose work directory, and the outputs of the skipped steps that the `--only` steps depend on, feed the run (requires `--only`)
- `--output-from` — Run that gives the outputs of ONE skipped step, as `<step>=<run-id>`; repeatable, takes precedence over `--outputs-from` for that step (requires `--only`). The run must be a finished run of the same DAG, and the step must have succeeded there
- `--output` — Deprecated: use `--output-from`. Output of a step skipped by `--only`, as `<step>.<name>=<value>`; repeatable, takes precedence over `--outputs-from` and `--output-from` (requires `--only`)
- `--labels` — Additional labels (comma-separated key=value or key-only)
- `--tags` — Deprecated alias for `--labels`
- `--default-working-dir` — Default working directory for DAGs without explicit workingDir
- `--no-reuse` — Recompute reusable build steps while preserving staged, atomic publication
- `--worker-id` — Worker ID executing this DAG run; auto-set in distributed mode and defaults to `local`
- `--worker-label` — Worker label (`key=value`) this run requires, replacing the DAG's `worker_selector` for this run and kept with the run for retries; repeatable. Not with `--worker-id`
- `--trigger-type` — Trigger source (`scheduler`, `manual`, `webhook`, `subdag`, `retry`, `catchup`, `restart`); defaults to `manual`

A DAG with `restart_policy: always` or `unless-stopped` has one run at a time: while a run is active, `dagu start`, `dagu enqueue` and `dagu retry` are refused. With no active run they work, which is how a stopped service comes back.

### dagu enqueue

Enqueue a DAG run for later execution.

```sh
dagu enqueue [flags] <dag> [-- params...]
```

Flags:

- `--params/-p` — Parameters (key=value or positional)
- `--name/-N` — Override DAG name
- `--run-id/-r` — Custom run ID
- `--queue/-u` — Override the DAG-level queue definition
- `--worker-label` — Worker label (`key=value`) this run requires, replacing the DAG's `worker_selector` for this run and kept with the run so queued dispatch and retries route the same way; repeatable
- `--labels` — Additional labels (comma-separated key=value or key-only)
- `--tags` — Deprecated alias for `--labels`
- `--default-working-dir` — Default working directory for DAGs without explicit workingDir
- `--no-reuse` — Recompute reusable build steps when the queued run starts
- `--trigger-type` — Trigger source (`scheduler`, `manual`, `webhook`, `subdag`, `retry`, `catchup`, `restart`); defaults to `manual`

### dagu exec

Execute a one-off command as a DAG run without a DAG YAML file.

```sh
dagu exec [flags] -- <command> [args...]
```

Flags:

- `--run-id/-r` — Custom run ID
- `--name/-N` — Override DAG name
- `--workdir` — Working directory for the command (defaults to the current directory)
- `--shell` — Override shell binary for the command
- `--base` — Path to a base DAG YAML whose defaults are applied before inline overrides
- `--env/-E` — Environment variable (`KEY=VALUE`) to include in the run; repeatable
- `--dotenv` — Path to a dotenv file to load before execution; repeatable
- `--worker-label` — Worker label selector (`key=value`) for distributed execution; repeatable

### dagu dequeue

Dequeue a DAG run from a queue (marks it as aborted): `dagu dequeue <queue-name> [--dag-run/-d <dag:run-id>]`

### dagu stop

Stop an active DAG run: `dagu stop <dag-name> [--run-id/-r <id>]`

### dagu restart

Stop and restart a DAG run: `dagu restart <dag-name> [--run-id/-r <id>]`

A DAG with `restart_policy: always` or `unless-stopped` is refused: the scheduler starts its runs, so stop the running run instead. With `unless-stopped`, the scheduler starts no run after a stop: start the DAG again with `dagu start`.

### dagu retry

Retry a previous DAG run using the same run ID.

```sh
dagu retry <dag> --run-id/-r <id> [--step <name>] [--downstream] [--bypass-preconditions] [--worker-id <id>]
```

`--step` retries only the selected step. Add `--downstream` to also reset every reachable descendant; unrelated branches keep their current status. `--downstream` requires `--step`. Add `--bypass-preconditions` to skip step precondition evaluation for this retry; it also requires `--step` and a local CLI context. DAG-level preconditions and lifecycle handlers still apply. With `--sub-run-id <child-id>`, the bypass also reaches the selected child retry, including children dispatched to workers.

### dagu human-task complete

Complete a waiting human task in a root DAG run. The run may be local or distributed, but the command operates on the local Dagu data store.

```sh
dagu human-task complete [flags] <root-dag-name>
```

Flags:

- `--run-id/-r` — Root DAG-run ID containing the human task; required
- `--step` — Human task step ID; required and matched against `id`, not the display name
- `--input` — Form input in `key=value` form; repeatable and coerced using the form schema
- `--inputs-json` — Typed form input as one JSON object

`--input` and `--inputs-json` are mutually exclusive. Omit both for an acknowledgement-only task. Completing a human task resumes the DAG run automatically when it unblocks a step or no other step is waiting; otherwise the run keeps waiting. Human tasks cannot be used in sub-DAGs. A distributed run is re-queued, so its scheduler must be running. The command only supports the local context.

```sh
dagu human-task complete --run-id=run-1 --step=review --input environment=production deploy
dagu human-task complete --run-id=run-1 --step=review --inputs-json='{"environment":"production","notify":true}' deploy
```

### dagu human-task push-back

Send a waiting human task that declares `with.push_back` back to its rewind target with feedback. The rewind target and every step that depends on it, directly or transitively, run again, then the task opens again.

```sh
dagu human-task push-back [flags] <root-dag-name>
```

Flags:

- `--run-id/-r`: Root DAG-run ID containing the human task; required
- `--step`: Human task step ID; required and matched against `id`
- `--input`: Feedback in `key=value` form; repeatable and coerced using `with.push_back.form`
- `--inputs-json`: Typed feedback as one JSON object
- `--expected-iteration`: Fail unless the task is at this push-back iteration; `0` before the first push-back

Feedback is limited to 16 KiB as JSON. The push-back is stored before the run is queued. If queueing fails, run the same command again: until the task opens again, an identical repeat only retries the queue and prints `Human task <step> was already pushed back to <target>`. After the task reopens, a repeat pushes it back again, so pass `--expected-iteration` when a command may be repeated. The command only supports the local context.

```sh
dagu human-task push-back --run-id=run-1 --step=review --input feedback="Add tests" --expected-iteration=0 deploy
```

### dagu dry

Dry-run a DAG without executing commands: `dagu dry [--params/-p] [--name/-N] [--no-reuse] [--format/-f text|json] <dag> [-- params...]`

`--format json` prints one JSON object on stdout, `{"checks":[...]}`, and omits the run summary. `text` is the default. A check does not change the exit status.

For a build DAG, `--no-reuse` previews the decisions with manifest reuse disabled. Dry-run still creates no locks, staging files, manifests, or run history.

### dagu validate

Validate DAG YAML without executing: `dagu validate <dag>`

### dagu status

Show DAG run status: `dagu status <dag-name> [--run-id/-r <id>] [--sub-run-id/-s <id>]`

### dagu history

Show DAG run history.

```sh
dagu history [dag-name]
```

Flags:

- `--from` — Start date/time in UTC (format: `2006-01-02` or `2006-01-02T15:04:05Z`)
- `--to` — End date/time in UTC (same formats as `--from`)
- `--last` — Relative time period (e.g. `7d`, `24h`, `1w`). Cannot combine with `--from`/`--to`
- `--status` — Filter by status: `running`, `succeeded`, `failed`, `aborted`, `queued`, `waiting`, `rejected`, `not_started`, `partially_succeeded`
- `--run-id` — Filter by run ID (partial match supported)
- `--labels` — Filter by labels (comma-separated key=value or key-only, AND logic)
- `--tags` — Deprecated alias for `--labels`
- `--format/-f` — Output format: `table` (default), `json`, `csv`
- `--limit/-l` — Max results (default 100, max 1000)

Default: shows runs from the last 30 days, newest first.

### dagu ls

List DAG definitions.

This command is local-only. If a remote CLI context is selected, use `--context local`.

```sh
dagu ls [flags] [pattern]
```

Flags:

- `--next/-n` — Show next scheduled run time
- `--last/-l` — Show last run status and time
- `--history/-H` — Show a compact recent-history summary
- `--sort-last/-t` — Sort by last run time, newest first
- `--reverse/-r` — Reverse sort order

### dagu rm

Remove DAG run history and/or the DAG YAML definition. At least one of `--history` or `--definition` is required. Active runs are never deleted from history; definition deletion is refused while the DAG has alive processes. With `--definition`, identify the DAG by filename, stem, or configured path. Deleting all history (no `--older-than`) also clears the browser and computer replay caches of the DAG on this host.

```sh
dagu rm [--history|-H] [--definition|-d] [-t <duration>] [-f] [--dry-run] <dag>
```

Flags:

- `--history/-H` — Delete run history
- `--definition/-d` — Delete the DAG YAML definition
- `--older-than/-t` — With `--history`: delete runs older than a duration (e.g. `10d`, `24h`, `1w`). Omitted = delete all history
- `--force/-f` — Skip confirmation prompt
- `--dry-run` — Preview deletions without removing history or the definition

### dagu browser cache clear

Clear the recorded `act` operations that browser steps replay, so the next run asks the model again. Use it after a site changes its layout. Without `--step`, every step of the DAG is cleared. The cache lives on the host that ran the step; in distributed mode, run it on the worker. REST: `DELETE /api/v1/dags/{fileName}/browser-cache[?step=<id>]`.

```sh
dagu browser cache clear <dag> [--step <id>]
```

### dagu browser session

Work a site in a browser kept open between commands, then turn what worked into a `browser.run` step. Each `do` runs one operation, read from stdin in the form of a `with.do` item, exactly as a step would, and reports what it did, the actions an act performed, what an extract read, and the page next with its outline. `describe` outlines the page without a model: fields with their labels (a field the page labels only by the cell before it in a table row takes that cell's text, numbered in order when fields of one kind share the cell, else its `name` or `id` attribute), selects with their options, file inputs, buttons, links with their addresses, tables and lists, each element with an ID that `do` can act on without a model: `{"click": ID}`, `{"type": {"into": ID, "text": TEXT}}`, `{"select": {"in": ID, "option": OPTION}}`, kept as the act a step writes, and `{"upload": {"into": ID, "files": PATHS}}`, kept as the upload into that element in words, each with its recording. An upload's relative paths are under the current directory, and the exported step uploads the same files until you point `files` at what each run uploads. `export` builds the step from the operations that succeeded and writes their act and upload recordings to the step's replay cache, so its first run on this host replays them without a model request. Variable values go on stdin under `variables`; `{"env": NAME}` is read from the environment on every command and masked, a plain string serves one command. Every command prints one JSON object; a failure prints `{"error": {"code", "message"}}` and exits 1. When the site refused the page, with a bot check or a 403 or 429, `open`, `do`, and `describe` report `refused: {host, status, bot_check, message}`, and the error of an operation that failed on it ends with the message. A session closes its browser when idle past `--idle-timeout` (default `30m`); its history can be exported for a day after. `close --keep` ends it the same way at once, freeing its profile for a step.

```sh
dagu browser session open [URL] [--provider P --model M | --llm '<llm block>'] [--profile N] [--headed] [--viewport WxH] [--allowed-domain D]... [--idle-timeout 30m]
echo '{"act": "Click Sign in"}' | dagu browser session do <id>
echo '{"click": "0-131"}' | dagu browser session do <id>
echo '{"upload": {"into": "0-57", "files": "/path/to/invoice.pdf"}}' | dagu browser session do <id>
dagu browser session describe <id> [--find TEXT] [--format text] [--screenshot NAME]
dagu browser session export <id> --dag <name|file> --step <id> [--skip N]... [--dry-run] [--close] [--format yaml]
dagu browser session close <id> [--force] [--keep]
dagu browser session list
```

### dagu computer check

Check that computer steps can capture the screen and send input in the current session. Run it as the user and in the session of the worker that runs computer steps. On macOS it also asks macOS to show the Screen Recording and Accessibility prompts for missing permissions. Exits nonzero and lists the problems when the desktop cannot be automated. `--format json` prints `{os, width, height, ready, problems}`, each problem with a `code` (such as `screen_recording`, `accessibility`, `screen_locked`, or `session_disconnected` for a Remote Desktop or child session whose client has gone) and a `message`.

```sh
dagu computer check [--format json]
```

### dagu computer elements

List the accessible elements of the window in front on macOS (with the Accessibility permission) or 64-bit Windows: each with `role`, `name`, `id`, `value`, `placeholder` (the hint an empty field shows, on macOS), `label`, `bounds` in display pixels, `window`, and `path` from the window down. `--at-pointer` reports the element under the pointer after `--after`; `--watch` reports it whenever it changes, one JSON object per line, until stdin closes; `--match` reports the elements a selector matches and succeeds only on exactly one, with `-` reading the selector from stdin. A selector is JSON with `role` and `name`, `id`, or `near` (`{label, side}`), optionally `app`, `window`, `in`, and `nth`; names take `*`. `--format json` prints one object; a failure prints `{"error": {"code", "message"}}` and exits 1, with `invalid_input`, `unsupported`, `load_failed`, `no_elements`, `not_found`, `ambiguous`, `failed`, or a `check` problem code.

```sh
dagu computer elements [--format json] [--limit N] [--at-pointer [--after 3s] | --watch | --match '<selector JSON>' | --match -]
```

### dagu computer cache clear

Clear the recorded `act` operations that computer steps replay, so the next run asks the model again. Without `--step`, every step of the DAG is cleared. The cache lives on the host that ran the step; in distributed mode, run it on the worker.

```sh
dagu computer cache clear <dag> [--step <id> [--op <position>]]
```

### dagu computer cache show

Show what a DAG's computer steps recorded on this host in the form a step pins it, to review and write into the step's `recorded` or `fields` so every device of that system runs it with no model. Without `--step`, every step is shown; `--op` keeps one task, counted from 1. A task recorded under several instructions or parameters shows each, newest first. An act with an action that has no pinned form, such as a drag, a click where no element could be read, or one near an element's edge, lists each such action and why instead of steps. An act whose actions never used a variable its task names, as one recorded where a field already showed the value, says so in a comment above its steps, since pinned they leave that field as each run finds it; one recorded by an older version, which did not note them, says to check its steps. `--format json` prints `{operations}`, each with `step`, `op`, `kind`, `system`, `saved_at`, `assumes` (those variables), `assumes_unknown` (recorded before they were noted), and `recorded`, `fields`, or `unpinnable` (`turn`, `action`, `code`, `reason`).

```sh
dagu computer cache show <dag> [--step <id> [--op <position>]] [--format json]
```

### dagu xlsx inspect

Describe every sheet of an `.xlsx` workbook: used range, detected data block, header row, column names and types, a profile of each column, row count, tables, and a few typed sample rows, plus the workbook's named ranges and date system. Types and the profile cover every data row up to 5000: filled and blank counts, distinct count, the values when a few repeat, min and max of number and date columns, and the cells that do not read as the column's type. The text format shows them after each column, as in `状態 (string: 済, 未; 40 blank), 数量 (number; 1..250; 1 odd: D300 "未定")`. It reads the file directly, needs no configuration or engine, and creates no run. `--sheet` describes one sheet only and `--rows` sets the sample size. A hidden sheet is marked `(hidden)` and rows hidden by a filter or by hand are counted, as in `300 rows, 12 hidden`. A protected workbook takes its password from `DAGU_XLSX_PASSWORD`, which keeps it out of the process list; `--password` is a convenience that wins when both are set. `--format json` prints one object: `path`, `date_system`, `sheets` (each with `name`, `hidden`, `used_range`, `range`, `header_row`, `headers`, `types`, `row_count`, `hidden_rows`, `columns`, `profile_truncated`, `tables`, `sample`), `named_ranges`, and `warnings`.

```sh
dagu xlsx inspect <path> [--sheet <name>] [--rows <n>] [--password <password>] [--format json]
```

### dagu xlsx read

Print the typed rows of a sheet the way `xlsx.read` publishes them: numbers stay numbers, dates become ISO 8601 text, text keeps its leading zeros, and each row carries `_row`. The flags mirror the action's fields: `--sheet`, `--range`, `--header` (`true`, `false`, a row number, or `3,4`), `--columns` (comma-separated, with `name:alias` renames), `--types` (`name:type` pairs such as `Amount:number,Due:date`, or a JSON object; Japanese forms such as `123,456` and `令和8年10月3日` convert as under the action's `types`), `--on-type-error` (`fail`, `warn` to read the cell as null, or `keep` to keep it as read; both warn), `--max-rows`, `--skip-hidden` (leave out rows hidden by a filter or by hand), and `--password` (a protected workbook; prefer `DAGU_XLSX_PASSWORD`, which keeps it out of the process list, and the flag wins when both are set). The text format is tab-separated, with tabs, line breaks, and backslashes inside a cell escaped as `\t`, `\n`, `\r`, and `\\` so one cell stays in one column; `--format json` prints `rows`, `count`, `headers`, `sheet`, `range`, `warnings`, and `truncated`.

```sh
dagu xlsx read <path> [--sheet <name>] [--range A2:F] [--header false] [--columns "a,b:c"] [--types "a:number"] [--on-type-error keep] [--max-rows <n>] [--skip-hidden] [--password <password>] [--format json]
```

### dagu ps

List running DAG processes.

```sh
dagu ps [-d <dag-name>] [-r <run-id>]
```

`-r`/`--run-id` accepts a partial run ID and matches accordingly.

### dagu cleanup

Remove old DAG run history. Active runs are never deleted.

Deprecated: prefer `dagu rm --history`.

```sh
dagu cleanup <dag-name> [--retention-days <n>] [--dry-run] [--yes/-y]
```

### dagu prune-artifacts

Remove artifact directories and index records that no surviving DAG run points to. Orphans appear when a run record is deleted by a route other than `dagu rm`, or when the artifact root moved. Only the artifact layout is examined: `<root>/YYYY/MM/DD/<run>` directories with their index records, and pre-date `<root>/<dag>/dag-run_<ts>_<id>` directories. Liveness is decided by name: an entry is removed only when no run in the current history tree could still claim it, and only when it is older than `--older-than`. A minimum age of 1h is always enforced because a run's artifact directory exists before its record does. Without `--yes`, the command reports how many entries it found before asking to delete them.

```sh
dagu prune-artifacts [--older-than|-t <duration>] [--root <dir>] [--dry-run] [--yes/-y]
```

Flags:

- `--older-than/-t` — Only remove entries older than a duration (e.g. `10d`, `24h`, `1w`). Default `24h`; a minimum of 1h is enforced
- `--root` — Artifact root to prune (default: configured `paths.artifact_dir`). Pass a previous artifacts directory (e.g. `<old data_dir>/artifacts`) or a DAG's `artifacts.dir`. A root that holds the run history or the log directory is refused
- `--dry-run` — Preview removals without deleting
- `--yes/-y` — Skip confirmation prompt

### dagu schema

Show JSON schema documentation. Use a dot-separated path to drill into nested sections.

```sh
dagu schema <dag|config> [path]
```

Examples:

- `dagu schema dag` — All DAG root-level fields
- `dagu schema dag steps` — Step definition structure
- `dagu schema dag steps.container` — Container configuration
- `dagu schema dag steps.retry_policy` — Retry policy fields
- `dagu schema dag steps.harness` — Harness step configuration
- `dagu schema dag handler_on` — Lifecycle event hooks
- `dagu schema config` — All config root-level fields
- `dagu schema config auth` — Authentication configuration

### dagu config

Show resolved configuration paths.

```sh
dagu config
```

## Server & Scheduling

### dagu start-all

Start server + scheduler + optionally coordinator in one process. Coordinator enabled by default (disable with `DAGU_COORDINATOR_ENABLED=false`).

```sh
dagu start-all [--host/-s <host>] [--port/-p <port>] [--dags/-d <dir>]
```

Also accepts `--coordinator.*` and `--peer.*` flags for distributed setup.

### dagu server

Start web UI + REST API.

```sh
dagu server [--host/-s <host>] [--port/-p <port>] [--dags/-d <dir>] [--tunnel/-t]
```

### dagu scheduler

Start cron scheduler. Monitors DAGs and triggers runs on schedule; also processes queued runs.

```sh
dagu scheduler [--dags/-d <dir>]
```

## Distributed Execution

### dagu coordinator

Start gRPC coordinator: `dagu coordinator [--coordinator.host/-H <host>] [--coordinator.port/-P <port>] [--peer.*]`

### dagu worker

Start distributed worker: `dagu worker --worker.coordinators <host:port,...> [--worker.id/-w <id>] [--worker.max-active-runs/-m <n>] [--worker.labels/-l <k=v,...>] [--peer.*]`. Coordinator addresses are required. Every worker advertises immutable `os` and `arch` platform labels in addition to configured labels.

## Git Sync

`dagu sync <subcommand>` — Git sync for workflows, Wiki content, calendars, and supporting files.

| Subcommand | Description |
| ---------- | ----------- |
| `sync status` | Show repository, branch, and per-item status |
| `sync pull` | Pull changes from remote |
| `sync publish [item-id] [--message/-m] [--all] [--force/-f]` | Publish local changes to remote |
| `sync discard <item-id> [--yes/-y]` | Discard local changes, restore remote version |
| `sync forget <id>... [--yes/-y]` | Remove state entries for missing/untracked items |
| `sync cleanup [--dry-run] [--yes/-y]` | Remove all missing entries from sync state |
| `sync delete <id> [--message/-m] [--force] [--all-missing] [--dry-run] [--yes/-y]` | Delete from remote, local, and sync state |
| `sync mv <old> <new> [--message/-m] [--force] [--dry-run] [--yes/-y]` | Rename across local, remote, and sync state |

## Other Commands

- `dagu example [id]` — Show built-in example DAGs
- `dagu version` — Show version
- `dagu upgrade [--check] [--version/-v <ver>] [--dry-run] [--yes/-y]` — Self-update binary
- `dagu license <activate [key]|deactivate|check>` — Manage license; `activate` without a key prints a Dagu Console approval URL and waits for an owner to approve this server
- `dagu secret resolve <ref> [--workspace <name>]` — Print a registry secret's plaintext value to stdout, without a trailing newline; a workspace falls back to global like a DAG's `secrets:` entry, and an unknown workspace fails. Each read is recorded in the audit log. Local context only

---

# Context References, Scoped Values, And Step Outputs

Use `${context.*}` when DAG YAML needs metadata about the current DAG run. These values are resolved by Dagu before the step executor runs.

If a supported context value is not available in the current scope, Dagu keeps the original reference text. It does not replace the reference with an empty string.

## Scoped Value References

Use scoped references when Dagu should resolve a named value before execution.

| Reference | Meaning |
| --- | --- |
| `${consts.NAME}` | Top-level `consts:` value |
| `${params.NAME}` | Runtime parameter value |
| `${env.NAME}` | Value in Dagu's runtime environment scope for the current step |
| `${steps.<step_id>.outputs.<name>}` | Declared value output, human-task form value, or committed build path output |

Use shell `$NAME` or `printenv NAME` only when the target shell or process should read the variable at execution time.

`${steps.<step_id>.outputs.<name>}` is only for declared step outputs. Other step properties keep the step-ID form that the resolver supports, such as `${step_id.stdout}`, `${step_id.output.name}`, and `${step_id.outputs.name}`.

## Top-Level `consts:`

Use `consts:` for static DAG values that Dagu should resolve before step execution. Constants are not process environment variables. Use `${consts.NAME}` when Dagu should substitute the value into a DAG field. Copy a constant into `env:` only when a child process needs an environment variable.

```yaml
consts:
  - service: api
  - region: us-east-1
  - endpoint: https://${consts.service}.${consts.region}.example

env:
  - API_ENDPOINT=${consts.endpoint}

steps:
  - id: healthcheck
    run: curl -fsS '${consts.endpoint}/health'
```

Rules:

- `consts:` must use list form. Mapping form is invalid.
- Each list item must be a single-key mapping, such as `- service: api`.
- Names must start with a letter and then contain only letters, digits, or `_`.
- Values must be literal strings, finite numbers, or booleans. `null`, arrays, and objects are invalid.
- A const can reference inherited consts or earlier consts in the same list with `${consts.NAME}`.
- A const cannot read runtime values while it is being defined. References such as `${params.NAME}`, `${env.NAME}`, `${steps.step_id.outputs.name}`, self-references, later const references, and unknown const references remain unresolved text inside the const value.
- Consts from a base config are inherited. A DAG-local const with the same name overrides the inherited value.

Use `consts:` for fixed workflow configuration. Use `params:` for per-run user input. Use `env:` for values that should be available to the running process environment.

## Supported Context References

Run and step metadata:

| Reference | Meaning |
| --- | --- |
| `${context.dag.name}` | Current DAG name |
| `${context.run.id}` | Current DAG run ID |
| `${context.run.status}` | Current run status when status is available, such as lifecycle handler scopes |
| `${context.run.scheduled_at}` | Scheduled time for a scheduled run |
| `${context.run.root_name}` | Root DAG name when this run is part of a nested run |
| `${context.run.root_id}` | Root run ID when this run is part of a nested run |
| `${context.attempt.id}` | Current attempt ID |
| `${context.attempt.started_at}` | Current attempt start time |
| `${context.step.id}` | Current step ID |
| `${context.step.name}` | Current step name |
| `${context.step.repeat_index}` | Zero-based execution index of a step with `repeat_policy`; also `DAG_RUN_STEP_REPEAT_INDEX` |
| `${context.trigger.type}` | Run trigger type when trigger metadata is available |
| `${context.trigger.actor}` | Trigger actor when trigger metadata includes one |

Path metadata:

| Reference | Meaning |
| --- | --- |
| `${context.paths.log_file}` | DAG run log file path |
| `${context.paths.work_dir}` | Per-run working directory when one exists |
| `${context.paths.artifacts_dir}` | Artifact directory when artifacts are enabled |
| `${context.paths.step_stdout_file}` | Current step stdout log file |
| `${context.paths.step_stderr_file}` | Current step stderr log file |
| `${context.paths.step_output_file}` | Current step output file path |

Other scoped metadata:

| Reference | Meaning |
| --- | --- |
| `${context.profile.name}` | Runtime profile name when profile metadata exists |
| `${context.profile.resolved_at}` | Runtime profile resolution time when profile metadata exists |
| `${context.pushback.iteration}` | Push-back iteration when the step is running in push-back flow |
| `${context.pushback.previous_stdout_file}` | Previous stdout file when push-back provides one |

Example:

```yaml
steps:
  - id: summarize
    run: ./summarize.sh '${context.dag.name}' '${context.run.id}'
```

Use single quotes around context references when the shell should receive the resolved value as one argument.

## Declared Value Outputs

A step can declare value outputs and write them during execution. Dagu reads the output file after the command succeeds and publishes the values as `${steps.<step_id>.outputs.<name>}`.

Human task form outputs use the same reference namespace without an authored `outputs:` field. Every declared property is published when submitted or defaulted.

```yaml
steps:
  - id: build
    run: |
      printf 'image_tag=v1.2.3\n' >> "$DAGU_OUTPUT_FILE"
      {
        printf 'metadata<<JSON\n'
        printf '{"commit":"abc123"}\n'
        printf 'JSON\n'
      } >> "$DAGU_OUTPUT_FILE"
    outputs:
      - name: image_tag
      - name: metadata
        type: json

  - id: deploy
    depends: [build]
    run: ./deploy.sh '${steps.build.outputs.image_tag}'
```

Rules:

- A step that declares `outputs:` must have an `id`.
- `outputs:` must be a non-empty sequence.
- Each output must declare `name`.
- `type` is optional. Supported values are `string` and `json`.
- The output file must contain valid UTF-8.
- Use `name=value` for a single-line value.
- Use `name<<DELIMITER`, value lines, and a matching `DELIMITER` for a multi-line value.
- Every declared output must be written exactly once.
- Writing an undeclared output fails the step.
- Writing a duplicate output fails the step.
- For `type: json`, the emitted value must be valid JSON.
- Dagu captures declared outputs only after the command itself succeeds.

Declared step outputs are different from DAG or remote action outputs. To publish caller-visible DAG/action outputs, use `stdout.outputs` or `action: outputs.write`.

In a `type: build` workflow, an `outputs:` entry may declare `path` instead of a value `type`. The owning step writes to `${outputs.<name>}`, and `${steps.<step_id>.outputs.<name>}` publishes the final path only after commit or reuse. See `references/build.md`.

---

# Build Workflows

Use `type: build` for local workflows that transform stable regular files and should reuse unchanged work across DAG runs. Build execution is materialization reuse, not a cache that restores a missing output: a missing or modified final output causes recomputation.

## Minimal Pipeline

```yaml
type: build
working_dir: /srv/build

steps:
  - id: compile
    inputs:
      - name: source
        path: source.c
    outputs:
      - name: binary
        path: app.bin
    run: compiler "${inputs.source}" -o "${outputs.binary}"

  - id: package
    inputs:
      - name: binary
        path: app.bin
    outputs:
      - name: archive
        path: app.tar
    run: packager "${inputs.binary}" "${outputs.archive}"
```

The `package` step does not need `depends: compile`. Its canonical input path matches the `compile` output path, so Dagu infers the dependency. Keep explicit dependencies for ordering that is not represented by a file.

## Declarations and References

- Set a stable `working_dir`, or load the DAG from a file whose directory can anchor relative paths.
- Each input requires `name` and `path`.
- A path-backed output requires `name` and `path`. It cannot also declare a value `type`.
- A step may declare at most one path-backed output.
- Names must start with a letter and contain only letters, digits, or `_`.
- Path declarations are supported only on host command and shell steps without DAG-level or step-level containers.
- Output parent directories must already exist.

| Reference | Meaning |
| --- | --- |
| `${inputs.<name>}` | Absolute final path of the owning step's declared input |
| `${outputs.<name>}` | Fresh, initially absent staging path for the current executor attempt |
| `${steps.<step_id>.outputs.<name>}` | Absolute final output path after commit or reuse |

`${inputs.*}` and `${outputs.*}` are step-scoped. Use `${outputs.<name>}` only in command text, scripts, arguments, and executor environment where an attempt staging path exists. Path declarations may use stable parameters and environment values, but must resolve before execution; do not derive them from step outputs or command substitution.

## Dependency and Path Rules

- Matching canonical producer outputs and consumer inputs create inferred dependencies.
- Each canonical output path must have exactly one producer.
- A step cannot declare the same canonical path as both input and output.
- Explicit and inferred dependencies must remain acyclic.
- Inputs and outputs must be regular, non-symlink files. Inputs must exist when the step is evaluated.

## Reuse and Publication Safety

A reusable step has exactly one path-backed output and a stable host command or shell recipe. Steps with dynamic or scalar output surfaces, secrets, repeat, human tasks, approvals, parallel or foreach bodies, child DAGs, or containers execute normally instead of being reused.

Dagu hashes the resolved recipe, declared input contents, and current output. A matching materialization produces a `reuse` decision and a succeeded node. Changed inputs, recipe, environment, tools, working directory, missing output, or modified output produce an `execute` decision.

The default per-run scratch directory is normalized in the recipe digest, so a different run ID does not prevent reuse by itself. An authored working directory remains part of the recipe and changing it requires execution.

Always write the result to `${outputs.<name>}`. Dagu verifies the staged file and input snapshots before atomically replacing the final output. Writing directly to the final output bypasses that contract. `stdout`, `stderr`, `stdout.artifact`, and `stderr.artifact` destinations cannot target any declared build input or output.

Potentially reusable producers expose downstream data only through `${steps.<step_id>.outputs.<name>}`. Do not read `${step_id.stdout}`, `${step_id.stderr}`, or `${step_id.exit_code}` from them. This also applies to `${step_id.output.<name>}` and `${step_id.outputs.<name>}`, including their whole-value `${step_id.output}` and `${step_id.outputs}` forms, because reuse does not recreate attempt results. Path-output steps also cannot use `continue_on.mark_success`; failed attempts must not expose an old or missing final file as a successful publication.

Each retry receives a new staging path. A failed, timed-out, or aborted attempt removes its staging file and leaves the previous final output and manifest unchanged.
Crash recovery refuses to overwrite a final output that was externally replaced with content matching neither the previous nor the proposed materialization.

## Execution Controls

Build workflows are local-only. Distributed execution is rejected because workers do not share the required materialization fencing.

Preview decisions without executing:

```sh
dagu dry workflow.yaml
```

Disable reuse for one run without disabling staging or commit safety:

```sh
dagu start --no-reuse workflow.yaml
dagu enqueue --no-reuse workflow.yaml
dagu dry --no-reuse workflow.yaml
```

In the Web UI, enable **Disable reuse for this run** in the Start or Enqueue dialog. REST and MCP `dagu_execute` clients can send `noReuse: true` for start or enqueue.

---

# External Harnesses

Use `action: harness.run` to invoke external coding agents from DAG steps. Dagu selects the configured provider; its executable must be installed on the host or available in the selected container.

## Supported Providers

| Provider | Binary | Invocation | Supplementary input |
|----------|--------|------------|---------------------|
| `aider` | `aider` | `aider --message "<prompt>" [flags]` | Folded into the prompt |
| `amp` | `amp` | `amp -x "<prompt>" [flags]` | Piped to stdin |
| `claude` | `claude` | `claude -p "<prompt>" [flags]` | Piped to stdin |
| `cline` | `cline` | `cline [flags] "<prompt>"` | Piped to stdin |
| `codex` | `codex` | `codex exec "<prompt>" [flags]` | Piped to stdin |
| `copilot` | `copilot` | `copilot -p "<prompt>" [flags]` | Piped to stdin |
| `cursor` | `cursor-agent` | `cursor-agent -p "<prompt>" [flags]` | Folded into the prompt |
| `deepseek` | `dsh` | `dsh --profile headless [flags] "<prompt>"` | Folded into the prompt |
| `droid` | `droid` | `droid exec "<prompt>" [flags]` | Folded into the prompt |
| `gemini` | `gemini` | `gemini -p "<prompt>" [flags]` | Piped to stdin |
| `goose` | `goose` | `goose run --text "<prompt>" [flags]` | Folded into the prompt |
| `kilo` | `kilo` | `kilo run "<prompt>" --auto [flags]` | Piped to stdin |
| `kiro` | `kiro-cli` | `kiro-cli chat --no-interactive "<prompt>" [flags]` | Piped to stdin |
| `opencode` | `opencode` | Managed session or `opencode run "<prompt>" [flags]` | Piped to stdin on the CLI path |
| `pi` | `pi` | `pi -p "<prompt>" [flags]` | Piped to stdin |
| `qwen` | `qwen` | `qwen -p "<prompt>" [flags]` | Piped to stdin |

Codex defaults to `skip_git_repo_check: true`, so its default invocation includes `--skip-git-repo-check`. Set `skip_git_repo_check: false` or `skip-git-repo-check: false` to omit it.

Cursor defaults to `output_format: text`, and Goose defaults to `quiet: true`. Explicit values in `with` override these defaults.

Kilo defaults to `auto: true`, so `provider: kilo` runs unattended. Set `auto: false` to disable automatic approval explicitly.

The `deepseek` adapter targets the official DeepSeek Harness `dsh` CLI and its headless profile. It does not select DeepSeek as the model backend for another harness.

Claude's `bare` flag skips keychain reads among other things, so a subscription login is invisible to the step and the run fails with `Not logged in`. Set it only when the credential comes from `ANTHROPIC_API_KEY` in the step environment.

For host subprocess runs, built-in provider adapters resolve binaries through `PATH`. Host custom harnesses can use a binary name or an explicit path; relative paths with a path separator are resolved from the step working directory. For containerized runs, the binary is executed inside the selected container and must be valid there.

## Feature Reference

- `with.prompt` is required and is passed to the selected provider according to its built-in adapter or custom harness definition. Multiline prompt text is preserved.
- `with.stdin` is optional supplementary input for host subprocess runs. Each built-in adapter either pipes it to stdin or folds it into the prompt as shown above. Containerized harness runs reject it.
- Built-in provider adapters are `aider`, `amp`, `claude`, `cline`, `codex`, `copilot`, `cursor`, `deepseek`, `droid`, `gemini`, `goose`, `kilo`, `kiro`, `opencode`, `pi`, and `qwen`. Non-reserved `with` keys become CLI flags.
- Custom providers must be declared under top-level `harnesses:`. A non-null custom definition shadows a built-in provider with the same name; deleting the custom definition exposes the built-in again.
- `fallback` is an ordered list of provider configs. Dagu tries the next config only when the previous attempt fails and the run context is still active. Fallback configs cannot contain another `fallback`.
- `provider` may use value references only if they resolve to a concrete provider string before executor creation. If `${...}` remains unresolved at runtime, the harness fails with an unresolved provider template error.
- `provider` and `fallback` are harness control keys. They are not passed as CLI flags.

### Managed OpenCode Sessions

Built-in `provider: opencode` steps use a managed OpenCode server session by default when they run from a standalone Dagu server, from `dagu start-all`, or on a distributed worker. The run page shows the agent timeline and lets an operator answer permission requests and questions. Each managed step has its own conversation by default; runs with multiple managed steps provide a step selector in the Agent tab. A waiting answer suspends the step durably; Dagu resumes the same OpenCode session on the host that owns it. The final assistant text becomes step stdout.

Root interaction responses and clean-session restarts resume directly unless the run belongs to an enabled global queue. Standalone API servers resume unqueued local sessions on their own OpenCode host. Enabled global queues require a running scheduler; use `dagu start-all` for queued local managed sessions so the scheduler shares the server's OpenCode host. Distributed sessions retain their owning worker.

Managed mode starts one process-local OpenCode server under the Dagu service identity. Install a compatible `opencode` executable for that service and authenticate it with `opencode auth login` (normally stored in `~/.local/share/opencode/auth.json`) or pass selected provider credentials through the service configuration:

```yaml
opencode:
  executable: opencode
  env_passthrough:
    - OPENAI_API_KEY
```

The equivalent environment variables are `DAGU_OPENCODE_EXECUTABLE` and `DAGU_OPENCODE_ENV_PASSTHROUGH`. A DAG-level `tools:` or `secrets:` declaration affects step subprocesses and supplies only the CLI integration, not the long-lived managed server.

Managed mode forces OpenCode sharing off and does not support `share`; explicit sharing uses the CLI integration and may publish the conversation at a public URL.

Managed options are `agent`, `model`, `variant`, `session`, `fork`, `title`, `file`, `command`, and `format: default`. Set `managed: false` to force the one-shot CLI. Options outside that set, `share`, non-default formats, containers, standalone scheduler launches, embedded-engine runs, and direct Dagu CLI execution use the CLI integration. Set `managed: true` to require managed mode and fail clearly when the current execution topology or provider capabilities cannot support it. Automatic mode may fall back only before a managed session is created.

“Allow for this Dagu session” applies only provider-proposed wildcard patterns to the current Dagu session generation; Dagu never grants OpenCode's process-wide `always` permission. Dagu-created and forked sessions are retained until the DAG run is cleaned up, which also removes their provider resources. Sessions supplied with `session:` remain externally owned and are retained.

If the owning worker or OpenCode server disappears, the step remains waiting and the run page offers a clean-session restart. A clean restart discards the conversation and pending interaction, then submits the original prompt to a new session. A retry after a terminal session error also submits the original prompt in a new session generation. Previous Dagu-owned sessions remain available until DAG-run cleanup, and files already changed in the shared workspace are not reverted.

OpenCode persists managed conversations in the Dagu service user's OpenCode data directory; run `opencode debug paths` as that user to locate it. Dagu stores the session reference and UI timeline with the DAG run. If OpenCode's session data is lost, Dagu remains running and marks the affected step unavailable. Use **Start clean session** to create a new conversation and submit the original prompt again.

The managed server is a shared service-identity boundary for trusted workflows. Use separate Dagu workers or containerized CLI execution when workflows require isolation from one another. Managed compatibility is capability-checked at startup; OpenCode v1.18.11 and v2.1.4 are tested releases. Install v2 with `npm install -g @opencode/cli@2.1.4`; the v1 package is `opencode-ai`. Dagu selects the managed HTTP protocol from the server version. V2 message history is paginated, and pending permissions and question forms are reconciled while a step runs. For v2, specify `model` when using the separate `variant` option, or use `model: provider/model#variant`. CLI-only options must match the installed OpenCode version; v2 no longer accepts the v1 `--variant` and `--command` flags. Use managed mode for Dagu’s `variant` and `command` options. A managed v2 command must submit a prompt to the current session, as configured template commands do; callbacks that only change settings or launch a separate subagent do not produce a harness result. The existing question interface handles OpenCode’s built-in question forms.

## How `with` Works

Harness supports built-in provider adapters and named custom harness definitions:

- `with.provider` selects a built-in provider adapter or a custom `harnesses:` entry
- top-level `harnesses.<name>` defines a custom CLI or ACP connection

For CLI provider adapters and local ACP startup, non-reserved `with` keys are passed directly as CLI flags:

- `key: "value"` → `--key value`
- `key: true` → `--key`
- `key: false` → omitted
- `key: 123` → `--key 123`
- Arrays repeat the flag once per item
- Built-in provider adapters also normalize `snake_case` keys to kebab-case flags, so `max_turns` becomes `--max-turns`

Reserved keys are `prompt`, `stdin`, `provider`, `fallback`, and `managed`.

## Custom Harness Registry

Define reusable custom harness adapters once at the DAG level:

```yaml
harnesses:
  gemini-custom:
    binary: gemini
    prompt_mode: flag
    prompt_flag: --prompt
    option_flags:
      model: --model

steps:
  - id: review
    action: harness.run
    with:
      prompt: "Review the current branch"
      provider: gemini-custom
      model: gemini-2.5-pro
```

Custom harness definition fields:

- `protocol` — `cli` (default) or `acp`
- `transport` — ACP `stdio` (default), `websocket`, or `http`
- `url` — remote ACP endpoint; `ws://`/`wss://` or `http://`/`https://`
- `headers` — remote request headers; values support workflow variables
- `cwd` — absolute working directory on the remote agent host
- `ca_file` — optional local PEM roots for a remote connection, alongside system roots
- `bearer_subprotocol` — WebSocket-only opt-in for bridges requiring `bearer.<token>`; defaults to false

- `binary` — CLI binary or path
- `prefix_args` — args that always appear before prompt placement and runtime flags
- `prompt_mode` — `arg`, `flag`, or `stdin`
- `prompt_flag` — required when `prompt_mode: flag`
- `prompt_position` — `before_flags` or `after_flags`
- `flag_style` — `gnu_long` or `single_dash`
- `option_flags` — per-option override from `with` key to exact flag token

## ACP Agents

Install and authenticate an ACP adapter, then select `protocol: acp`:

```yaml
harnesses:
  agent:
    protocol: acp
    binary: codex-acp
steps:
  - action: harness.run
    with:
      provider: agent
      prompt: Review this project.
      acp_permission: deny
    output: REVIEW
```

Remote connections use the same step with a remote definition:

```yaml
harnesses:
  agent:
    protocol: acp
    transport: websocket            # or http for Streamable HTTP
    url: wss://acp.example.com/acp   # https://acp.example.com/acp for HTTP
    cwd: /srv/project
    headers:
      Authorization: Bearer ${ACP_TOKEN}
    # ca_file: /path/to/private-ca.pem
```

Remote definitions reject local startup fields and adapter CLI flags, and remote
steps inherit only `acp_permission` from DAG-level `harness:` defaults, also when
a variable selects the provider. URL, cwd,
CA path, and header values support workflow variables. Dagu warns when headers
go over `ws://` or `http://` to a non-loopback host. Relative CA paths resolve
from the local step working directory; remote cwd belongs to the agent host.
TLS certificate and hostname verification stay enabled. WebSocket uses control
Ping frames and offers `acp.v1`. Bearer tokens stay in the Authorization header
unless `bearer_subprotocol: true` is explicitly enabled for a compatible bridge;
that option rejects tokens containing characters such as `/` or `=`.
HTTP uses the ACP SDK's experimental Streamable HTTP profile, with connection and
session SSE streams; endpoints must implement that profile.

All transports capture assistant text as stdout and support ordered fallback.
`with.stdin` is appended to the prompt after a blank line. `with.acp_permission`
defaults to `deny`; `allow` selects only an offered `allow_once` permission and
never grants persistent approval. A DAG-level `acp_permission` applies only to
ACP steps; CLI providers never receive it as a flag. A denial selects the agent's reject option and
fails the attempt. Only `end_turn` succeeds. A graceful Stop sends
`session/cancel` and waits for the agent to end its turn; a forced Stop (after
`max_clean_up_time_sec`) or a timeout also closes the connection. Stop skips
fallback. Use `timeout_sec` to bound a turn. Client filesystem/terminal
callbacks, interactive authentication, session resume, and local adapters inside
containers are unsupported; remote steps ignore a DAG-level `container:`.

## DAG-Level Defaults and Fallback

Use top-level `harness:` to define shared defaults for every harness step in the DAG.

```yaml
harness:
  provider: claude
  model: sonnet
  fallback:
    - provider: codex
      full-auto: true
    - provider: copilot
      yolo: true
      silent: true

steps:
  - id: step1
    action: harness.run
    with:
      prompt: "Write tests"

  - id: step2
    action: harness.run
    with:
      prompt: "Fix bugs"
      model: opus
      effort: high

  - id: step3
    action: harness.run
    with:
      prompt: "Generate docs"
      provider: copilot
      fallback:
        - provider: claude
          model: haiku
```

Merge rules:

- DAG-level primary harness config is the base
- Step-level `with` overlays it
- Step-level `with.fallback` replaces DAG-level `fallback`
- Step-level `with.fallback: []` disables inherited fallback
- New DAGs should use `action: harness.run`. Legacy step-level `type: harness` remains loadable for backward compatibility.
- A top-level `harness:` config only supplies defaults. It does not set the type of a step that does not name one, so shell `run:` steps can sit alongside harness steps in the same DAG.
- A step that named its prompt with a bare `command:`, relying on the removed inference, is rejected while loading. Port it to `action: harness.run` with `with.prompt`, and `with.stdin` where it used `script:`.

## Containerized Harness Steps

`container:` is optional for `harness.run`. It can be defined at the DAG root or on a specific harness step.

Use root-level `container:` when all compatible steps should run in the same DAG-level container. Image-mode root containers create a shared container for the run; `container.exec` uses an existing container. A harness step without its own `container:` executes the provider CLI inside that shared container.

```yaml
container:
  image: my-codex-runner:latest
  pull_policy: always
  working_dir: /workspace
  volumes:
    - .:/workspace:rw

steps:
  - id: fix_tests
    action: harness.run
    with:
      provider: codex
      prompt: "Fix the failing tests in this repository"
      sandbox: workspace-write
      skip-git-repo-check: true
    timeout_sec: 600
```

Use step-level `container:` when only that harness step needs a container, or when it needs a different container from the DAG-level one. If both root-level and step-level containers are present, the step-level container is used for that step.

```yaml
steps:
  - id: fix_tests
    action: harness.run
    container:
      image: my-codex-runner:latest
      pull_policy: always
      working_dir: /workspace
      volumes:
        - .:/workspace:rw
    with:
      provider: codex
      prompt: "Fix the failing tests in this repository"
      sandbox: workspace-write
      skip-git-repo-check: true
    timeout_sec: 600
```

Container rules:

- The selected provider binary must exist inside the container that runs the step.
- Built-in provider adapters and custom providers with `prompt_mode: arg` or `prompt_mode: flag` can run in a container.
- `with.stdin` is not supported in a container. Step-level container plus `with.stdin` is rejected during DAG validation; root shared-container stdin is rejected when the harness step runs.
- Custom providers with `prompt_mode: stdin` are not supported in a container.
- A step-level image-mode container creates a container for the step. Dagu uses the provider binary as the container entrypoint and passes provider arguments as the command.
- Do not set `container.name` for image-mode harness steps. Use `container.exec` when the step must execute inside an existing container.
- For a root-level container, Dagu executes the full provider command inside the shared DAG-level container.
- A step-level container inherits user-defined runtime values such as DAG env, step env, params, secrets, and step outputs. The engine process environment is not injected.
- A root-level shared-container harness env filters host-path runtime values such as `PWD`, DAG run log paths, artifact paths, and step stream paths. Do not rely on those host paths inside the shared container.
- Step-level `container.env` overrides inherited values with the same key.
- DAG resource limits are applied to created step-level harness containers and created DAG-level containers when the container runtime supports those limits. Existing-container exec mode cannot change that container's host resources.
- Provider flags still belong under `with:`. For example, Codex `sandbox: workspace-write` configures Codex inside the outer container boundary.
- Docker or Podman is selected by the Dagu service process. This is not configured in the DAG YAML.

## Pattern 1: Checkout a Repository and Run a Harness

```yaml
name: review-repository
type: graph

params:
  - REPOSITORY: https://github.com/dagucloud/dagu.git
  - REF: main
  - PROMPT: "Review this repository and identify the highest-impact improvement."

steps:
  - id: checkout
    action: git.checkout
    with:
      repository: "${params.REPOSITORY}"
      ref: "${params.REF}"
      path: "${context.paths.work_dir}/repo"

  - id: review
    depends: [checkout]
    working_dir: "${context.paths.work_dir}/repo"
    action: harness.run
    with:
      provider: opencode
      model: openrouter/deepseek/deepseek-v4-flash
      prompt: "${params.PROMPT}"
    timeout_sec: 600
```

`git.checkout` clones the repository when the target is absent and refreshes an existing checkout otherwise. The per-run work directory gives concurrent DAG runs separate checkouts, keeps the workspace available for retries, and removes it when the DAG run is cleaned up.

## Pattern 2: Multiple Harness Steps

Chain harness steps that invoke external CLIs, passing output between steps via env-scope `output:` variables or `${step_id.stdout}` file references.

```yaml
type: graph

params:
  - topic: ""

steps:
  - id: research
    action: harness.run
    with:
      prompt: "Research every approach to: ${params.topic}. List all approaches with pros, cons, and when to use each."
      provider: claude
      model: sonnet
    output: RESEARCH

  - id: review
    action: harness.run
    with:
      prompt: "Review the research provided on stdin for completeness and gaps"
      # Interpolated before execution, then piped to the harness CLI on stdin.
      stdin: |
        Review this research for completeness and gaps:

        ${env.RESEARCH}
      provider: codex
      full-auto: true
      skip-git-repo-check: true
    depends: [research]
    output: REVIEW

  - id: refine
    action: harness.run
    with:
      prompt: "Refine this research incorporating the review feedback provided via stdin."
      stdin: |
        === Research ===
        ${env.RESEARCH}

        === Review Feedback ===
        ${env.REVIEW}
      provider: claude
      model: sonnet
    depends: [review]
    output: REFINED
```

`with.prompt` is the prompt. For host subprocess runs, built-in adapters handle `with.stdin` according to the provider table. Custom `arg`/`flag` harnesses receive it on stdin. For custom `stdin` harnesses, stdin receives the prompt, then a blank line, then `with.stdin` when both are present.

## Pattern 3: Parameterized

```yaml
params:
  - PROVIDER: claude
  - MODEL: sonnet
  - PROMPT: "Analyze this codebase"

steps:
  - id: agent
    action: harness.run
    with:
      prompt: "${params.PROMPT}"
      provider: "${params.PROVIDER}"
      model: "${params.MODEL}"
    output: RESULT
```

## Provider Examples

### Claude Code

```yaml
steps:
  - id: task
    action: harness.run
    with:
      prompt: "Write tests for the auth module"
      provider: claude
      model: sonnet
      effort: high
      max-turns: 20
      max-budget-usd: 2.00
      permission-mode: auto
      allowed-tools: "Bash,Read,Edit"
    timeout_sec: 300
    output: RESULT
```

### Codex

```yaml
steps:
  - id: task
    action: harness.run
    with:
      prompt: "Fix failing tests in src/"
      provider: codex
      full-auto: true
      sandbox: workspace-write
      ephemeral: true
      skip-git-repo-check: true
    timeout_sec: 300
```

### Copilot

```yaml
steps:
  - id: task
    action: harness.run
    with:
      prompt: "Refactor the authentication middleware"
      provider: copilot
      autopilot: true
      yolo: true
      silent: true
      no-ask-user: true
      no-auto-update: true
    timeout_sec: 300
```

### OpenCode

```yaml
steps:
  - id: task
    action: harness.run
    with:
      prompt: "Refactor the database layer"
      provider: opencode
      model: openrouter/deepseek/deepseek-v4-flash
    timeout_sec: 300
```

### Pi

```yaml
steps:
  - id: task
    action: harness.run
    with:
      prompt: "Design a rate limiting middleware"
      provider: pi
      thinking: high
      tools: read,bash
    timeout_sec: 300
```

### DeepSeek Harness

```yaml
steps:
  - id: task
    action: harness.run
    with:
      prompt: "Review the current branch and fix the failing tests"
      provider: deepseek
    timeout_sec: 300
```

### Kilo Code

```yaml
steps:
  - id: task
    action: harness.run
    with:
      prompt: "Review the current branch and fix the failing tests"
      provider: kilo
    timeout_sec: 300
```

Kilo runs as `kilo run "<prompt>" --auto` by default for unattended execution.

## Notes

1. **Model names** — Look up current model names from each provider's documentation. Do not rely on hardcoded names; they change frequently.
2. **Prompt as a parameter** — Expose the prompt via `params:` so users can customize from UI/CLI without editing the DAG.
3. **Timeouts** — Set `timeout_sec:` (300-600s+) on harness steps. External CLI providers can run for minutes.
4. **Retry on transient failures** — Add `retry_policy: { limit: 3, interval_sec: 30 }` to handle rate limits and network errors.
5. **Working directory** — Use `working_dir:` on the step. The CLI operates relative to this directory.
6. **Output capture** — Use string-form `output: VAR_NAME` for small flat values, declared `outputs:` for explicit `${steps.<step_id>.outputs.<name>}` values, object-form `output:` for structured `${step_id.output.*}` access, and `stdout.artifact` / `stderr.artifact` when large provider output, reports, JSON, Markdown, or logs should be stored as DAG-run artifacts. Use `${step_id.stdout}` only when a downstream step needs the stdout log file path.
7. **Exit codes** — `0` means success. Provider process or container failures keep the provider's exit code when available; setup and internal errors use `1`; timeouts use `124`. Explicit ACP Stop uses the signal exit code (normally `143`, or `137` when forced).
8. **Failure output** — Recent stderr is included in the error message on failure. When failed stdout exists, Dagu also includes a recent stdout tail and writes that tail to stderr.
9. **Fallback behavior** — If the primary harness config fails and the context is still active, fallback entries are tried in order. Failed-attempt stdout is not emitted to step stdout; its recent tail may be written to stderr and included in failure diagnostics. Stderr from failed attempts remains visible in logs.