Skip to content

Workflow building blocks

Recipes

Each recipe below is a complete, copyable workflow that combines Dagu's building blocks.

Commands become a graph

Start with existing scripts. Independent steps run together; a dependent step waits for both.

yaml
type: graph
steps:
  - id: checkout
    run: git checkout main

  - id: test
    depends: [checkout]
    run: ./scripts/test

  - id: build
    depends: [checkout]
    run: ./scripts/build

  - id: package
    depends: [test, build]
    run: ./scripts/package

Dependencies and parallel execution →

Typed values move between steps

Validate parameters at the boundary and publish structured values for downstream steps.

yaml
params:
  - name: ENVIRONMENT
    type: string
    enum: [staging, production]
    required: true
    default: staging

consts:
  - service: payments

env:
  - DEPLOY_TARGET: "${consts.service}-${params.ENVIRONMENT}"

steps:
  - id: release
    output:
      version: v2.5.0
      target: ${env.DEPLOY_TARGET}

  - id: deploy
    depends: [release]
    run: ./deploy --version '${release.output.version}' --target '${release.output.target}'

Parameters, variables, and data flow →

Run on schedule and recover

Turn a command into an operated job with one cron expression and shared reliability defaults.

yaml
schedule: "CRON_TZ=America/New_York 0 9 * * 1-5"
catchup_window: 4h

defaults:
  retry_policy:
    limit: 2
    interval_sec: 30
  timeout_sec: 900

steps:
  - id: sync_customers
    run: ./sync-customers --incremental
  - id: rebuild_search
    depends: [sync_customers]
    run: ./rebuild-search-index

Schedules and catch-up behavior →

Run a step in Docker or Podman

Give one step its exact runtime without containerizing the Dagu server or the rest of the workflow.

yaml
steps:
  - id: test_in_container
    action: docker.run
    with:
      image: python:3.13-slim
      pull: missing
      auto_remove: true
      working_dir: /workspace
      volumes:
        - .:/workspace:ro
      command: python -m pytest -q
    stdout:
      artifact: test-results.txt

Set DAGU_CONTAINER_RUNTIME=podman to use a Docker-compatible Podman socket.

Container execution options →

Call an LLM

Use a model as an ordinary step, capture its answer, and pass it into the rest of the graph.

yaml
secrets:
  - name: OPENROUTER_API_KEY
    provider: env
    key: OPENROUTER_API_KEY

llm:
  provider: openrouter
  model: deepseek/deepseek-v4-flash

steps:
  - id: summarize
    action: chat.completion
    with:
      prompt: Summarize today's incident log in five bullets.
    output:
      summary:
        from: stdout

  - id: save_summary
    depends: [summarize]
    action: template.render
    with:
      template: "# Incident summary\n\n{{ .summary }}\n"
      data:
        summary: ${summarize.output.summary}
    stdout:
      artifact: incident-summary.md

Chat completions and model configuration →

Run a coding agent

Run OpenCode, Codex, Claude Code, Copilot, or Pi as a durable workflow step with retries, logs, and artifacts.

yaml
working_dir: .

tools:
  - anomalyco/opencode@v1.18.11

secrets:
  - name: OPENROUTER_API_KEY
    provider: env
    key: OPENROUTER_API_KEY

steps:
  - id: review
    action: harness.run
    with:
      provider: opencode
      model: openrouter/deepseek/deepseek-v4-flash
      prompt: |
        Review the most recent commit without modifying files.
        Return short Markdown with a summary, risks, and verdict.
    retry_policy:
      limit: 2
      interval_sec: 30
    stdout:
      artifact: ai/repository-review.md

The pinned tool is installed for the run, so the worker does not need OpenCode preinstalled.

Coding-agent providers and sandboxing →

Collect human input and create an artifact

Dagu generates a typed form, persists the answer, resumes the same run, and renders a result visible in the Web UI.

yaml
params:
  - name: RELEASE
    default: v1.4.0

steps:
  - id: handoff
    action: human.task
    with:
      prompt: Complete the handoff for ${params.RELEASE}
      form:
        type: object
        additionalProperties: false
        properties:
          environment:
            type: string
            enum: [staging, production]
          change_ticket:
            type: string
            pattern: '^CHG-[0-9]+$'
        required: [environment, change_ticket]

  - id: report
    depends: [handoff]
    action: template.render
    with:
      template: |
        # Release {{ .release }}
        Environment: {{ .environment }}
        Change: {{ .ticket }}
      data:
        release: ${params.RELEASE}
        environment: ${steps.handoff.outputs.environment}
        ticket: ${steps.handoff.outputs.change_ticket}
    stdout:
      artifact: release-handoff.md

human.task is a processless root-DAG step. It collects input and completes; use an approval gate when reviewers must approve, reject, or push work back.

Human tasks and generated forms →

Compose reusable workflows

A child DAG is a real nested run with its own graph, logs, and status. Inputs cross the boundary explicitly.

yaml
type: graph
steps:
  - id: tests
    action: dag.run
    with:
      dag: test-suite
      params:
        PACKAGE: ./internal/runtime

  - id: publish
    depends: [tests]
    run: ./publish-results
---
name: test-suite
type: graph
params:
  - name: PACKAGE
    type: string
    required: true
steps:
  - id: unit
    run: go test '${params.PACKAGE}'
  - id: race
    run: go test -race '${params.PACKAGE}'

Child DAGs do not inherit the parent's environment. Pass every required value through with.params.

Nested runs and sub-DAG outputs →

Next steps

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