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.
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/packageTyped values move between steps
Validate parameters at the boundary and publish structured values for downstream steps.
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}'Run on schedule and recover
Turn a command into an operated job with one cron expression and shared reliability defaults.
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-indexRun a step in Docker or Podman
Give one step its exact runtime without containerizing the Dagu server or the rest of the workflow.
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.txtSet DAGU_CONTAINER_RUNTIME=podman to use a Docker-compatible Podman socket.
Call an LLM
Use a model as an ordinary step, capture its answer, and pass it into the rest of the graph.
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.mdRun a coding agent
Run OpenCode, Codex, Claude Code, Copilot, or Pi as a durable workflow step with retries, logs, and artifacts.
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.mdThe pinned tool is installed for the run, so the worker does not need OpenCode preinstalled.
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.
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.mdhuman.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.
Compose reusable workflows
A child DAG is a real nested run with its own graph, logs, and status. Inputs cross the boundary explicitly.
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.

