Skip to content

Harness ​

Run coding agents as workflow steps. Harnesses run external CLI agents on the host or inside a container, or connect to agents over the Agent Client Protocol (ACP).

For CLI providers, the harness executor normally starts a subprocess, captures stdout and stderr, and uses the process exit status as the step result. Built-in OpenCode steps use a managed server session by default on long-lived Dagu execution hosts; see OpenCode.

The selected CLI attempt's binary must either be available in PATH or be referenced by path. Dagu resolves each CLI provider binary when that attempt runs, so a missing fallback binary does not fail a successful primary attempt. Agent CLIs that authenticate with an environment variable can also be installed per-DAG with tools; the managed toolset directory is prepended to PATH for the run. DAG-level tools and secrets supply CLI steps only, not the process-owned OpenCode managed service.

To run CLI agents inside a container sandbox, see Harness Sandboxed Execution. This is the recommended shape when an AI or coding agent should run with explicit filesystem mounts, toolchains, network mode, external egress controls, and credentials.

To run an agent until a check passes, have a second agent review its work, or let a person send the work back with feedback, see Agent Loops.

Supported Providers ​

Dagu has built-in support for the following providers. Each adapter is pre-configured with the provider's non-interactive invocation and supplementary-input behavior:

ProviderKeyBase invocationwith.stdin
Claude Codeclaudeclaude -p "<prompt>" [flags]Piped to stdin
Codexcodexcodex exec "<prompt>" [flags]Piped to stdin
GitHub Copilotcopilotcopilot -p "<prompt>" [flags]Piped to stdin
OpenCodeopencodeManaged session or opencode run "<prompt>" [flags]Piped to stdin on the CLI path
Pipipi -p "<prompt>" [flags]Piped to stdin
Gemini CLIgeminigemini -p "<prompt>" [flags]Piped to stdin
Cursorcursorcursor-agent -p "<prompt>" [flags]Folded into the prompt
Clineclinecline [flags] "<prompt>"Piped to stdin
Aideraideraider --message "<prompt>" [flags]Folded into the prompt
Qwen Codeqwenqwen -p "<prompt>" [flags]Piped to stdin
Goosegoosegoose run --text "<prompt>" [flags]Folded into the prompt
Kiro CLIkirokiro-cli chat --no-interactive "<prompt>" [flags]Piped to stdin
Droiddroiddroid exec "<prompt>" [flags]Folded into the prompt
Ampampamp -x "<prompt>" [flags]Piped to stdin
DeepSeek Harnessdeepseekdsh --profile headless [flags] "<prompt>"Folded into the prompt
Kilo Codekilokilo run "<prompt>" [flags incl. --auto]Piped to stdin

Codex enables skip_git_repo_check by default, Cursor defaults to output_format: text, Goose defaults to quiet: true, and Kilo Code defaults to auto: true so the agent runs unattended. Explicit values under with override these defaults. The deepseek adapter targets the official DeepSeek Harness dsh CLI in its headless profile; it does not select DeepSeek as another harness's model backend.

You can also define custom harness definitions for any CLI agent. The Hermes Agent guide is a complete custom-provider example. For agents that speak ACP, locally or on another host, see ACP Agents.

Step Contract ​

  • with.prompt is the prompt. Harness steps accept a single command string; command arrays are rejected.
  • with.stdin is optional extra stdin content. It is inline text, not a file path. The step-level stdin field, which names a file, applies to run steps and is rejected on harness steps.
  • After DAG-level defaults are applied, the step needs a provider. Omitted provider configuration is still invalid.
  • with.provider may be a built-in CLI provider or a name defined under top-level harnesses:.
  • with.provider may contain scoped references such as ${env.PROVIDER} and is resolved after interpolation at runtime.

Example:

yaml
steps:
  - name: review
    action: harness.run
    with:
      prompt: |
        Review the current branch and list problems
      provider: claude
      model: sonnet
      bare: true

Structured JSON Output ​

Use the step-level output_schema field when downstream steps require stdout to contain exactly one JSON object with a known shape:

yaml
steps:
  - id: analyze
    action: harness.run
    with:
      provider: codex
      prompt: |
        Analyze the authentication code.
        Return only one JSON object with `summary` and `risk` fields.
    output_schema:
      type: object
      additionalProperties: false
      required: [summary, risk]
      properties:
        summary:
          type: string
        risk:
          type: string
          enum: [low, medium, high]

After the successful harness attempt exits, Dagu decodes its complete stdout as one JSON value and validates it against the schema. The step fails if stdout is empty, contains non-JSON output or multiple JSON values, or does not match the schema. output_schema belongs on the step, not under with, and works with both built-in providers and custom harness definitions.

output_schema validates output; it does not instruct the agent to generate JSON. Tell the agent to return only JSON, or use an appropriate provider-native structured-output option. Provider options such as with.format: json, with.output_format: json, and with.json: true are passed through as CLI flags. They are not portable Dagu options, and a provider may use them to emit a JSONL event stream rather than one final JSON document.

See the validated JSON harness example for an end-to-end workflow and the output_schema workflow specification for the general step-level contract.

Repeating Until a Check Passes ​

A harness step with repeat_policy runs the agent again until a check passes. Each repeat starts a new agent process and resolves with again, so the prompt can count attempts with ${context.step.repeat_index} and read what the last check printed:

yaml
working_dir: /srv/repos/app

steps:
  - id: fix
    action: harness.run
    with:
      provider: codex
      sandbox: workspace-write
      prompt: |
        Make `make test` pass. Earlier attempts: ${context.step.repeat_index}.
        If ${context.paths.artifacts_dir}/test.log exists, read it first.
    repeat_policy:
      repeat: until
      condition: make test > "${context.paths.artifacts_dir}/test.log" 2>&1
      limit: 5

  - id: tests_pass
    depends: [fix]
    run: make test

The step stops repeating when the condition command exits 0, or after five attempts. Reaching the limit does not fail the step, so tests_pass runs the check once more. Inner Loops covers feedback files, protecting the check from the agent, attempts made of several steps, and reviewer agents.

Push-back ​

Harness steps automatically receive push-back context from a human task or an approval when they are rewound and re-executed. Dagu appends a push-back context block to the prompt with:

  • the current push-back iteration
  • reviewer-provided feedback inputs, such as feedback
  • the previous stdout log path, when the step had stdout before reset

A harness step that first runs after a push-back has no earlier work to revise, so its prompt stays as written. The feedback is still in its environment, and you can hand it to the agent by referencing it in the prompt, such as ${feedback}.

Dagu passes the previous stdout as a file path only. It does not inline stdout into the prompt because harness output can be large. The path is also available as DAG_PUSHBACK_PREVIOUS_STDOUT_FILE; the current iteration is available as DAG_PUSHBACK_ITERATION.

yaml
steps:
  - id: implement
    action: harness.run
    with:
      prompt: |
        Implement the requested change and summarize what changed
      provider: codex

  - 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

On the first run, implement receives only the original prompt. If the reviewer pushes back with feedback, Dagu reruns the harness step and augments the prompt with the feedback, iteration number, and previous stdout log path. A harness step that also has a repeat_policy gets the block on every repeat. Outer Loops builds code review, plan approval, and hand-over to a person on this.

For built-in CLI providers:

  • the prompt is always passed on the command line
  • additional with keys become CLI flags, with snake_case keys normalized to kebab-case
  • with.stdin, if present, is piped or folded into the prompt as shown in the provider table

Agent Configuration on the Host ​

A built-in CLI provider runs under the account that runs Dagu, so it loads that account's configuration: instructions such as a global CLAUDE.md or AGENTS.md, hooks, plugins, and MCP servers. A personal rule like "keep diffs minimal" steers the workflow's agents too, and the same workflow can behave differently on another machine.

For repeatable runs, run Dagu under a dedicated account whose agent configuration you control. To narrow what a step loads:

ProviderSettingWhat it does
Claude Codesetting_sources: project,local under withLeaves out the account's user settings
CodexCODEX_HOME in the step's envReads configuration, and any stored login (auth.json), from that directory
yaml
steps:
  - id: review
    action: harness.run
    with:
      provider: claude
      setting_sources: project,local
      prompt: Review the change.

  - id: fix
    action: harness.run
    env:
      - CODEX_HOME: /srv/dagu/codex-home
    with:
      provider: codex
      prompt: Fix what the review found.

Claude Code's bare flag also skips CLAUDE.md and hooks, but it authenticates only with ANTHROPIC_API_KEY or an apiKeyHelper, and never reads a subscription login. On a subscription, a step using it fails or moves to API billing.

Custom Harness Definitions ​

Use top-level harnesses: to define named custom harness adapters.

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

steps:
  - name: summarize
    action: harness.run
    with:
      prompt: |
        Summarize the repository status
      provider: gemini-custom
      model: gemini-2.5-pro

Custom harness definition fields:

FieldTypeRequiredDefaultMeaning
protocolcli | acpnocliCLI invocation, or an ACP connection
binarystringyes, except for remote ACP-CLI binary name or path
prefix_argsstring[]no[]Arguments emitted before prompt placement and generated flags
prompt_modearg | flag | stdinnoargHow the prompt is passed
prompt_flagstringonly for flag mode-Exact flag token used for the prompt
prompt_positionbefore_flags | after_flagsnobefore_flagsWhere prompt tokens go relative to generated flags
flag_stylegnu_long | single_dashnognu_longDefault generated flag token style
option_flagsobjectno-Exact flag token overrides per with key

Rules enforced by Dagu:

  • a non-null custom definition shadows a built-in provider with the same name; removing that definition exposes the built-in again
  • prompt_flag is valid only when prompt_mode: flag
  • unknown keys inside a harness definition are rejected
  • ACP definitions add transport and connection fields; see ACP Agents

Custom Prompt Placement ​

prompt_mode: arg

yaml
harnesses:
  reviewbot:
    binary: my-review-agent
    prefix_args: ["exec"]
    prompt_mode: arg
    prompt_position: after_flags
    flag_style: single_dash

steps:
  - action: harness.run
    with:
      prompt: |
        Review the auth module
      provider: reviewbot
      model: sonnet

Generated argv:

text
my-review-agent exec -model sonnet "Review the auth module"

prompt_mode: flag

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

steps:
  - action: harness.run
    with:
      prompt: |
        Review the auth module
      provider: gemini-custom
      model: gemini-2.5-pro

Generated argv:

text
gemini --prompt "Review the auth module" --model gemini-2.5-pro

prompt_mode: stdin

yaml
harnesses:
  llm:
    binary: my-agent
    prefix_args: ["exec"]
    prompt_mode: stdin

steps:
  - action: harness.run
    with:
      prompt: |
        Review this patch
      stdin: |
        diff --git a/main.go b/main.go
        ...
      provider: llm
      format: json

Generated argv:

text
my-agent exec --format json

Stdin content:

text
Review this patch

diff --git a/main.go b/main.go
...

For stdin mode:

  • if with.stdin is empty, stdin is just the prompt
  • if both with.prompt and with.stdin are present, stdin is prompt + "\n\n" + stdin

with-to-Flag Mapping ​

After removing reserved keys, Dagu converts remaining with entries to CLI flags.

YAML valueResult
key: "value"flag value
key: truebare flag
key: falseomitted
key: ""omitted
key: 20flag 20
key: 5.5flag 5.5
key: [a, b]flag a flag b

Flag token selection:

  • built-in CLI providers use --key
  • custom definitions with flag_style: gnu_long use --key
  • custom definitions with flag_style: single_dash use -key
  • option_flags.<key> overrides the exact token for that key

Additional details:

  • built-in CLI providers normalize snake_case keys to kebab-case flag names, so max_turns becomes --max-turns
  • custom harness definitions keep keys as written unless option_flags overrides them
  • keys are emitted in lexicographic order for deterministic argv generation
  • reserved keys are prompt, stdin, provider, fallback, managed, and acp_permission
  • Dagu does not validate provider-specific flag names or values

DAG-Level Defaults ​

Top-level harness: provides defaults for harness steps.

yaml
harness:
  provider: claude
  model: sonnet
  bare: true
  fallback:
    - provider: codex
      full-auto: true

steps:
  - action: harness.run
    with:
      prompt: |
        Write tests for the auth module

  - action: harness.run
    with:
      prompt: |
        Fix the flaky integration tests
      model: opus

Merge rules:

  • DAG-level harness: is the base config for every harness step
  • step-level with: overrides primary keys from DAG-level harness:
  • DAG-level options pass to whichever provider the step selects; a step that sets another provider still receives keys such as model and bare, so keep provider-specific flags on the steps or fallback entries that need them
  • step-level with.fallback replaces the DAG-level fallback list; it is not merged
  • remote ACP steps inherit only provider and acp_permission, because a remote agent takes no CLI flags

Fallbacks ​

with.fallback is an ordered list of alternative provider configs.

yaml
steps:
  - name: implement
    action: harness.run
    with:
      prompt: |
        Implement the feature and add tests
      provider: claude
      fallback:
        - provider: codex
        - provider: gemini
          model: gemini-2.5-pro

Behavior:

  • Dagu tries the primary provider first, then each fallback in order
  • fallback entries are flat provider configs; nested fallback blocks are not supported
  • if the step context is cancelled, remaining fallbacks are skipped
  • stdout from failed attempts is discarded
  • stderr from every attempt remains in the step logs

Parameterized Provider Selection ​

yaml
params:
  - PROVIDER: claude

steps:
  - name: task
    action: harness.run
    with:
      prompt: |
        Analyze the repository layout
      provider: "${params.PROVIDER}"

Interpolated scalar strings are normalized before flags are generated, so values such as "true", "10", and "5.5" become booleans or numbers.

Model Usage ​

A managed OpenCode step adds what its session's models used to the step's model usage. CLI providers and ACP agents do not report usage, so the step records the provider in notReported instead.

Exit Codes ​

Exit codeMeaning
0CLI completed successfully
124Step context was cancelled or timed out
any other non-zero valueThe child process exit code, or 1 when setup failed before a process exit code existed

An explicit stop of an ACP step reports the signal exit code, normally 143, or 137 when forced.

On failure, Dagu includes the last 1024 bytes of stderr and the last 1024 bytes of stdout in the returned error message. The stdout tail is also written to the step's stderr log.

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