跳转至

DAG & Task Reference

The complete YAML schema for a cronova DAG (directed acyclic graph of tasks) — every DAG-level and task-level field, the five task types, trigger rules, and resource pools. For a hands-on introduction see Getting Started; for the project overview see the README.

A DAG is a single YAML file in the dags/ directory (default ./dags, or the service's data dir). cronova validates and cycle-checks every DAG on load; runnable examples live in dags/. Parsing is strict: unknown fields, unsupported task types, trailing YAML documents, invalid negative values, and out-of-range settings are rejected instead of ignored.

Safety limits are enforced before a definition can enter the scheduler: 1 MiB per YAML document, 1,000 tasks, 10,000 dependency edges, 256 dependencies per task, 128-byte identifiers, 256 KiB commands, 100 retries, and one year for any configured delay/timeout/SLA. These bounds prevent a typo or oversized upload from turning validation into unbounded memory or scheduler work.

dag_id: daily_etl
schedule: "0 2 * * *"
start_date: 2026-06-01
catchup: true
max_active_runs: 1
default_retries: 2
tasks:
  - id: extract
    type: shell
    command: "python extract.py --date {{ logical_date }}"
    pool: default
  - id: transform
    command: "python transform.py --date {{ logical_date }}"
    deps: [extract]
  - id: load
    command: "psql -f load.sql"
    deps: [transform]
    retries: 3
    timeout: 1800
trigger_after:
  - dag_id: upstream_ingest
notify:
  url: https://hooks.example.com/cronova
  on: [failure]

DAG-level fields

Field Type Default Description
dag_id string — (required) Unique identifier for the DAG.
schedule string "" (manual) Cron expression ("0 2 * * *") or an interval ("@every 30s"). Empty = the DAG runs only on manual trigger, trigger_after, or trigger_on_event.
timezone string "" (UTC) IANA zone (e.g. Asia/Shanghai) the cron fields — and a date-only start_date — are evaluated in, DST included.
start_date date string Earliest logical date the DAG is scheduled for; anchors catchup/backfill.
catchup bool false Backfill missed periods between start_date and now. Backfilled runs are throttled so they never flood.
max_active_runs int 1 Maximum concurrent runs of this DAG (0 is treated as 1).
max_active_tasks int 0 (unlimited) Cap on this DAG's concurrently queued/running tasks across all of its runs — a per-DAG budget complementing the global pools.
execution_policy string parallel How queued runs are admitted when another run of this DAG is active: parallel (up to max_active_runs concurrently), serial_wait (one at a time; later runs queue in logical-date order), serial_discard (one at a time; runs arriving while busy are cancelled, visibly), or serial_priority (one at a time; the queue drains highest run priority first — set priority when triggering). Serial policies force at most one active run regardless of max_active_runs.
worker_group string "" (local) Default dial-in worker group for every task that doesn't set its own worker_group. Empty = tasks run on the scheduler's configured local executor.
trigger_on_event list of strings External event keys this DAG subscribes to: POST /api/events {"key": …} creates one event-triggered run per subscriber (idempotent per key). The event payload becomes the run's params.
default_retries int 0 Retry count applied to tasks that don't set their own retries.
default_retry_delay int (seconds) 0 Retry delay applied to tasks that don't set their own retry_delay.
sla int (seconds) 0 Run-level soft deadline, measured from run start. A breach raises an alert; it does not cancel the run.
dagrun_timeout int (seconds) 0 Run-level hard deadline, measured from run start. 0 = no limit.
tasks list — (required) The task list (see below).
trigger_after list of {dag_id} Run this DAG after another DAG succeeds (cross-DAG dependency). Visualized in the console's DAG Graph.
notify {url, on, format, group} Run-completion notification. on is a list of "failure" and/or "success". url is an http(s):// webhook or mailto:addr[,addr] (delivered through the server's smtp: relay).
notify.format string raw One of raw, slack, feishu, dingtalk, email. raw posts the full JSON payload; the chat formats wrap the summary text in the platform's incoming-webhook envelope, so the message renders in Slack/Feishu/DingTalk without a relay service; email is the plain-text mail body used for mailto: targets.
notify.group string Name of an alert group — a named fan-out of 1–16 channels managed in the console or via POST /api/alert-groups/{name}. When set it wins over notify.url and every channel in the group is alerted.

Cron schedules are evaluated in UTC by default; prefix the expression with CRON_TZ=<zone> to evaluate it in a specific timezone:

schedule: "CRON_TZ=Asia/Shanghai 0 2 * * *"   # 02:00 Shanghai time, every day

paused is not a YAML field. Pausing is operational state managed from the console, CLI (cronova pause <dag_id>), or API, and is preserved across DAG reloads.

Notifications: webhooks, email & alert groups

notify.url accepts two kinds of targets:

  • an http(s):// incoming-webhook URL — the run summary is POSTed as JSON, shaped by notify.format;
  • mailto:addr[,addr] — the alert is sent as email through the server's SMTP relay. This requires the smtp: section of the server config to be filled in; without it, mail channels fail delivery (logged, never blocking the scheduler).

Instead of pasting the same URL into every DAG, notify.group references a named alert group: a reusable bundle of 1–16 channels, each with its own URL (webhook or mailto:) and format. Groups are managed in the console (Variables & Connections → Alert groups) or via the API (GET /api/alert-groups, POST/DELETE /api/alert-groups/{name}), and one run alert fans out to every channel of the group.

notify:
  group: oncall      # alert every channel of the "oncall" group
  on: [failure]

Resolution is most-specific-first: a set notify.group wins over notify.url; a group name that no longer resolves (e.g. the group was deleted) falls back to the DAG's own notify.url, and then to the instance-wide default notify target — a dangling reference is logged loudly but never loses the alert.

Definition snapshots

Every run stores the exact canonical YAML and SHA-256 definition hash it started with. Editing a DAG while a run is active therefore changes future runs only; the active run keeps its original task graph and cannot be wedged by a removed or renamed task. An explicit retry is the exception: it intentionally adopts the latest DAG definition, records that definition hash on the new attempts, and leaves removed task instances as historical rows rather than dispatching them again.

Task-level fields

Each entry under tasks: describes one task.

Field Type Default Description
id string — (required) Task identifier, unique within the DAG.
type string shell One of shell, python, sql, jar, http, subdag. See Task types.
command string The command (shell), code (python), or query (sql). Supports template variables. Not used for http.
deps list of task ids Upstream tasks that must satisfy this task's trigger_rule before it runs. Edges are cycle-checked.
pool string default The resource pool this task consumes a slot from.
priority int 0 Higher runs first when tasks contend for the same pool.
worker_group string inherits DAG worker_group Routes this task to a group of dial-in remote workers (the workers' group label, default "default"). Empty (and no DAG-level default) = run on the scheduler's local executor.
retries int inherits default_retries Times to retry on failure.
retry_delay int (seconds) inherits default_retry_delay Delay between retries.
retry_backoff string fixed How the wait between retries grows: fixed (constant retry_delay) or exponential (waits retry_delay·2^(n-1) before the n-th retry).
retry_delay_max int (seconds) 0 Caps the exponential wait. 0 = no explicit cap (a built-in 24h safety ceiling still applies).
timeout int (seconds) 0 Per-attempt execution timeout; on breach the whole process group is killed. 0 = none.
sla int (seconds) 0 Task soft deadline from run start; breach alerts only.
trigger_rule string all_success When to run relative to upstream states. See Trigger rules.
when string Runtime condition template (e.g. "{{ params.env }}" or "{{ ti.check.proceed }}"), evaluated once the task is otherwise ready. A falsy render ("", false, 0, no, or an unresolved placeholder) marks the task skipped.
foreach list of strings Fans the task out into one task per item at definition time: ids become <id>_<index>, {{ item }} / {{ item_index }} are substituted in command/when, and downstream deps on the original id cover every shard. Each shard keeps its own retries, log, and state.
conn string Connection id for a sql task (selects driver + builds the DSN).
project string Name of an uploaded project directory to stage as the working directory (shell tasks; not combinable with worker_group). See Getting Started → Projects.
http object HTTP request spec for http tasks (see below).
subdag string For type: subdag: the DAG to run as a sub-workflow. The task launches a linked child run (visible in run history with trigger type subdag and a parent link) and mirrors its terminal state. Cancelling the parent cascades to the child; a task retry starts a fresh child run (the old one stays as history). Nesting is capped at 5 levels as a cycle backstop.
depends_on_dag object Cross-DAG wait: hold this task until another DAG's matching period run has succeeded. Fields: dag (target id), offset (which period, in date-expression offset grammar — ""/same, - 1d, .month_start…), timeout (seconds from run start; 0 = wait until dagrun_timeout), on_timeout (fail default, or skip). A failed target run keeps the wait alive (it may be retried); only the timeout resolves the standoff.

http task spec

Set under a task's http: key when type: http:

Field Type Default Description
method string GET HTTP method.
url string — (required) Request URL. Supports templates (e.g. https://{{ conn.api.host }}/path).
headers map Header name → value; values support templates (e.g. Authorization: Bearer {{ var.TOKEN }}).
body string Request body; supports templates.
expected_status list of int 2xx Status codes considered success (e.g. [200, 201]).

Task types

Type Runs as command holds Needs on host
shell OS subprocess (sh -c) any shell command the tools the command invokes
python OS subprocess (python3) Python code python3 on the service PATH
sql in-process (native driver) the SQL query; conn selects the connection nothing extra
jar OS subprocess (java) a java -jar … command a JRE/JDK on the PATH
http in-process HTTP client — (use the http: spec) nothing extra
subdag scheduler-internal (child run) — (use the subdag: field) nothing extra

sql and http tasks are self-contained in the binary. shell, python, and jar tasks (and anything a shell task invokes) require that tool installed and on the service PATH — see Deployment.

tasks:
  - id: shell_task
    type: shell
    command: "echo running {{ logical_date }}"
  - id: python_task
    type: python
    command: |
      import os
      print(os.environ['CRONOVA_LOGICAL_DATE'])
  - id: sql_task
    type: sql
    conn: warehouse
    command: "SELECT count(*) FROM events WHERE day = '{{ params.day }}'"
  - id: jar_task
    type: jar
    command: "java -jar app.jar --in {{ logical_date }}"
  - id: http_task
    type: http
    http:
      method: POST
      url: "https://{{ conn.api.host }}/ingest"
      headers: { Authorization: "Bearer {{ var.TOKEN }}" }
      body: '{"date":"{{ logical_date }}"}'
      expected_status: [200, 201]

Template variables

Any command, url, header, body, or query can reference {{ name }} placeholders, substituted at dispatch. Built-in run variables are also injected into the process environment as CRONOVA_<NAME> (uppercased):

Variable Env var Meaning
{{ logical_date }} CRONOVA_LOGICAL_DATE The run's logical date (YYYY-MM-DD) — the period it represents, which is what makes catchup meaningful.
{{ logical_datetime }} CRONOVA_LOGICAL_DATETIME Logical date-time, RFC3339.
{{ run_id }} CRONOVA_RUN_ID Unique id of this run.
{{ dag_id }} CRONOVA_DAG_ID The DAG id.
{{ task_id }} CRONOVA_TASK_ID This task's id.
{{ try_number }} CRONOVA_TRY_NUMBER Attempt number (increments on retry).

When the DAG declares a timezone:, logical_date/logical_datetime render in that zone (the run's own calendar day), while storage stays UTC.

Date expressions

logical_date / logical_datetime accept offsets, anchors, and custom formats directly inside the placeholder:

{{ logical_date[.anchor][ ±N<unit> ]... [| format] }}
Piece Values Notes
anchor .month_start .month_end .week_start .week_end Binds to the base name, applies first; weeks start Monday; time resets to midnight.
offset ±N + d (days) h (hours) w (weeks) mo (months) Repeatable, applied left to right. d/w/mo are calendar arithmetic (wall clock survives DST); h is an absolute duration.
format \| + strftime subset: %Y %y %m %d %H %M %S %% Default: YYYY-MM-DD for logical_date, RFC3339 for logical_datetime.

Examples:

command: "python etl.py --day {{ logical_date - 1d | %Y%m%d }}"     # yesterday as 20260807
command: "report.sh --from {{ logical_date.month_start }} --to {{ logical_date.month_end }}"
command: "cleanup.sh --before {{ logical_date.month_start - 1d }}"  # last day of previous month
command: "sync.sh --since {{ logical_datetime - 6h }}"

An expression that does not parse (unknown unit, bad % token, stray text) is left in the command verbatim — typos stay visible in the task log instead of silently rendering empty.

Shell tasks do not inherit the scheduler's complete process environment. Cronova passes a small runtime-safe set (PATH, locale, home/temp and certificate variables) plus the task-specific CRONOVA_* values above. This prevents server credentials such as CRONOVA_ADMIN_PASSWORD from reaching task code. Add a parent variable explicitly with CRONOVA_TASK_ENV_ALLOWLIST=name1,name2, or put the value in the task's resolved environment instead.

Plus UI-managed references, resolved server-side (secrets never enter the blanket env):

  • {{ var.KEY }} — a shared variable.
  • {{ conn.ID.FIELD }} — a connection field: host, port, login (alias user), password, type, or an extra JSON field as extra.KEY.
  • {{ params.KEY }} — a manual-trigger parameter (also injected as CRONOVA_PARAM_<KEY>). Event-triggered runs receive the event payload as params plus {{ params.event_key }}.
  • {{ ti.TASK_ID.KEY }} — a field of an upstream task's emitted output (this run only).

Passing data between tasks

A task can hand small values (row counts, generated file paths, ids) to its downstream tasks by writing a flat JSON string map to the file named in $CRONOVA_OUTPUT (up to 64 KB):

tasks:
  - id: produce
    command: 'echo "{\"rows\":\"1234\"}" > "$CRONOVA_OUTPUT"'
  - id: consume
    command: 'echo upstream wrote {{ ti.produce.rows }} rows'
    deps: [produce]

The output is collected when the task finishes successfully and stored per (run, task); trigger rules guarantee the upstream finished before a downstream referencing it is dispatched. This is metadata passing, not a data channel — move real datasets through external storage.

Self-skipping tasks

A task that exits with code 99 is recorded as skipped instead of failed — the shell-level way to say "nothing to do here today". Combine with downstream trigger_rule: none_failed (skip passes through) or the default all_success (skip blocks) to shape what happens next; when: (see task fields) is the declarative alternative evaluated before the task even starts.

In the console task editor these are inserted as click/drag pills — you don't type the {{ }}.

Trigger rules

trigger_rule decides when a task runs given its upstream (deps) task states:

Rule Runs when
all_success (default) every upstream task succeeded
all_done every upstream task finished (any state)
all_failed every upstream task failed
one_success at least one upstream task succeeded
one_failed at least one upstream task failed
none_failed no upstream task failed (success or skipped)

Resource pools

A pool is a named set of global concurrency slots; a task consumes one slot of its pool while running, and higher-priority tasks win contended slots. Pools are global resources configured out-of-band (not in DAG YAML):

cronova pools                    # list pools and usage
cronova pools set reports 4      # create/resize the "reports" pool to 4 slots

Every task defaults to the default pool. See the CLI Reference and Architecture.

See also