Cross-DAG triggers & notifications¶
So far every dependency lived inside one DAG. In this final chapter you'll chain whole DAGs together with trigger_after, see the result in the console's DAG Graph, and wire a notify webhook so a failed run pings you instead of waiting to be discovered.
Task deps vs. DAG deps¶
deps wires tasks within a single run of a single DAG. trigger_after works one level up: it runs an entire downstream DAG after another DAG succeeds. That's the natural shape when one pipeline ends where another begins — an ingest DAG owned by one team, a reporting DAG owned by another — without merging them into one giant workflow.
cronova ships both halves of this pattern as runnable examples in the repo's dags/ directory: upstream_ingest.yaml and downstream_report.yaml.
The upstream DAG¶
The upstream is a completely ordinary DAG — nothing in it knows a downstream exists:
dag_id: upstream_ingest
# manual/scheduled upstream; downstream_report runs after this succeeds
start_date: 2026-06-01
max_active_runs: 1
tasks:
- id: ingest
type: shell
command: "echo ingesting for $CRONOVA_LOGICAL_DATE && sleep 1"
It has no schedule, so it runs only when triggered — convenient for this walkthrough, but a cron schedule works exactly the same way.
The downstream DAG: trigger_after¶
The downstream declares the dependency on its side:
dag_id: downstream_report
start_date: 2026-06-01
max_active_runs: 1
default_retries: 1
# runs automatically once upstream_ingest succeeds for the same logical_date
trigger_after:
- dag_id: upstream_ingest
tasks:
- id: build_report
type: shell
command: "echo building report from $CRONOVA_LOGICAL_DATE data"
pool: reports # configure size with: cronova pools set reports <n>
retries: 2
timeout: 600
Two things to notice:
- No
schedule. A DAG with an empty schedule runs only on manual trigger ortrigger_after— the upstream is its schedule. trigger_afternames the upstream. Wheneverupstream_ingestfinishes a run insuccess, the scheduler creates a run ofdownstream_reportfor the same logical date.
Because the logical date carries over, a backfilled upstream run triggers a report for that period — not for wall-clock "now". Catchup and cross-DAG triggers compose for free.
Tip
trigger_after takes a list, so a downstream can fan in from several upstreams. It fires only when every listed upstream has a successful run for that logical date, and it still respects the downstream's own max_active_runs.
The success signal is durable: cronova commits it with the upstream run's final
state. If the global queued-run limit is full, the signal stays pending and is
retried on later scheduler ticks. Once admitted, the downstream waits in
queued until its own max_active_runs slot is available.
Trigger the chain¶
With cronova serve running, fire the upstream:
Give it a few seconds (the ingest task sleeps for one, and the scheduler ticks every 2s), then check both DAGs:
The upstream shows a normal manual run:
RUN_ID LOGICAL_DATE STATE TRIGGER TASKS
upstream_ingest__20260707T091502Z 2026-07-07T09:15:02Z success manual ingest=success
And the downstream shows a run you never triggered:
RUN_ID LOGICAL_DATE STATE TRIGGER TASKS
downstream_report__20260707T091502Z 2026-07-07T09:15:02Z success dependency build_report=success
The TRIGGER column reads dependency, and LOGICAL_DATE matches the upstream run exactly — that's the cross-DAG trigger at work.
See it in the DAG Graph¶
Open the console at http://localhost:8090 and click Graph in the navigation. The DAG Graph view draws the trigger dependencies between DAGs — you'll see an edge from upstream_ingest to downstream_report. As your workflow scheduler grows to dozens of DAGs, this graph is how you answer "what runs after what?" at a glance.
Webhook notifications with notify¶
A pipeline that fails silently is worse than no pipeline. notify is a DAG-level field: a URL that receives a JSON POST when a run finishes in a state you list. Add it to any DAG:
dag_id: downstream_report
# … tasks as above …
notify:
url: https://hooks.slack.com/services/T000/B000/XXXX
on: [failure]
on accepts failure and/or success — failure also covers cancelled and timed-out runs, so anything non-green alerts. The URL must be http(s).
The payload looks like this:
{
"text": "cronova · downstream_report · run downstream_report__20260707T091502Z finished: failed (tasks: [build_report])",
"dag_id": "downstream_report",
"run_id": "downstream_report__20260707T091502Z",
"state": "failed",
"logical_date": "2026-07-07T09:15:02Z",
"started_at": "2026-07-07T09:15:04Z",
"finished_at": "2026-07-07T09:15:07Z",
"duration_ms": 3000,
"failed_tasks": ["build_report"]
}
The text field is a ready-made human summary, so a Slack, Feishu, or Discord incoming webhook URL renders it directly with zero glue code; the structured fields serve your own endpoints.
To see it fire, temporarily change build_report's command to exit 1, trigger upstream_ingest again, and watch the cronova serve log: you'll see a notify sent line with the run id (or notify non-2xx / notify post if delivery failed). Delivery is asynchronous and best-effort — it never blocks the scheduling loop.
If you set sla or dagrun_timeout on a DAG, breaches report through the same webhook — configuring the threshold is itself the opt-in, independent of on.
Warning
Outbound webhooks are SSRF-hardened: cronova refuses URLs that resolve to private or internal addresses (localhost, RFC 1918 ranges, link-local, cloud metadata) and never follows redirects. Test against a public endpoint — a real Slack/Feishu webhook or a hosted request-inspection service — not a receiver on http://localhost.
What you learned¶
trigger_afterchains whole DAGs: the downstream runs automatically once every listed upstream has a successful run for the same logical date, showingTRIGGER=dependencyincronova runs.- A DAG with no
scheduleruns only on manual trigger ortrigger_after, and the console's Graph view visualizes all cross-DAG edges. notifyPOSTs a JSON payload — with a Slack-readytextsummary — onfailureand/orsuccess, delivered asynchronously with SSRF protection built in.
Where to go next¶
That's the whole tutorial — you've gone from a single echo task to a scheduled, dependency-aware, retry-hardened, cross-DAG pipeline with alerting, all on a compact native install with embedded SQLite. From here:
- Deployment — install cronova as a systemd/launchd service, switch to the crash-recoverable gRPC executor so running tasks survive a scheduler restart or upgrade, and keep it updated with
cronova update. - AI Agents (MCP) — let AI agents list, create, validate, and trigger DAGs through the built-in MCP server and the remote JSON CLI.
- DAG & Task Reference — the exhaustive schema: every DAG and task field, all five task types, trigger rules, and pools.
- CLI Reference — every command and flag, from
servetotokens create.