Step Types Reference
Overview
Step types extend Dagu's capabilities beyond simple shell commands. Available step types:
- Shell (default) - Execute shell commands
- Agent - Run an AI agent as a workflow step (multi-turn tool-calling loop)
- SSH - Execute commands on remote hosts
- Docker - Run commands in Docker containers
- HTTP - Make HTTP requests
- Router - Route execution to different steps based on pattern matching
- Mail - Send emails
- Chat - Execute LLM requests (OpenAI, Anthropic, Gemini, etc.)
- JQ - Process JSON data
- S3 - S3 operations (upload, download, list, delete)
- Redis - Execute Redis commands and operations
- Archive - Extract, create, and list archive files
- GitHub Actions (experimental) - Run marketplace actions locally with nektos/act
TIP
For detailed documentation on each step type, click the links above to visit the feature pages.
Shell (Default)
INFO
For detailed Shell step type documentation, see Shell Guide.
The default step type runs commands in the system shell. Set a DAG-level shell to pick the program and flags once; steps inherit it unless you override them.
shell: ["/bin/bash", "-e"] # Default shell for the workflow
steps:
- command: echo "Hello World"
- command: echo $BASH_VERSION # Uses DAG shell
- shell: /usr/bin/zsh # Step-level override
command: echo "Uses zsh"Shell Selection
steps:
- name: default-shell
command: echo "Uses DAG shell or system default"
- name: bash-specific
shell: ["bash", "-e", "-u"] # Array form for flags
command: echo "Uses bash features"
- name: custom-shell
shell: /usr/bin/zsh
command: echo "Uses zsh"Agent
INFO
For detailed Agent step documentation, see Agent Step Guide.
Run an AI agent as a workflow step. The agent executes a multi-turn tool-calling loop — it reads files, runs commands, edits code, and searches the web to accomplish the task described in messages.
Agent settings (models, API keys, tool policies, skills, souls) are configured via the Web UI at /settings/agent (requires admin role), not in DAG YAML files. Steps reference models by ID and inherit global policies.
Minimal
steps:
- type: agent
messages:
- role: user
content: "Summarize the README.md in this repository"
output: SUMMARYWith Model Override and Restricted Tools
steps:
- type: agent
agent:
model: claude-opus
tools:
enabled:
- read
- think
safe_mode: false
messages:
- role: user
content: "Analyze the architecture of this codebase without modifying anything"
output: ANALYSISWith Bash Policy
steps:
- type: agent
agent:
tools:
bash_policy:
default_behavior: deny
deny_behavior: block
rules:
- name: allow-status-commands
pattern: "^(kubectl get|kubectl describe|helm status)\\b"
action: allow
prompt: |
Check the deployment status of the staging environment.
Only use read-only kubectl and helm commands.
max_iterations: 20
messages:
- role: user
content: "Report the health of all pods in the staging namespace"
output: HEALTH_REPORTWith Web Search, Memory, Skills, and Soul
steps:
- type: agent
agent:
model: claude-sonnet
web_search:
enabled: true
max_uses: 5
memory:
enabled: true
skills:
- code-review
- testing
soul: tsumugi
messages:
- role: user
content: "Research best practices for database migration and apply them to our schema"
output: MIGRATION_PLANPipeline with Output Capture
params:
- REPO_PATH
steps:
- type: agent
messages:
- role: user
content: "Analyze the test coverage of ${REPO_PATH} and identify untested code paths"
output: COVERAGE_ANALYSIS
- type: agent
agent:
model: claude-opus
max_iterations: 100
messages:
- role: user
content: |
Based on this analysis:
${COVERAGE_ANALYSIS}
Write unit tests for the untested code paths in ${REPO_PATH}.
output: TEST_RESULTSSH
INFO
For detailed SSH step type documentation, see SSH Guide.
Execute commands on remote hosts over SSH.
Basic SSH
steps:
- name: remote-command
type: ssh
config:
user: deploy
host: server.example.com
port: 22
key: /home/user/.ssh/id_rsa
command: ls -la /var/wwwWith Environment
steps:
- name: remote-with-env
type: ssh
config:
user: deploy
host: 192.168.1.100
key: ~/.ssh/deploy_key
command: |
export APP_ENV=production
cd /opt/app
echo "Deploying"Multiple Commands
steps:
- name: remote-script
type: ssh
config:
user: admin
host: backup.server.com
key: ${SSH_KEY_PATH}
script: |
#!/bin/bash
set -e
echo "Starting backup..."
tar -czf /backup/app-$(date +%Y%m%d).tar.gz /var/www
echo "Cleaning old backups..."
find /backup -name "app-*.tar.gz" -mtime +7 -delete
echo "Backup complete"Docker
INFO
For detailed Docker step type documentation, see Docker Guide.
Run commands in Docker containers for isolation and reproducibility. The container field supports two modes:
- Image mode: Create a new container from a Docker image
- Exec mode: Execute commands in an already-running container
Image Mode (Create New Container)
Use the container field to run a step in its own container:
steps:
- name: run-in-container
container:
image: alpine:latest
command: echo "Hello from container"TIP
The container is automatically removed after execution. Set keep_container: true to preserve it.
Exec Mode (Use Existing Container)
Execute commands in an already-running container:
steps:
# String form - exec with container's defaults
- name: run-migration
container: my-app-container
command: php artisan migrate
# Object form with overrides
- name: admin-task
container:
exec: my-app-container
user: root
working_dir: /app
command: chown -R app:app /dataExec mode is ideal for running commands in containers started by Docker Compose or other orchestration tools.
Image Pull Options
steps:
- name: pull-always
container:
image: myapp:latest
pull_policy: always # Always pull from registry
command: ./app
- name: pull-if-missing
container:
image: myapp:latest
pull_policy: missing # Default - pull only if not local
command: ./app
- name: never-pull
container:
image: local-image:dev
pull_policy: never # Use local image only
command: ./testRegistry Authentication
# Configure authentication for private registries
registry_auths:
docker.io:
username: ${DOCKER_USERNAME}
password: ${DOCKER_PASSWORD}
ghcr.io:
username: ${GITHUB_USER}
password: ${GITHUB_TOKEN}
steps:
- name: use-private-image
container:
image: ghcr.io/myorg/private-app:latest
command: echo "Running"Authentication can also be configured via DOCKER_AUTH_CONFIG environment variable.
Volume Mounts
steps:
- name: with-volumes
container:
image: python:3.13
volumes:
- /host/data:/container/data:ro # Read-only
- /host/output:/container/output:rw # Read-write
- ./config:/app/config # Relative path
command: python process.py /container/dataEnvironment Variables
env:
- API_KEY: secret123
steps:
- name: with-env
container:
image: node:22
env:
- NODE_ENV=production
- API_KEY=${API_KEY} # Pass from DAG env
- DB_HOST=postgres
command: npm startNetwork Configuration
steps:
- name: custom-network
container:
image: alpine
network: my-network
command: ping other-servicePlatform Selection
steps:
- name: specific-platform
container:
image: myapp:latest
platform: linux/amd64 # Force platform
command: ./appWorking Directory
steps:
- name: custom-workdir
container:
image: python:3.13
working_dir: /app
env:
- PYTHONPATH=/app
volumes:
- ./src:/app
command: python main.pyComplete Docker Example
steps:
- name: run-postgres
container:
name: test-db
image: postgres:17
pull_policy: missing
platform: linux/amd64
keep_container: true
env:
- POSTGRES_USER=test
- POSTGRES_PASSWORD=test
- POSTGRES_DB=testdb
volumes:
- postgres-data:/var/lib/postgresql/data
ports:
- "127.0.0.1:5432:5432"
network: bridge
command: postgresHTTP
INFO
For detailed HTTP step type documentation, see HTTP Guide.
Make HTTP requests to APIs and web services.
GET Request
steps:
- name: simple-get
type: http
config:
silent: true # Output body only
command: GET https://api.example.com/statusPOST with Body
steps:
- name: post-json
type: http
config:
headers:
Content-Type: application/json
Authorization: Bearer ${API_TOKEN}
body: |
{
"name": "test",
"value": 123
}
timeout: 30
command: POST https://api.example.com/dataQuery Parameters
steps:
- name: search-api
type: http
config:
query:
q: "dagu workflow"
limit: "10"
offset: "0"
silent: true
command: GET https://api.example.com/searchForm Data
steps:
- name: form-submit
type: http
config:
headers:
Content-Type: application/x-www-form-urlencoded
body: "username=user&password=pass&remember=true"
command: POST https://example.com/loginSelf-Signed Certificates
steps:
- name: internal-api
type: http
config:
tls_skip_verify: true # Skip certificate verification
headers:
Authorization: Bearer ${INTERNAL_TOKEN}
command: GET https://internal-api.local/dataComplete HTTP Example
steps:
- name: api-workflow
type: http
config:
headers:
Accept: application/json
X-API-Key: ${API_KEY}
timeout: 60
silent: false
command: GET https://api.example.com/data
output: API_RESPONSE
- name: process-response
command: echo "${API_RESPONSE}" | jq '.data[]'Router
INFO
For detailed Router step type documentation, see Router Guide.
Route execution to different steps based on pattern matching against a value.
WARNING
Router steps require type: graph at the DAG level.
Basic Usage
type: graph
env:
- INPUT: exact_value
steps:
- name: router
type: router
value: ${INPUT}
routes:
"exact_value": [route_a]
"other": [route_b]
- name: route_a
command: echo "Route A executed"
- name: route_b
command: echo "Route B executed"Regex Patterns
Prefix patterns with re: for regex matching:
type: graph
steps:
- name: router
type: router
value: ${INPUT}
routes:
"re:^apple.*": [apple_handler]
"re:^banana.*": [banana_handler]
"re:.*": [default_handler]Multiple Targets
Route to multiple steps from a single pattern:
type: graph
steps:
- name: router
type: router
value: ${TRIGGER}
routes:
"all": [step_a, step_b, step_c]DAG (Subworkflow)
INFO
The DAG step type allows running other workflows as steps. See Nested Workflows.
Execute other workflows as steps, enabling workflow composition.
Execute External DAG
steps:
- name: run-etl
type: dag
command: workflows/etl-pipeline.yaml
params: "DATE=${TODAY} ENV=production"Execute Local DAG
name: main-workflow
steps:
- name: prepare-data
type: dag
command: data-prep
params: "SOURCE=/data/raw"
---
name: data-prep
params:
- SOURCE: /tmp
steps:
- name: validate
command: validate.sh ${SOURCE}
- name: clean
command: clean.py ${SOURCE}Capture DAG Output
steps:
- name: analyze
type: dag
command: analyzer.yaml
params: "FILE=${INPUT_FILE}"
output: ANALYSIS
- name: use-results
command: |
echo "Status: ${ANALYSIS.outputs.status}"
echo "Count: ${ANALYSIS.outputs.record_count}"Error Handling
steps:
- name: may-fail
type: dag
command: risky-process.yaml
continue_on:
failure: true
retry_policy:
limit: 3
interval_sec: 300Dynamic DAG Selection
steps:
- name: choose-workflow
command: |
if [ "${ENVIRONMENT}" = "prod" ]; then
echo "production-workflow.yaml"
else
echo "staging-workflow.yaml"
fi
output: WORKFLOW_FILE
- name: run-selected
type: dag
command: ${WORKFLOW_FILE}
params: "ENV=${ENVIRONMENT}"Mail
INFO
For detailed Mail step type documentation, see Mail Guide.
Send emails for notifications and alerts.
Basic Email
smtp:
host: smtp.gmail.com
port: "587"
username: sender@gmail.com
password: ${SMTP_PASSWORD}
steps:
- name: send-notification
type: mail
config:
to: recipient@example.com
from: sender@gmail.com
subject: "Workflow Completed"
message: "The data processing workflow has completed successfully."With Attachments
steps:
- name: send-report
type: mail
config:
to: team@company.com
from: reports@company.com
subject: "Daily Report - ${TODAY}"
message: |
Please find attached the daily report.
Generated at: ${TIMESTAMP}
attachments:
- /tmp/daily-report.pdf
- /tmp/summary.csvMultiple Recipients
steps:
- name: alert-team
type: mail
config:
to:
- ops@company.com
- alerts@company.com
- oncall@company.com
from: dagu@company.com
subject: "[ALERT] Process Failed"
message: |
The critical process has failed.
Error: ${ERROR_MESSAGE}
Time: ${TIMESTAMP}HTML Email
steps:
- name: send-html
type: mail
config:
to: marketing@company.com
from: notifications@company.com
subject: "Weekly Stats"
contentType: text/html
message: |
<html>
<body>
<h2>Weekly Statistics</h2>
<p>Users: <strong>${USER_COUNT}</strong></p>
<p>Revenue: <strong>${REVENUE}</strong></p>
</body>
</html>Chat
INFO
For detailed Chat step type documentation, see Chat Guide.
Execute requests to Large Language Model providers.
Basic Chat Request
steps:
- type: chat
llm:
provider: openai
model: gpt-4o
messages:
- role: user
content: "What is 2+2?"
output: ANSWERSupported Providers
| Provider | Environment Variable |
|---|---|
openai | OPENAI_API_KEY |
anthropic | ANTHROPIC_API_KEY |
gemini | GOOGLE_API_KEY |
openrouter | OPENROUTER_API_KEY |
local | (none) |
Aliases ollama, vllm, and llama map to local.
Multi-turn Session
type: graph
steps:
- name: setup
type: chat
llm:
provider: openai
model: gpt-4o
system: "You are a helpful assistant."
messages:
- role: user
content: "What is 2+2?"
- name: followup
depends: [setup]
type: chat
llm:
provider: openai
model: gpt-4o
messages:
- role: user
content: "Now multiply that by 3."Steps inherit session history from dependencies.
Variable Substitution
params:
- TOPIC: "quantum computing"
steps:
- type: chat
llm:
provider: anthropic
model: claude-sonnet-4-20250514
messages:
- role: user
content: "Explain ${TOPIC} briefly."Local Models (Ollama)
steps:
- type: chat
llm:
provider: local
model: llama3
messages:
- role: user
content: "Hello!"DAG-Level Configuration
Define LLM defaults at the DAG level to share configuration across steps:
llm:
provider: openai
model: gpt-4o
system: "You are a helpful assistant."
temperature: 0.7
steps:
- type: chat
messages:
- role: user
content: "First question"
- type: chat
llm:
provider: anthropic
model: claude-sonnet-4-20250514
messages:
- role: user
content: "Override with different provider"When a step specifies llm:, it completely replaces DAG-level config (no field merging).
JQ
INFO
For detailed JQ step type documentation, see JQ Guide.
Process and transform JSON data using jq syntax.
Raw Output
Set config.raw: true to mirror jq's -r flag and emit unquoted primitives.
steps:
- name: list-emails
type: jq
config:
raw: true
command: '.data.users[].email'
script: |
{
"data": {
"users": [
{"email": "user1@example.com"},
{"email": "user2@example.com"}
]
}
}Output:
user1@example.com
user2@example.comFormat JSON
steps:
- name: pretty-print
type: jq
script: |
{"name":"test","values":[1,2,3],"nested":{"key":"value"}}Output:
{
"name": "test",
"values": [1, 2, 3],
"nested": {
"key": "value"
}
}Query JSON
steps:
- name: extract-value
type: jq
command: '.data.users[] | select(.active == true) | .email'
script: |
{
"data": {
"users": [
{"id": 1, "email": "user1@example.com", "active": true},
{"id": 2, "email": "user2@example.com", "active": false},
{"id": 3, "email": "user3@example.com", "active": true}
]
}
}Output:
"user1@example.com"
"user3@example.com"Transform JSON
steps:
- name: transform-data
type: jq
command: '{id: .id, name: .name, total: (.items | map(.price) | add)}'
script: |
{
"id": "order-123",
"name": "Test Order",
"items": [
{"name": "Item 1", "price": 10.99},
{"name": "Item 2", "price": 25.50},
{"name": "Item 3", "price": 5.00}
]
}Output:
{
"id": "order-123",
"name": "Test Order",
"total": 41.49
}Complex Processing
steps:
- name: analyze-logs
type: jq
command: |
group_by(.level) |
map({
level: .[0].level,
count: length,
messages: map(.message)
})
script: |
[
{"level": "ERROR", "message": "Connection failed"},
{"level": "INFO", "message": "Process started"},
{"level": "ERROR", "message": "Timeout occurred"},
{"level": "INFO", "message": "Process completed"}
]S3
INFO
For detailed S3 step type documentation, see S3 Guide.
Execute S3 operations including upload, download, list, and delete. Supports AWS S3 and S3-compatible services (MinIO, GCS, DigitalOcean Spaces).
DAG-Level Configuration
s3:
region: us-east-1
access_key_id: ${AWS_ACCESS_KEY_ID}
secret_access_key: ${AWS_SECRET_ACCESS_KEY}
bucket: my-bucket
steps:
- name: upload-file
type: s3
config:
key: data/file.txt
source: /tmp/file.txt
command: uploadUpload
steps:
- name: upload-report
type: s3
config:
bucket: my-bucket
key: reports/daily.csv
source: /tmp/report.csv
content_type: text/csv
storage_class: STANDARD_IA
command: uploadDownload
steps:
- name: download-config
type: s3
config:
bucket: my-bucket
key: config/settings.json
destination: /tmp/settings.json
command: downloadList Objects
steps:
- name: list-logs
type: s3
config:
bucket: my-bucket
prefix: logs/2024/
max_keys: 100
recursive: true
command: list
output: OBJECTSDelete
steps:
# Single object
- name: delete-file
type: s3
config:
bucket: my-bucket
key: temp/old-file.txt
command: delete
# Batch delete by prefix
- name: cleanup
type: s3
config:
bucket: my-bucket
prefix: logs/2023/
command: deleteS3-Compatible Services
# MinIO
s3:
endpoint: http://localhost:9000
access_key_id: minioadmin
secret_access_key: minioadmin
bucket: my-bucket
force_path_style: true
# Google Cloud Storage
s3:
endpoint: https://storage.googleapis.com
access_key_id: ${GCS_HMAC_KEY}
secret_access_key: ${GCS_HMAC_SECRET}
bucket: my-gcs-bucketRedis
INFO
For detailed Redis step type documentation, see Redis Guide.
Execute commands against Redis servers.
Basic Usage
steps:
- name: ping
type: redis
config:
host: localhost
port: 6379
command: PINGDAG-Level Configuration
Define connection defaults at the DAG level:
redis:
host: localhost
port: 6379
password: ${REDIS_PASSWORD}
steps:
- name: set-value
type: redis
config:
command: SET
key: mykey
value: "hello"
- name: get-value
type: redis
config:
command: GET
key: mykey
output: RESULTString Operations
steps:
- name: cache-user
type: redis
config:
command: SET
key: user:${USER_ID}
value: '{"name": "John", "email": "john@example.com"}'
- name: get-user
type: redis
config:
command: GET
key: user:${USER_ID}
output: USER_DATAHash Operations
steps:
- name: set-user-field
type: redis
config:
command: HSET
key: user:1
field: email
value: "john@example.com"
- name: get-all-fields
type: redis
config:
command: HGETALL
key: user:1
output: USER_HASHPipeline Operations
steps:
- name: batch-ops
type: redis
config:
pipeline:
- command: SET
key: key1
value: "value1"
- command: SET
key: key2
value: "value2"
- command: MGET
keys: ["key1", "key2"]Connection Modes
# Standalone (default)
redis:
host: localhost
port: 6379
# Sentinel
redis:
mode: sentinel
sentinel_master: mymaster
sentinel_addrs:
- sentinel1:26379
- sentinel2:26379
# Cluster
redis:
mode: cluster
cluster_addrs:
- node1:6379
- node2:6379Archive
INFO
For detailed Archive step type documentation, see Archive Guide.
Manipulate archives without shelling out to tar, zip, or other external tools.
Extract Archive
steps:
- name: unpack
type: archive
config:
source: logs.tar.gz
destination: ./logs
verify_integrity: true
command: extractCreate Archive
steps:
- name: package
type: archive
config:
source: ./logs
destination: logs-backup.tar.gz
include:
- "**/*.log"
command: createList Contents
steps:
- name: inspect
type: archive
config:
source: logs-backup.tar.gz
command: list
output: ARCHIVE_INDEXGitHub Actions
INFO
For the full guide, see GitHub Actions.
Run marketplace actions (e.g. actions/checkout@v4) inside Dagu steps.
secrets:
- name: GITHUB_TOKEN
provider: env
key: GITHUB_TOKEN
steps:
- name: checkout
command: actions/checkout@v4
type: gha # Aliases: github_action, github-action
config:
runner: node:24-bookworm
params:
repository: dagu-org/dagu
ref: main
token: "${GITHUB_TOKEN}"WARNING
This executor is experimental. It depends on Docker, downloads images on demand, and currently supports single-action invocations per step.
See Also
- Shell - Shell command execution details
- Agent Step - AI agent workflow step guide
- SSH - Remote execution guide
- Docker - Container execution guide
- HTTP - API interaction guide
- Router - Pattern-based routing guide
- Approval - Human approval gates
- Mail - Email notification guide
- Chat - LLM integration guide
- JQ - JSON processing guide
- S3 - S3 operations guide
- Redis - Redis operations guide
- Writing Workflows - Using step types in workflows
