Actions & Integrations Examples
Examples for custom actions, containers, Kubernetes, SSH, HTTP, jq, sandboxed JavaScript, archive extraction, mail, and mailboxes.
Custom Action
actions:
say:
description: Print a reusable message
input_schema:
type: object
additionalProperties: false
required: [message]
properties:
message:
type: string
template:
action: exec
with:
command: echo
args:
- {$input: message}
steps:
- action: say
with:
message: "build finished"with is validated by input_schema; the rendered template runs as a builtin exec action.
Add outputs when a step should publish validated values for downstream steps. Downstream references then use steps.<id>.outputs.<name>.
Container Workflow
# DAG-level container for all steps
container:
image: python:3.11
env:
- PYTHONPATH=/app
volumes:
- ./src:/app
steps:
- id: install
run: pip install -r requirements.txt
- id: test
run: pytest tests/
depends: install
- id: build
run: python setup.py build
depends: testKeep Container Running
# Use keep_container at DAG level
container:
image: postgres:16
keep_container: true
env:
- POSTGRES_PASSWORD=secret
ports:
- "5432:5432"
steps:
- id: start_postgres
run: postgres -D /var/lib/postgresql/data
- id: wait_for_postgres
run: pg_isready -U postgres -h localhost
retry_policy:
limit: 10
interval_sec: 2
depends: start_postgresStep-Level Container
steps:
- id: build
container:
image: node:18
volumes:
- ./src:/app
working_dir: /app
run: npm run buildKubernetes Job
kubernetes:
namespace: batch
service_account: dagu-runner
steps:
- id: report
action: k8s.run
with:
image: alpine:3.20
command: sh -c 'echo hello from kubernetes'Exec Into Existing Container
# Run commands in an already-running container
container: my-app-container
steps:
- id: migrate
run: php artisan migrate
- id: clear_cache
run: php artisan cache:clear
depends: migrateExec Mode with Overrides
# Override user and working directory
container:
exec: my-app-container
user: root
working_dir: /var/www
env:
- APP_DEBUG=true
steps:
- id: install
run: composer install
- id: fix_permissions
run: chown -R www-data:www-data storage
depends: installMixed Exec and Image Mode
steps:
# Exec into app container
- id: maintenance_mode
container: my-app
run: php artisan down
# Run migration in fresh container
- id: migrate
container:
image: my-app:latest
run: php artisan migrate
depends: maintenance_mode
# Exec back into app container
- id: restart
container: my-app
run: php artisan up
depends: migrateRemote Commands via SSH
# Configure SSH once for all steps
ssh:
user: deploy
host: production.example.com
key: ~/.ssh/deploy_key
steps:
- id: health_check
run: curl -f localhost:8080/health
- id: restart_app
run: systemctl restart myapp
depends: health_checkContainer Volumes: Relative Paths
working_dir: /app/project
container:
image: python:3.11
volumes:
- ./data:/data # Resolves to /app/project/data:/data
- .:/workspace # Resolves to /app/project:/workspace
steps:
- run: python process.pyHTTP Requests
steps:
- action: http.request
with:
method: POST
url: https://api.example.com/webhook
headers:
Content-Type: application/json
body: '{"status": "started"}'Wait for Readiness
steps:
- id: wait_for_api
action: wait.http
with:
url: https://api.example.com/health
status: 200
poll_interval: 5s
timeout_sec: 300
- id: continue_after_ready
run: ./run-after-ready.sh
depends: wait_for_apiJSON Processing
steps:
# Fetch sample users from a public mock API
- id: fetch_users
run: |
response="$(curl -fsS https://reqres.in/api/users)"
{
printf 'api_response<<JSON\n'
printf '%s\n' "$response"
printf 'JSON\n'
} >> "$DAGU_OUTPUT_FILE"
outputs:
- name: api_response
type: json
# Extract user emails from the JSON response
- id: extract_emails
action: jq.filter
with:
filter: '.data[] | .email'
data: ${steps.fetch_users.outputs.api_response}
depends: fetch_usersJavaScript Transform
steps:
# Fetch a page
- id: fetch
action: http.request
with:
method: GET
url: https://example.com
output: HTML
# Extract and normalize links in a sandboxed script, no Node.js needed
- id: links
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
depends: fetch
# A returned array arrives in the next script as an array
- id: report
action: js.run
with:
input: ${LINKS}
script: |
return `${input.length} links, first ${input[0] ?? "none"}`;
depends: linksArchive Extraction
working_dir: /tmp/data
steps:
- action: archive.extract
with:
source: dataset.tar.zst
destination: ./datasetContainer Startup & Readiness
container:
image: alpine:latest
startup: command # keepalive | entrypoint | command
command: ["sh", "-c", "my-daemon"]
wait_for: healthy # running | healthy
log_pattern: "Ready" # Optional regex to wait for
restart_policy: unless-stopped
steps:
- run: echo "Service is ready"Private Registry Auth
registry_auths:
ghcr.io:
username: ${env.GITHUB_USER}
password: ${env.GITHUB_TOKEN}
container:
image: ghcr.io/myorg/private-app:latest
steps:
- run: ./appMulti-Container Workflow
steps:
- id: build
container:
image: node:24
volumes:
- ./src:/app
working_dir: /app
run: npm run build
- id: test
container:
image: node:24
volumes:
- ./src:/app
working_dir: /app
run: npm test
depends: build
- id: deploy
container:
image: python:3.11
env:
- AWS_DEFAULT_REGION=us-east-1
run: python deploy.py
depends: testSSH: Advanced Options
ssh:
user: deploy
host: app.example.com
port: 2222
key: ~/.ssh/deploy_key
strict_host_key: true
known_host_file: ~/.ssh/known_hosts
steps:
- run: systemctl status myappMail
smtp:
host: smtp.gmail.com
port: "587"
username: "${env.SMTP_USER}"
password: "${env.SMTP_PASS}"
steps:
- action: mail.send
with:
to: team@example.com
from: noreply@example.com
subject: "Weekly Report"
message: "Attached."
attachments:
- report.txtProcess Incoming Email
secrets:
- name: SUPPORT_MAIL_PASSWORD
ref: mail/support
mail_accounts:
support@example.com:
provider: google
password: ${SUPPORT_MAIL_PASSWORD}
schedule: "*/5 * * * *"
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
key: ${foreach.email.id}
max_concurrent: 1
steps:
- id: ticket
run: ./create-ticket.sh "$SUBJECT"
env:
- SUBJECT: ${foreach.email.subject}
- id: done
depends: ticket
action: mail.organize
with:
mailbox: support@example.com
emails: ${foreach.email.id}
mark: readEach email is marked read after its ticket exists, so one that fails stays unread and is retried on the next run.
Excel Round Trip
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: 5mThe rows without a status are read, each one is submitted, and the status is written back to the row it came from, so a rerun skips the rows already done. A loop with one failed submission is partially succeeded, so the write-back still marks the rows that succeeded and the failed rows wait for the next run. No spreadsheet application is needed.

