Sub-DAGs
Run another DAG from a step and compose workflows out of smaller, reusable ones.
Overview
A step that uses action: dag.run or action: dag.enqueue starts a child DAG run. The DAG containing the step is the parent run, and the top of the chain is the root run. A child run has its own DAG-run ID, its own status, its own logs, and its own work directory, and it is linked back to the step that started it.
| Action | Parent waits? | Child run | Use when |
|---|---|---|---|
dag.run | Yes | Executes immediately as a sub-DAG of the parent | Later steps need the child's result |
dag.enqueue | No | Becomes its own top-level run with status queued | The child is fire-and-forget work |
Calling a Child DAG
steps:
- id: prepare
run: ./prepare.sh
- id: run_child
action: dag.run
with:
dag: child-workflow
params:
task_id: "123"
depends: prepare
- id: report
run: echo "Parent completed"
depends: run_childwith.dag is required. with.params is optional. dag.run accepts no other with fields.
Resolving the Child DAG
with.dag is resolved in this order:
- A DAG document defined in the same file after
---. - A DAG registered in the DAGs directory, by name (
child-workflow) or by path relative to the DAGs directory (workflows/child.yaml).
Names, paths, and the .yaml suffix are all accepted:
with:
dag: child-workflow # registered name
with:
dag: workflows/child.yaml # path under the DAGs directoryIf the child cannot be resolved, the step fails during setup with failed to find DAG "<name>".
Defining Child DAGs in the Same File
Separate documents with --- and call them by name. This keeps a small pipeline in one file.
steps:
- id: process
action: dag.run
with:
dag: data-processor
params:
type: daily
---
name: data-processor
params:
- name: type
default: batch
steps:
- id: extract
run: echo "Extracting ${params.type} data"
- id: transform
run: echo "Transforming data"
depends: extractLocal DAG documents are addressable only from within the file that defines them. They are not separately registered, so they cannot be started, scheduled, or queried by name on their own. Move a child into its own file once other workflows need it.
Passing Parameters
Child DAGs receive input through with.params only. Declare the contract with params: in the child, and read it with ${params.<name>}.
Map form is preferred:
steps:
- id: run_child
action: dag.run
with:
dag: child
params:
source: production
limit: 100Space-separated string form is also accepted:
with:
dag: child
params: "source=production limit=100"Values are resolved in the parent before the child starts, so parent parameters, environment values, and step outputs can be forwarded:
with:
dag: child
params:
date: ${params.date}
build: ${steps.build.outputs.version}Runtime overrides passed this way stay literal in the child. See Parameters.
Reading Child Results
A dag.run step writes the child run result to stdout as JSON. Capture it with output::
steps:
- id: run_child
action: dag.run
with:
dag: child
output: CHILD
- id: use_result
depends: run_child
run: |
echo "run id: ${CHILD.dagRunId}"
echo "status: ${CHILD.status}"
echo "message: ${CHILD.outputs.MESSAGE}"Result Object
| Field | Description |
|---|---|
name | Child DAG name. |
dagRunId | Child DAG-run ID. |
params | Parameters the child ran with. |
outputs | Values captured by output: on child steps. |
outputValues | Values published by outputs.write or stdout.outputs in the child. |
status | Terminal status of the child run. |
Publishing Values From a Child
Two child-side mechanisms cross the run boundary:
# child.yaml
steps:
- id: build
run: ./build.sh # prints the image tag
output: IMAGE # -> ${CHILD.outputs.IMAGE}
- id: publish
action: outputs.write
with:
values:
image: ${IMAGE} # -> ${CHILD.outputValues.image}Declared step outputs (outputs: with name:) are scoped to the DAG document that defines them. They are not visible to the parent — republish them with outputs.write or stdout.outputs when a parent needs them.
Keep returned values small. Use Artifacts for files, reports, and large payloads.
Running Children in Parallel
Add parallel to a dag.run or dag.enqueue step to fan out one child run per item. Each child receives ${ITEM}.
steps:
- id: fanout
action: dag.run
with:
dag: file-processor
params:
file: ${ITEM}
parallel:
items: [a.csv, b.csv, c.csv]
max_concurrent: 2
output: RESULTS| Field | Description |
|---|---|
parallel.items | Static list of items. |
parallel.variable | Variable or reference whose value expands at runtime. |
parallel.max_concurrent | Maximum concurrent child runs. Must be greater than 0. |
Object items expose their fields as ${ITEM.<field>}:
parallel:
items:
- account_id: acct_1
region: us-east-1
- account_id: acct_2
region: eu-west-1The captured aggregate is JSON:
{
"summary": { "total": 3, "succeeded": 3, "failed": 0 },
"results": [ { "name": "file-processor", "dagRunId": "...", "params": "file=a.csv", "outputs": {}, "status": "succeeded" } ],
"outputs": [ { "PROCESSED": "a.csv" } ]
} - id: reduce
depends: fanout
run: echo "${RESULTS.summary.succeeded} of ${RESULTS.summary.total} succeeded"outputs contains only the successful child runs, so its indexes do not line up with the item list. Do not infer item identity from array position — include the item in each child's own output instead.
Discovering work at runtime uses the same shape:
steps:
- id: discover
run: ./list-files.sh
outputs:
- name: files
type: json
- id: process
action: dag.run
with:
dag: file-processor
params:
file: ${ITEM}
parallel:
items: ${steps.discover.outputs.files}
max_concurrent: 4
depends: discoverQueueing Children Asynchronously
dag.enqueue persists and queues the child run, then lets the parent continue. The parent never sees the child's result.
steps:
- id: queue_rollup
action: dag.enqueue
with:
dag: customer-rollup
params:
customer_id: ${params.customer_id}
queue: background
output: QUEUED
- id: continue
depends: queue_rollup
run: echo "Queued ${QUEUED.dagRunId}"with.queue overrides the queue for that run. Without it, Dagu uses the child DAG's own queue, the base-config default, or the child's local queue. A single enqueue returns {name, dagRunId, params, queue, status}; a parallel enqueue returns {summary: {total, queued}, runs: [...]}.
See Queue Assignment.
What a Child Run Does Not Inherit
Working Directory
Every child run gets its own work directory, exposed as DAG_RUN_WORK_DIR and ${context.paths.work_dir}. A child that does not declare working_dir runs there — not in the parent's working directory. Parallel children each get a distinct directory, so relative file writes cannot collide.
To pin a child to a specific directory, declare it in the child:
name: child
working_dir: /srv/app
steps:
- run: pwd # /srv/appStep-level working_dir inside the child takes precedence over the child's root working_dir. An explicit working_dir inherited through the child's base configuration counts as explicit and is honored.
Lifecycle Handlers
Sub-DAGs do not inherit handler_on from base configuration. Without this, a parent's failure alert would fire again for every nested run. Declare handlers explicitly in each child that needs them. See Sub-DAG Handler Isolation.
Tools
Managed tools are scoped to one DAG run. A child that uses a pinned external command must declare its own tools. See Tools.
What Does Carry Over
The runtime profile selected for the parent run is passed to the child run, and child retries inherit the profile recorded on that child run. See Runtime Profiles.
Failure Handling
A failed or aborted child run fails the parent step. A partially succeeded child produces a partially succeeded parent step.
Use continue_on when the parent should proceed anyway:
steps:
- id: optional_child
action: dag.run
with:
dag: nice-to-have
continue_on:
failure: true
- id: next
depends: optional_child
run: echo "Continues even if the child failed"Retries, timeouts, preconditions, and repeat apply to the parent step as usual and re-run the child as a unit. For retry behavior inside the child, configure retry_policy on the child's own steps.
Approval Gates
approval on a dag.run step gates after the child finishes: the child executes fully, then the step enters Waiting so a reviewer can inspect the result before downstream steps run.
steps:
- id: run_integration_tests
action: dag.run
with:
dag: integration-test-suite
approval:
prompt: "Review test results before deploying"
- id: deploy
run: ./deploy.sh production
depends: run_integration_testsSee Approval Gates.
Observing and Retrying Child Runs
In the Web UI, a run's detail page lists its child runs; selecting one opens that child run.
From the CLI:
# Inspect a child run
dagu status --run-id=<root-run-id> --sub-run-id=<child-run-id> my-workflow
# Retry the whole root run (children re-run as part of it)
dagu retry --run-id=<root-run-id> my-workflow
# Retry a single step inside a persisted child run
dagu retry --run-id=<root-run-id> --sub-run-id=<child-run-id> --step=build my-workflow--sub-run-id requires --step. Retry targets the root run — a child run cannot be retried as if it were a top-level run. See Durable Execution.
The REST API exposes child runs under the root run:
/api/v1/dag-runs/{name}/{dagRunId}/sub-dag-runs/{subDAGRunId}/...Distributed Execution
worker_selector on a dag.run step routes the child run to a matching worker:
steps:
- id: run_on_gpu
action: dag.run
with:
dag: train-model
worker_selector:
gpu: "true"Two constraints apply:
- A child DAG containing approval steps cannot be dispatched to a worker.
- The worker must be able to resolve the child DAG. If a parent is dispatched to a worker without a local DAG cache, child resolution fails; set
worker_selector: localon the parent, or make the child available to the worker.
Limitations
human.tasksteps are allowed only in root DAGs. A child DAG containing one is rejected.- Declared step outputs do not cross a run boundary.
- Local DAG documents cannot be started, scheduled, or queried independently.
Related
- Control Flow — dependencies, iteration, and routing
- Parameters — declaring and overriding child inputs
- Outputs — publishing values from steps
- Queue Assignment — where enqueued children run
- YAML Specification — field reference

