-
Notifications
You must be signed in to change notification settings - Fork 0
Configuration Reference
The canonical, exhaustive reference for the yacron2 YAML configuration. It
documents the top-level structure and every per-job option, with the exact
strictyaml type, default, and load-time validation rule taken from
yacron2/config.py. Deep topics (schedules, reporting, the HTTP API, metrics,
logging, includes) have dedicated pages linked from each section.
A configuration is either a single YAML file or a directory of *.yml/*.yaml
files, selected with the -c flag (see Command-Line Reference).
The document is parsed and validated against a fixed strictyaml schema
(CONFIG_SCHEMA); an unknown key, a wrong type, or a malformed value is a hard
ConfigError at load time. An empty document is valid.
Classic (Vixie-style) crontab files are accepted alongside YAML, recognised by
name (*.crontab, *.cron, or a file named crontab): each entry is lowered
to an ordinary job definition and merged over the same DEFAULT_CONFIG
defaults documented below, so internally it is configured to yacron2's
standard behavior rather than an emulation of cron's. A crontab can only
define jobs; every other section on this page (and any per-job option beyond
schedule, command, shell, timezone, and environment) is YAML-only. See
Classic Crontabs.
In the option tables below, "Required" means the strictyaml key is mandatory
(not wrapped in Opt(...)); every other key is optional and falls back to the
default shown. Per-job defaults come from DEFAULT_CONFIG; a defaults: block
and any included files override DEFAULT_CONFIG, and an individual job overrides
both. See Includes, Defaults, and Multi-File Config for
the precise merge order.
defaults: { ... } # optional: per-job defaults for this file
jobs: # optional: list of job definitions
- name: ...
command: ...
schedule: ...
dags: # optional: durable orchestration DAGs (needs state)
- name: ...
tasks: [ ... ]
include: [ ... ] # optional: list of other config files to merge
web: { ... } # optional: HTTP control API
cluster: { ... } # optional: mTLS peer attestation / leader election
state: { ... } # optional: durable state store (history, catch-up, retries)
logging: { ... } # optional: Python logging dictConfig| Key | Type | Required | Description |
|---|---|---|---|
defaults |
Map of the per-job common options |
No | Default values inherited by every job in the same file. May contain any per-job option except name, command, and schedule. See Includes, Defaults, and Multi-File Config. |
jobs |
Seq(Map) of job definitions |
No | The list of cron jobs. Each entry is validated against the per-job schema below. |
dags |
Seq(Map) of DAG definitions |
No | Durable orchestration workflows: each DAG is a graph of tasks with dependsOn edges, run on a schedule. Requires a state section with jobApi enabled. See Orchestration and DAGs. |
include |
Seq(Str) |
No | Paths (relative to the including file) of other config files to parse and merge. Include cycles raise a ConfigError. See Includes, Defaults, and Multi-File Config. |
web |
Map |
No | Enables the HTTP control API. See HTTP Control API. |
cluster |
Map |
No | Enables mutual-TLS peer attestation and optional leader election across replicas. See Clustering and Leader Election. |
state |
Map |
No | Enables the opt-in durable state store: restart-durable run history, missed-run catch-up, restart-surviving retries, and once-per-boot @reboot runs. Without it yacron2 is stateless (everything in memory, exactly as before). See Durable State. |
logging |
Map (Python logging.config dictConfig) |
No | Custom logging configuration. See Logging Configuration. |
| Option | Type | Default | Description |
|---|---|---|---|
listen |
Seq(Str) |
required | Listen URLs, e.g. http://127.0.0.1:8080 or unix:///tmp/yacron2.sock. http:// listeners work everywhere; unix:// listeners are not supported on Windows (the Proactor loop lacks create_unix_server) and are skipped with the warning Ignoring web listen url <url>: unix-socket listeners are not supported on this platform. Use an http:// listener instead. See Running on Windows. |
headers |
MapPattern(Str, Str) |
none | Extra HTTP response headers applied to all endpoints. |
authToken |
Map with value / fromFile / fromEnvVar (each EmptyNone() | Str) |
none | Opt-in bearer-token auth. When set but resolving empty, yacron2 refuses to start. |
socketMode |
Str |
none | Octal permissions applied to a unix:// listen socket. Only ever applies to unix sockets, so it is irrelevant on Windows (where unix:// listeners are unsupported). |
metrics |
Bool | Map with enabled / public (each Bool) and durationBuckets (Seq(Float)) |
enabled | The Prometheus GET /metrics endpoint, served by default whenever the web API is on. metrics: false (bool shorthand) disables it; the map form sets enabled (default true), public (default false; exempts only /metrics from authToken), and durationBuckets (histogram bounds in seconds; must be finite, positive, and strictly increasing, else a ConfigError). See Metrics with Prometheus. |
nodeHistory |
Bool | Map with enabled (Bool), interval (Float) and points (Int) |
enabled | Background node CPU/memory sampling for the dashboard's node history chart, served by GET /node/history. On by default whenever the web API is on; nodeHistory: false disables the sampling task. The map form sets interval (seconds between samples, default 5.0, minimum 1.0) and points (ring size, default 720 — the last hour at the default cadence; 10–50000). The ring is in-memory only and follows the web app's lifecycle. |
listen is the only required key. Full behavior, authentication, and endpoint
semantics are documented in HTTP Control API.
Optional. Gates scheduled jobs on a leadership backend so several replicas
can run from one config without double-running jobs. cluster.backend chooses
how: the default gossip backend attests, over mutual TLS, that a static
list of peers is running the same job set and runs a best-effort quorum
election; the kubernetes and etcd backends use a coordination store
(a Lease / a lease-bound key) for a fenced, exactly-once election; the
filesystem backend elects through a fenced TTL lease on a shared POSIX
mount, with no coordination service at all (its safety additionally rests on
synchronized clocks; see its table below). There must be exactly one cluster
block across the whole configuration; a duplicate in an included file or a
second config-directory file raises a ConfigError. Defaults come from
DEFAULT_CLUSTER (plus DEFAULT_K8S / DEFAULT_ETCD / DEFAULT_FILESYSTEM
for the lease backends) and are applied only when a cluster section is
present.
| Option | Type | Default | Description |
|---|---|---|---|
backend |
Enum(["gossip", "kubernetes", "etcd", "filesystem"]) |
gossip |
Which leadership backend gates jobs. gossip (default) is the embedded mTLS best-effort election; kubernetes/etcd/filesystem are fenced lease backends. kubernetes/etcd talk to their store over plain HTTP via the core aiohttp dependency, and filesystem needs only a shared POSIX mount, so none of them adds a runtime dependency. |
Gossip backend (backend: gossip). listen, tls, and peers are
required only for this backend:
| Option | Type | Default | Description |
|---|---|---|---|
listen |
Str |
required (gossip) |
host:port the mTLS /peer listener binds to (e.g. 0.0.0.0:8443). Served only here, never on the public web API. |
tls.ca |
Str |
required (gossip) | Path to the cluster CA (trust anchor for peer certificates). |
tls.cert |
Str |
required (gossip) | Path to this node's certificate (used both to serve /peer and to authenticate as a client). Its SAN must match the host other nodes use to reach it. |
tls.key |
Str |
required (gossip) | Path to this node's private key. |
peers |
Seq(Map({"host": Str})) |
required (gossip) | Every other member as host:port. Cluster size is len(peers) + 1. |
nodeName |
Str |
system hostname | Stable, human-readable identity for this node; the leader is the lowest nodeName among agreeing members. Must be unique across the cluster: a duplicate is detected at runtime (status conflict) and pauses Leader jobs until resolved. The hostname default is already unique per host. (Also used as the lease backends' default identity / etcd key value.) |
interval |
Int |
30 |
Seconds between peer-attestation rounds. Must be > 0. |
driftAfter |
Int |
3 |
Consecutive reachable-but-mismatched rounds before a peer is reported drifted (debounce). Must be >= 1. |
connectTimeout |
Int |
10 |
Seconds per request (also the HTTP timeout for the lease backends). Must be > 0. |
electLeader |
Bool |
false |
When true, only the quorum-gated elected leader runs scheduled jobs (manual API triggers and retries are unaffected). Off by default, so a gossip cluster section is observe-only until opted in. The lease backends imply electLeader: true (configuring one is opting into leadership). |
distribution |
Enum(["single-leader", "spread"]) |
single-leader |
How leader-gated jobs spread across the quorate cluster. single-leader: one elected leader runs every Leader job. spread: per-job ownership via rendezvous hashing, so the work fans out across the quorate nodes (same quorum gate, same guarantee). Inert without electLeader (warns if set anyway). With a lease backend (kubernetes/etcd/filesystem) a non-default distribution is a hard ConfigError at load (a single lease holder cannot be a per-job owner), not a silent fallback. See Clustering and Leader Election. |
Gossip load-time validation (in addition to the numeric ranges above): with
electLeader: true, a 2-node cluster (one peer) is rejected outright with a
ConfigError (a quorum of 2 needs both up, strictly worse than one replica);
an even cluster size greater than 2 is allowed but logs a warning (an
odd count is best for a clean majority).
Kubernetes backend (backend: kubernetes), under cluster.kubernetes. A
coordination.k8s.io/v1 Lease is the fence. Defaults from DEFAULT_K8S:
| Option | Type | Default | Description |
|---|---|---|---|
leaseName |
Str |
yacron2-leader |
Name of the Lease object the replicas contend for. Must be a valid RFC1123 subdomain (lowercase alphanumerics, - and .; <=253 chars), checked at load; it is spliced into the apiserver URL path, so a stray /, ?, #, or space is rejected. |
leaseNamespace |
Str or null |
null → in-cluster namespace | Namespace of the Lease; defaults to the pod's own namespace (the service-account namespace file). When set, must be a valid RFC1123 label (lowercase alphanumerics and -; <=63 chars), checked at load. |
leaseDurationSeconds |
Int |
15 |
How long a renewal keeps the lease valid. Must be > renewDeadlineSeconds. |
renewDeadlineSeconds |
Int |
10 |
Per-round renew/observe deadline: a round that exceeds it is abandoned and retried next round, so a stuck apiserver call cannot run out the full lease. Must be > 0 and < leaseDurationSeconds. |
retryPeriodSeconds |
Int |
2 |
Seconds between renew/observe rounds. Must be > 0 and < renewDeadlineSeconds (a holder must be able to attempt a renew before its own deadline). Additionally, renewDeadlineSeconds + retryPeriodSeconds < leaseDurationSeconds is enforced at load, so the worst-case interval between two successful refreshes still fits inside the lease. |
identity |
Str or null |
null → nodeName
|
The human-readable holder for this node (shown in the dashboard / GET /cluster). yacron2 appends a per-process token to the holderIdentity it actually writes (<identity>#<token>), so two nodes sharing an identity/nodeName still write distinct holders and cannot both believe they hold the Lease. See Node identity. |
kubeconfig |
Str or null |
null → in-cluster | Path to a kubeconfig for out-of-cluster / local testing; otherwise the in-cluster service-account credentials are used. On the hand-rolled HTTP transport a kubeconfig user that relies on an exec credential plugin or an auth-provider raises a ConfigError (those must be executed, which only the native client can do); use clientLibrary: library (yacron2[kubernetes]) or a kubeconfig with a static token / client certificate instead. insecure-skip-tls-verify is honored (the apiserver certificate is not validated) but logs a warning. |
apiServer |
Str or null |
null | Override the apiserver URL (else the in-cluster KUBERNETES_SERVICE_* env or the kubeconfig). When set, must be an https:// URL: a non-https value is a ConfigError at load, since the ServiceAccount bearer token must not travel in cleartext. |
clientLibrary |
Enum(["auto", "http", "library"]) |
auto |
Transport selection. auto uses the official kubernetes client when it is importable (install yacron2[kubernetes]) and otherwise falls back to a hand-rolled apiserver REST transport over aiohttp; library requires the native client (a ConfigError if absent); http forces the hand-rolled transport. |
etcd backend (backend: etcd), under cluster.etcd. A lease-bound key is
the fence; the backend uses etcd's v3 gRPC-gateway JSON/HTTP API directly (no
native client). Defaults from DEFAULT_ETCD:
| Option | Type | Default | Description |
|---|---|---|---|
endpoints |
Seq(Str) |
["http://127.0.0.1:2379"] |
etcd client URLs, tried in order for failover. Each must be http(s)://host[:port]; the port is optional (defaults to the scheme's port, e.g. 443 behind an https ingress) and only an explicitly out-of-range port is rejected. Credentials embedded in the URL are refused. |
electionName |
Str |
yacron2/leader |
The etcd key contended for; its value is the holder's nodeName. There is no separate identity key for etcd (the holder identity is always cluster.nodeName), but leadership is fenced on the bound lease id, not this string, so a duplicate nodeName cannot make two nodes both lead. See Node identity. |
ttl |
Int |
15 |
Lease time-to-live, seconds. Must be >= 3: the leader holds the key only until ttl minus a 1s clock-skew margin, so a smaller ttl would make a fresh winner treat its own lease as already expired (no Leader job would ever run). The keepalive cadence is ~ttl/3 against the effective ttl, which etcd may grant smaller than requested (a smaller granted TTL narrows the fence window). |
username |
Str or null |
null | etcd auth username (omit for an auth-less cluster). Pair it with a resolvable password. The auth token is re-fetched automatically when it expires (re-auth on a 401). |
password |
Map with value / fromFile / fromEnvVar (each EmptyNone() | Str) |
unset | etcd auth password source, resolved like web.authToken from exactly one of value / fromFile / fromEnvVar; a configured-but-empty source fails closed. |
tls.ca / tls.cert / tls.key
|
Str or null |
null | Optional client TLS for https:// endpoints. tls.cert and tls.key are all-or-nothing (a client certificate needs its private key), enforced at load. |
etcd load-time guards: any TLS material (tls.ca/tls.cert/tls.key) requires
at least one https:// endpoint (otherwise it would be silently ignored and
traffic sent in cleartext, a ConfigError). Likewise a username or resolved
password requires every endpoint to be https://, so the credentials and
bearer token are never POSTed in cleartext; a username without a resolvable
password is also rejected.
Filesystem backend (backend: filesystem), under cluster.filesystem. The
flock-guarded, fence-counted TTL lease of the durable state store's filesystem
backend is the fence, taken over a shared POSIX mount (Amazon EFS (NFSv4) / S3
Files) -- no coordination service; the mount is the store. Defaults from
DEFAULT_FILESYSTEM:
| Option | Type | Default | Description |
|---|---|---|---|
path |
Str |
required | Directory the election lease lives in -- normally a shared mount. Must be present and non-empty: a missing, blank, or whitespace-only value is a ConfigError at load (cluster.filesystem.path is required and must be non-empty). Sharing the state store's directory (same mount, same deploymentId) is legal and recommended when both are configured: the namespaces are disjoint, and one mount stays the whole coordination surface. |
electionName |
Str |
cluster/leader |
Name of the lease the replicas contend for. Must be non-empty: a blank or whitespace-only value is a ConfigError at load (cluster.filesystem.electionName must be non-empty). |
ttl |
Int or Float
|
15 |
Lease time-to-live, seconds. Must be >= 3, for the same reason as etcd's floor: the leader holds the lease only until ttl minus a clock-skew margin and renews every max(1s, ttl/3), so a smaller ttl would make a node that wins the election immediately treat its own lease as expired (no Leader job would ever run). |
deploymentId |
Str or null |
null → namespace default
|
Stable namespace prefix inside the store, same semantics as state.deploymentId. Use the same value as state.deploymentId when sharing a mount with the state store. |
topology |
Enum(["auto", "single-node", "shared"]) |
auto |
Whether the mount's locks may be trusted across hosts; same semantics and probe as state.topology. auto probes /proc/mounts for a shared network mount; Windows and macOS cannot probe, so there auto resolves to single-node (the election then only excludes processes on this host, with a startup warning) unless overridden with an explicit shared. |
The lease-backend family rules apply exactly as for kubernetes/etcd:
electLeader is forced on (configuring the backend is opting into
leadership), distribution: spread is a hard ConfigError, a store block
belonging to a different backend (say an etcd: block under
backend: filesystem) is a ConfigError rather than silently ignored, and
gossip-only keys (listen, tls, peers, interval, driftAfter) draw an
emit-once startup advisory.
One filesystem-backend guard is deferred to startup, because it needs the
live mount: start() probes lock fidelity (two descriptors of one file must
actually contend on a non-blocking exclusive lock; on Linux an NFS mount
carrying nolock or local_lock=flock/local_lock=all is additionally
refused, since those honour locks host-locally) and hard-refuses a store
whose locks are no-ops, verbatim
cluster.backend filesystem: refusing to elect over <path>: <reason>. A
refused start leaves the cluster manager unbuilt, so Leader jobs fail
closed -- the safe direction. Both checks run on one host, so on platforms
without /proc/mounts (Windows, macOS) the residual risk rests on the
operator's topology: shared assertion. Unlike the kubernetes/etcd
fences, election safety here also rests on wall clocks (two leaders need
inter-host clock skew above roughly 2 seconds): run NTP on every node, the
same requirement Durable State documents for shared mounts.
Because the cluster schema has many load-time rejections (the ordering rules,
the RFC1123 and https guards, the credential-over-plaintext refusals, and the
lease-family rules above), check a cluster config before deploying with
yacron2 --validate-config, which runs the full load path and prints the
first ConfigError without starting the scheduler. See
Command-Line Reference.
Full behavior, the trust model, quorum math, the lease backends' guarantees,
and per-job clusterPolicy are documented in
Clustering and Leader Election.
cluster.observability shares fleet data — each node's live CPU/memory (see
GET /node) and per-job run summaries — across the cluster
for the dashboard's fleet view,
independent of which backend owns election. It exists because the fleet view
rides node-to-node gossip, which the lease backends do not have: it lets a
kubernetes/etcd/filesystem cluster stand up a second, election-inert
gossip mesh purely to carry that data.
| Option | Type | Default | Description |
|---|---|---|---|
shareNodeStats |
Bool |
true |
Gossip this node's whole-node CPU/memory for the fleet view. Set false to run the overlay mesh for job summaries only. |
listen, tls, peers
|
as backend: gossip
|
— | The overlay gossip transport. Required with a lease backend (the overlay is its own mesh); rejected with backend: gossip (redundant — the election mesh already carries fleet data). |
nodeName, interval, driftAfter, connectTimeout
|
as backend: gossip
|
gossip defaults | Optional overlay mesh tuning. Lease backends only (they tune the dedicated overlay mesh); rejected with backend: gossip, where node stats ride the election mesh and the cluster-level keys of the same names already apply. |
Two shapes:
-
backend: gossip— the election mesh already exchanges/peerbodies, soobservabilityis just an opt-in marker:observability: { shareNodeStats: true }adds node CPU/memory to what that mesh already gossips.listen/tls/peershere are aConfigError(redundant), and so are the overlay tuning keysnodeName/interval/driftAfter/connectTimeout(there is no overlay mesh to tune; the stats gossip atcluster.interval, so set the cluster-level keys instead). The stats ride each/peerresponse as anX-Yacron2-Node-Statsheader, never in the body, so sharing live load keeps the mesh's idle304optimisation intact. -
A lease backend (
kubernetes/etcd/filesystem) — election stays with the lease store;observabilitystands up a dedicated gossip mesh (its ownlisten/tls/peers, all required) that never elects (it holds no leadership and gates no jobs), purely to carry fleet data.
cluster:
backend: kubernetes # election via a coordination.k8s.io Lease
kubernetes:
leaseName: yacron2-leader
observability: # a gossip mesh JUST for the fleet view
listen: "0.0.0.0:8140"
tls: { ca: /tls/ca.pem, cert: /tls/node.pem, key: /tls/node.key }
peers:
- host: node-b:8140
- host: node-c:8140Requires psutil (a core dependency) for
the CPU/memory numbers; a node that cannot read its own load simply shares none.
Node stats are best-effort observability: a malformed or hostile peer payload
degrades to "no data for that node", never poisoning the view.
Optional. Enables the durable state store: restart-durable run history,
missed-run catch-up, restart-surviving retries, once-per-boot @reboot
dedupe, restart-durable Prometheus job counters, and durable output archival.
yacron2 is stateless by default: absent this section everything stays in
memory exactly as before. The store is a directory of immutable JSON records
behind a single filesystem backend -- a local path gives single-node
durability, while a shared Amazon EFS (NFSv4) / S3 Files mount gives the same
durability and coordination fleet-wide (the same code either way; the mount
decides the reach). There must be exactly one state block across the whole
configuration; a duplicate in an included file or a second config-directory
file raises a ConfigError. Defaults come from DEFAULT_STATE and are
applied only when a state section is present. (The
Web Dashboard's browser-side IndexedDB run ledger is a
separate, client-local feature, unrelated to this store.)
| Option | Type | Default | Description |
|---|---|---|---|
path |
Str |
required | Directory the store lives in. A local path gives single-node durability; a shared Amazon EFS (NFSv4) / S3 Files mount gives the same durability fleet-wide. Must be non-empty: a blank or whitespace-only value is a ConfigError at load (state.path is required and must be non-empty). |
topology |
Enum(["auto", "single-node", "shared"]) |
auto |
Whether the store may offer cross-node coordination. auto probes /proc/mounts for a shared network mount (NFS/EFS/S3 Files) and otherwise assumes single-node; Windows and macOS cannot probe, so there auto resolves to single-node unless overridden with shared. |
deploymentId |
Str |
none (namespace default) |
Stable namespace prefix so several deployments can share one store/bucket without colliding or cross-reading. Unset means the default namespace. |
maxRunsPerJob |
Int |
1000 |
Durable finished-run retention per job (the durable analogue of the in-memory history ring); the ledger is pruned to this after each append. <= 0 disables pruning (unbounded; rely on an external lifecycle rule). Durable retention is larger than the in-memory window on purpose. |
onStoreUnavailable |
Enum(["degrade", "fail-closed"]) |
degrade |
What the stateful features do while the store is configured but unavailable (down, unreadable, hung). degrade: durable-truth gates fail open to the in-memory state and failed writes are dropped with a warning (counted in yacron2_state_dropped_writes_total). fail-closed: prefer not running over possibly running wrong -- the onlyIfLastSucceeded gate blocks, a due durable retry defers until the store answers, and an unverifiable @reboot boot marker skips the boot run. Plain scheduled fires are never gated on the store under either policy. |
gcGraceSeconds |
Int |
604800 (7 days) |
Age past which durable state belonging to a job that no recent manifest references (no node's loaded config under this deploymentId has mentioned it for this long) is garbage collected. <= 0 disables automatic GC. Values between 1 and 86399 are a ConfigError at load: a grace below the manifest cadence would make live peers' manifests look stale and collect their state. |
maxOpsPerSecond |
Int or Float
|
0 |
Token-bucket cap on store operations per second (burst of one second's tokens), for request-rate/cost control on mounts that bill per request; throttled ops queue and are counted. 0 disables throttling. Must be >= 0 (a negative value is a ConfigError at load). Lease (coordination) operations bypass the bucket: a lease renew queued behind bulk writes could overshoot its TTL and double-run the very job the lease exists to fence. |
slotTtlSeconds |
Int or Float
|
30 |
TTL, in seconds, of the per-job concurrency slot lease taken for concurrencyScope: cluster jobs; the running holder renews it at a third of this, and a crashed holder's slot frees itself after at most this long. Must be >= 5 (a ConfigError at load, state.slotTtlSeconds must be >= 5): the renew cadence needs headroom, and below ~5s one slow renew on a network mount expires a healthy holder's slot and invites the cross-node double-run the lease exists to fence. |
jobApi |
Map |
(see below) | The job-facing state endpoint: a loopback HTTP server the daemon injects into every job's environment, backing the yacron2 state|cursor|lock|artifact|idempotent|secret commands. A nested block (merged over its defaults, so a partial block keeps the rest). |
The state.jobApi sub-keys:
| Key | Type | Default | Meaning |
|---|---|---|---|
enabled |
Bool |
true |
Run the loopback endpoint and inject its address/token into every job. false keeps the durable scheduler features but exposes nothing to jobs. |
listen |
Str |
(ephemeral) | Override the bind, as an http://host:port URL (a unix:// URL is a ConfigError: the job CLI speaks TCP only). Unset binds an OS-assigned ephemeral port on 127.0.0.1. An explicit port must be an integer in 0-65535 (a ConfigError otherwise; 0 or omitting the port keeps the ephemeral bind), and a non-loopback host is a ConfigError unless allowNonLoopbackBind is also true. |
maxValueBytes |
Int |
1048576 |
Cap (bytes) on one KV / cursor value; a larger set is refused (HTTP 413). Must be >= 0. |
maxArtifactBytes |
Int |
67108864 |
Cap (bytes) on one artifact payload; a larger put is refused (HTTP 413). Must be >= 0. |
lockTtlSeconds |
Int or Float
|
30 |
TTL of a job mutex/semaphore lease, renewed by the daemon at a third of this. Must be >= 5 (a ConfigError, state.jobApi.lockTtlSeconds must be >= 5), for the same reason as slotTtlSeconds. |
allowNonLoopbackBind |
Bool |
false |
Explicit opt-in for a non-loopback listen host. Without it, a non-loopback host is a ConfigError: the endpoint serves per-run bearer tokens and staged job secrets over plaintext HTTP, so exposing it beyond this host needs a deliberate choice (and should be paired with a reverse proxy adding TLS/auth). |
path is the only required key. Full behavior -- the store layout and
durability model, restart-surviving retries, missed-run catch-up, @reboot
dedupe, the SLA trends endpoint, garbage collection, the state metrics, and
the yacron2 state CLI subcommands (backup, restore, migrate, gc, check,
migrate-schema) --
is documented in Durable State. The per-job knobs that build
on this store are under Durable state and catch-up
below.
A list of durable orchestration DAGs. Requires a state section with
jobApi.enabled (the default). Full guide: Orchestration and DAGs.
Per-DAG keys:
| Key | Type | Default | Description |
|---|---|---|---|
name |
Str |
required | Unique DAG name. |
tasks |
Seq(Map) |
required | The task nodes (at least one). |
schedule |
Str or Map
|
none | Same grammar as a job's schedule, except it must parse to a cron expression: @reboot is a ConfigError (DAG schedules must be cron expressions; @reboot is not supported for dags), while @daily/@hourly-style aliases still work. Omit for a manual-only DAG. |
timezone / utc
|
Str / Bool
|
as jobs | Schedule time base, as jobs. |
onMissed |
skip / run-once / run-all
|
skip |
Missed-run catch-up on restart, as jobs. |
startingDeadlineSeconds |
Int |
none | Bound how old a missed run may be to replay. |
catchupJitterSeconds |
Int |
0 |
Spread boot-time catch-up. |
clusterPolicy |
Leader / PreferLeader / EveryNode
|
Leader |
Which node schedules the DAG under leader election. |
enabled |
Bool |
true |
Disable without deleting. |
retainRuns |
Int |
50 |
Keep the newest N terminal runs (must be ≥ 1). |
Per-task keys:
| Key | Type | Default | Description |
|---|---|---|---|
id |
Str |
required | Unique task id within the DAG. |
command |
Str or Seq(Str)
|
required (not for approval) |
The command to run. |
type |
task / sensor / approval
|
task |
Node kind. |
dependsOn |
Seq(Str) |
[] |
Upstream task ids. |
triggerRule |
all_success / all_done
|
all_success |
When the task becomes ready. |
retries |
Int |
0 |
Per-task retry attempts (DAG-owned). Must be >= 0: the job-level -1 retry-forever sentinel is a ConfigError here. |
retryDelaySeconds |
Int/Float
|
0 |
Delay between attempts. |
expand |
Map{fromTask, key} |
none | Dynamic mapping: fan out over an upstream's XCom list (a direct, non-mapped dependency). |
pokeIntervalSeconds |
Int/Float
|
30 |
Sensor: seconds between pokes. |
pokeTimeoutSeconds |
Int/Float
|
3600 |
Sensor: give up after this long. |
pokeJitterSeconds |
Int/Float
|
0 |
Sensor: jitter added to each poke. |
onReject |
fail / skip
|
fail |
Approval gate: what a rejection does. |
Plus the shared launch fields a job takes: shell, environment,
captureStdout / captureStderr, saveLimit, maxLineLength, streamPrefix,
failsWhen, executionTimeout, killTimeout, user / group, env_file,
secrets, stateAllowedScopes.
The graph is validated at load: unknown/duplicate ids, a cycle, a self-edge, or
an expand.fromTask that is not a direct non-mapped dependency are config
errors.
A standard Python logging.config dictionary-schema. version (Int) is
required; incremental, disable_existing_loggers, formatters, filters,
handlers, loggers, and root are optional. See
Logging Configuration.
Every key below comes from _job_schema_dict (jobs) / _job_defaults_common
(defaults and the per-file defaults). Defaults are from DEFAULT_CONFIG.
name, command, and schedule are required on a job; all other keys are
optional. The three keys name, command, and schedule are not allowed
in a defaults block (only the common keys are).
| Option | Type | Default | Description |
|---|---|---|---|
name |
Str |
required | Job identifier. Used in logs, the stream prefix, reports, statsd, and the HTTP API. |
command |
Str or Seq(Str)
|
required | A shell command string (run via shell) or an argv list (run directly, no shell). The shell used for a string command is platform-specific (/bin/sh on POSIX vs cmd.exe via %ComSpec% on Windows when shell is left empty); an argv list bypasses the shell on every platform. See Commands and Environment and Running on Windows. |
schedule |
Str or Map
|
required | A crontab string (5, 6, or 7 fields), the literal @reboot, or a mapping with second, minute, hour, dayOfMonth, month, year, dayOfWeek (each Str, all optional). The mapping is assembled into a crontab: 5 fields normally, 6 when year is set, 7 when second is set (second/year emitted only when used, the rest default to *). A second schedules at second granularity; year restricts to specific years. See Schedules and Timezones. |
shell |
Str |
/bin/sh (POSIX) / empty (Windows) |
Shell used to run a string command. Ignored when command is a list. The default is platform-specific: on POSIX a string command runs as ["/bin/sh", "-c", command]; on Windows the default is empty, which routes a string command through the native command processor %ComSpec% (cmd.exe) via asyncio.create_subprocess_shell. For PowerShell or another interpreter set shell: explicitly, or pass command as a list to bypass the shell entirely (on every platform). The shell field itself works on all OSes. See Running on Windows. |
enabled |
Bool |
true |
When false, the job is parsed and validated but never scheduled or runnable. |
| Option | Type | Default | Description |
|---|---|---|---|
captureStdout |
Bool |
false |
Capture the process's stdout for failure detection and reports. When false, the job's stdout passes through to yacron2's stdout. |
captureStderr |
Bool |
true |
Capture the process's stderr for failure detection and reports. When false, the job's stderr passes through to yacron2's stderr. |
saveLimit |
Int |
4096 |
Maximum number of captured lines retained per stream (split into the first half and the last half; lines in between are discarded and counted). 0 disables retention. |
maxLineLength |
Int |
16777216 (16 MiB) |
Maximum bytes buffered per line by the stream reader. A longer line is skipped with a warning. |
streamPrefix |
Str |
[{job_name} {stream_name}] |
Format string prefixed to every emitted output line. Supports {job_name} and {stream_name} placeholders; set to "" to disable. |
See Output Capturing for buffering, truncation, and the captured-output handoff to reporters.
| Option | Type | Default | Description |
|---|---|---|---|
utc |
Bool |
true |
When true, the schedule is interpreted in UTC. When false (and no timezone), local time is used. |
timezone |
Str |
none | IANA timezone name (e.g. America/Los_Angeles) overriding utc. An unknown name raises a ConfigError. |
| Option | Type | Default | Description |
|---|---|---|---|
concurrencyPolicy |
Enum(["Allow", "Forbid", "Replace"]) |
Allow |
Behavior when a scheduled run overlaps a still-running instance. Allow: run concurrently. Forbid: skip the new run. Replace: cancel the running instance and start the new one. |
concurrencyScope |
Enum(["node", "cluster"]) |
node |
How far concurrencyPolicy reaches. node (default): an overlap is another instance in this yacron2 process, exactly as before. cluster: Forbid/Replace also exclude instances of the job on other nodes sharing the state store, via a per-job TTL slot lease (slots/<job name>) in the state store -- it works with or without a cluster section. Requires a state section somewhere in the assembled config: without one, load fails with a ConfigError naming the offending job(s). cluster with concurrencyPolicy: Allow is likewise a ConfigError at load (Allow places no bound on concurrent instances, so widening its scope gates nothing). Enforcement is best-effort at-least-once, not exactly-once; state.slotTtlSeconds and state.onStoreUnavailable govern the edges. Part of the job-set id only when set to cluster (existing configs keep their digests; replicas disagreeing on it show as drift). See Concurrency and Timeouts. |
clusterPolicy |
Enum(["Leader", "PreferLeader", "EveryNode"]) |
Leader |
Where this job runs under cluster leader election. Inert unless cluster.electLeader is set (without election every job runs on every instance). Leader: only the quorum-gated leader runs it (at-most-once; may skip). PreferLeader: the lowest reachable agreeing node runs it, ignoring quorum (never skips; may double-run across a partition). EveryNode: every node runs it, independent of cluster health. Part of the job-set id. See Clustering and Leader Election. |
executionTimeout |
Float |
none | Seconds after which a still-running process is terminated. Unset means no timeout. Must be > 0 when set. The "terminated" action differs by platform (graceful SIGTERM->SIGKILL escalation on POSIX vs an immediate TerminateProcess on Windows); see killTimeout below and Running on Windows. |
killTimeout |
Float |
30 |
Seconds to wait after SIGTERM before sending SIGKILL when terminating a job. Must be >= 0. The SIGTERM-then-SIGKILL escalation is POSIX-specific: there terminate() sends SIGTERM (graceful, trappable) and kill() sends SIGKILL, a real escalation. On Windows there are no POSIX signals, so both terminate() and kill() call TerminateProcess (an immediate, ungraceful stop that does not notify the child), so the escalation is effectively moot; killTimeout still bounds the wait but the outcome is the same hard kill. See Running on Windows. |
failsWhen determines when a completed run is treated as a failure. In the
strictyaml schema only producesStdout is a required key inside failsWhen;
the others are optional. The DEFAULT_CONFIG defaults are:
| Option | Type | Default | Description |
|---|---|---|---|
failsWhen.producesStdout |
Bool |
false |
Fail if any stdout was captured. |
failsWhen.producesStderr |
Bool |
true |
Fail if any stderr was captured. |
failsWhen.nonzeroReturn |
Bool |
true |
Fail if the process exits with a non-zero return code. |
failsWhen.always |
Bool |
false |
Fail whenever the process exits, regardless of output or return code. |
jobs:
- name: example
command: echo "hi"
schedule: "* * * * *"
failsWhen:
producesStdout: false
producesStderr: true
nonzeroReturn: true
always: falseSee Failure Detection and Retries.
Three lifecycle hooks each carry a report block (mail, sentry, shell,
webhook); onFailure additionally carries a retry block. The report blocks
all share the same _report_schema and the same _REPORT_DEFAULTS (deep-copied
so the three blocks do not alias one another).
| Option | Type | Default | Description |
|---|---|---|---|
onFailure.retry.maximumRetries |
Int |
0 |
Max retry attempts after a failure. 0 disables retries; -1 retries forever. Must be >= -1. |
onFailure.retry.initialDelay |
Float |
1 |
Seconds before the first retry. Must be >= 0. |
onFailure.retry.maximumDelay |
Float |
300 |
Upper bound on the backoff delay. Must be > 0. |
onFailure.retry.backoffMultiplier |
Float |
2 |
Multiplier applied to the delay between retries (exponential backoff). Must be > 0. |
onFailure.report |
_report_schema (mail/sentry/shell/webhook) |
defaults below | Reporters fired on every detected failure (including each failed attempt). |
onPermanentFailure.report |
_report_schema |
defaults below | Reporters fired only after all retries are exhausted. |
onSuccess.report |
_report_schema |
defaults below | Reporters fired on a successful run. |
Inside onFailure.retry, all four keys (maximumRetries, initialDelay,
maximumDelay, backoffMultiplier) are required by the strictyaml schema once
a retry block is present. See
Failure Detection and Retries.
The report blocks are covered in full on Reporting (Mail, Sentry, Shell, Webhook);
their schema and _REPORT_DEFAULTS are summarized here.
from and to are required keys (each EmptyNone() | Str). Defaults from
_REPORT_DEFAULTS["mail"]:
| Option | Type | Default | Description |
|---|---|---|---|
from |
EmptyNone() | Str |
None |
Sender address (required key). |
to |
EmptyNone() | Str |
None |
Recipient address(es) (required key). |
smtpHost |
Str |
None |
SMTP server host. |
smtpPort |
Int |
25 |
SMTP server port. |
subject |
Str |
jinja2 default subject template | Email subject (jinja2). |
body |
Str |
jinja2 default body template | Email body (jinja2). |
username |
Str |
None |
SMTP login username (enables login with password). |
password |
Map with value/fromFile/fromEnvVar (each EmptyNone() | Str) |
all None
|
SMTP login password source. |
tls |
Bool |
false |
Use implicit TLS. |
starttls |
Bool |
false |
Use STARTTLS. |
validate_certs |
Bool |
true |
Validate TLS certificates. Defaults to true in yacron2 (a breaking change from upstream). |
html |
Bool |
false |
Send the body as HTML. |
Defaults from _REPORT_DEFAULTS["sentry"]:
| Option | Type | Default | Description |
|---|---|---|---|
dsn |
Map with value/fromFile/fromEnvVar (each EmptyNone() | Str) |
all None
|
Sentry DSN source. |
fingerprint |
Seq(Str) |
["yacron2", "{{ environment.HOSTNAME }}", "{{ name }}"] |
Issue-grouping fingerprint (jinja2 per entry). Replaces, never appends, on merge. |
level |
Str |
unset (effective error) |
Sentry event level. When unset, events are captured at level error. |
extra |
MapPattern(Str, Str | Int | Bool) |
unset | Extra structured context. |
body |
Str |
default subject + body templates | Event message (jinja2). |
environment |
Str |
None |
Sentry environment. |
maxStringLength |
Int |
8192 |
Max string length before Sentry truncation. |
| Option | Type | Default | Description |
|---|---|---|---|
shell |
Str |
/bin/sh (POSIX) / empty (Windows) |
Shell used to run the reporter command. The default is platform-specific, same as the per-job shell field: on Windows the default is empty (the reporter command runs via cmd.exe through %ComSpec%). Set shell: explicitly for another interpreter, or pass command as a list. See Running on Windows. |
command |
Str or Seq(Str)
|
None |
Reporter command (required key). Receives YACRON2_* environment variables. |
Defaults from _REPORT_DEFAULTS["webhook"]:
| Option | Type | Default | Description |
|---|---|---|---|
url |
Map with value/fromFile/fromEnvVar (each EmptyNone() | Str) |
all None
|
Webhook URL source (treated as a secret; never logged). No URL means webhook reporting is disabled. |
method |
Str |
POST |
HTTP method. |
contentType |
Str |
application/json |
Content-Type header value. |
headers |
MapPattern(Str, Str) |
{} |
Extra request headers, sent verbatim (not templated). |
body |
Str |
default webhook body template | Request body (jinja2). The default is a Slack-compatible {"text": ...} JSON payload of the default subject + body text. |
timeout |
Float |
10 |
Total request timeout, seconds. |
These options build on the top-level state store and only take
full effect when a state section is configured; without one they parse and
validate normally but change nothing. The one exception is
onlyIfLastSucceeded, which also works without a state section from the
in-memory history alone (the gate then resets on restart). See
Durable State for the full semantics.
| Option | Type | Default | Description |
|---|---|---|---|
onMissed |
Enum(["skip", "run-once", "run-all"]) |
skip |
Missed-run catch-up after downtime, computed from the durable last-run watermark. skip (classic behavior): occurrences missed while down are not run. run-once: fire once at boot, coalescing all missed slots. run-all: replay each missed occurrence. Inert without a state section. |
startingDeadlineSeconds |
Int or null |
none | Only occurrences missed within this many seconds are caught up; unset means no deadline. Bounds run-all to a recent window so a long outage cannot stampede (like the Kubernetes CronJob field of the same name). Also invalidates a persisted retry ladder older than the deadline. Must be > 0 when set. Only meaningful with a state section. |
catchupJitterSeconds |
Int |
0 |
Spread the boot-time catch-up launches of different jobs over [0, N) seconds, deterministic per job name, so a fleet of jobs does not all fire at once on restart. 0 fires them together. Must be >= 0. Only meaningful with a state section. |
onlyIfLastSucceeded |
Bool |
false |
Depends-on-past gate: skip a scheduled fire when the job's most recent finished run did not succeed, or when a previous instance is still running (unless concurrencyPolicy: Replace). The last real outcome is the newest of the in-memory history and the durable run ledger; cancelled and skipped runs are ignored; retries, catch-up backfills, and manual API triggers deliberately bypass the gate. Works without a state section from the in-memory history alone (resetting on restart); with one, the gate's memory survives restarts. |
archiveOutput |
Bool |
false |
Persist each finished run's captured output durably to the state store (the job's logs/ stream). Encryption at rest is the mount's job (EFS/S3 SSE, an encrypted volume). Inert without a state section (a startup warning notes it archives nothing). |
redactArchivedSecrets |
Bool |
true |
Scrub recognisable secrets (tokens, passwords, keys, auth URLs) from archived output before it is written. Applies only when archiveOutput is set, so it too has no effect without a state section. |
| Option | Type | Default | Description |
|---|---|---|---|
environment |
Seq(Map({"key": Str, "value": Str})) |
[] |
Environment variables set for the process. Both key and value are required per entry. Merged by key with defaults and with env_file (config values win). |
env_file |
Str |
none | Path to a KEY=VALUE file; blank lines and # comments are ignored. Variables in environment override file values. A read error or a line without = raises a ConfigError. |
secrets |
Seq(Map({"name": Str, "value"/"fromFile"/"fromEnvVar": Str})) |
[] |
Run-scoped secrets staged for the job over the job-facing state endpoint rather than placed in the environment, so they never show in /proc/<pid>/environ. Each needs a name and exactly one source (a nameless or sourceless entry is a ConfigError; a same-named entry merges last-wins, like environment). The job reads one with yacron2 secret get NAME. Requires a state section with jobApi enabled, else load fails naming the offending job(s). |
stateAllowedScopes |
Seq(Str) |
[] |
Extra scope names (besides the job's own name and global) this job's yacron2 state|cursor|lock|artifact calls may explicitly name via --scope. Naming any other scope -- most dangerously another job's own name, which IS that job's private scope -- is refused (403). See Scopes. |
jobs:
- name: example
command: env
schedule: "* * * * *"
env_file: .env
environment:
- key: PATH
value: /bin:/usr/bin| Option | Type | Default | Description |
|---|---|---|---|
user |
Str or Int
|
none | User name or numeric uid the process runs as. A numeric uid derives its primary gid and login name from the passwd database. An unknown name raises a ConfigError. |
group |
Str or Int
|
none | Group name or numeric gid the process runs as. If only user is set, the group defaults to that user's primary group. An unknown name raises a ConfigError. |
This section is POSIX-only (the setuid/setgid model). On POSIX, setting user
or group requires yacron2 to run as root (euid 0); otherwise a ConfigError
is raised. Privilege switching is not supported on Windows: a job with
user or group set raises a configuration error, verbatim
Job <name>: changing user/group is not supported on Windows. See
Production and Container Deployment and
Running on Windows.
| Option | Type | Default | Description |
|---|---|---|---|
statsd |
Map({"prefix": Str, "host": Str, "port": Int}) |
none | When set, emit start/stop/success/duration metrics over UDP. All three keys are required. |
monitorResources |
Bool or Map
|
false |
Sample each run's CPU time and peak resident memory (RSS) by polling the job's process tree while it runs. Observability only: the numbers ride the run record into the dashboard, GET /metrics and statsd, but never change a run's success/failure verdict. Off by default; a per-instance sampling task is spawned only when it is on. Set it under defaults: to enable it fleet-wide. The map form tunes it: enabled (Bool, default true), interval (Float seconds between samples, default 1.0), history (Int chart-series points kept per run, default 240; 0 keeps summary numbers only). |
See Metrics with statsd. Prometheus metrics are not
configured per job: the GET /metrics endpoint is global, tuned under
web.metrics in the web section above. See
Metrics with Prometheus.
Resource accounting (monitorResources). With it on, a run is sampled by
psutil (a core dependency) over its
whole process tree, so a job that shells out to child processes is accounted
too. The result (total user/system CPU seconds and the peak RSS observed)
appears in the dashboard run history and stats, in the durable run record's
resources object (so it survives a restart), and as the metrics listed in
Metrics with Prometheus. Report templates and the
shell reporter also receive cpu_seconds / max_rss_bytes
(YACRON2_CPU_SECONDS / YACRON2_MAX_RSS_BYTES).
DAG tasks accept monitorResources too, but surface the result differently:
a finished task instance's usage is recorded in the resources object of its
task record inside the durable dag_run document (returned by
GET /dags/<name>/runs/<run_key>), and sent to the task's statsd sink when
one is configured. DAG task instances are ephemeral and do not appear in the
per-job Prometheus families on GET /metrics.
The numbers are sampled, so a run that finishes between two samples is measured approximately; the long, heavy runs whose resource use actually matters are sampled many times. It is best-effort: if psutil cannot read a process (already exited, permission denied) the run simply carries no resource stats, and monitoring never fails a job.
Sampling cadence and chart series. The map form controls how monitoring behaves:
monitorResources:
interval: 0.5 # seconds between samples (default 1.0, minimum 0.1)
history: 240 # chart points kept per run (default 240; 0 = summary only)interval sets the process-tree polling cadence: shorter intervals catch
sharper RSS spikes at the cost of more wakeups (each sample walks the process
table, hence the 0.1s floor). Alongside the summary numbers, each monitored
run records a downsampled CPU%/RSS time series for the dashboard's
Resources tab: one point per sample until history points accumulate, after
which adjacent points merge (mean CPU%, peak RSS — spikes are never averaged
away) and the effective resolution halves, so a run of any length stays within
history points (at most 2000). The series is embedded in the durable run
record's resources.series, so charts survive restarts and are bounded by the
same state.maxRunsPerJob retention as run records; it is served by
GET /jobs/<name>/resources and deliberately excluded from the polled
/jobs and /jobs/<name>/runs payloads.
The dashboard's node-level CPU/memory history chart is configured separately,
under web.nodeHistory (see the web section): interval (Float seconds,
default 5.0, minimum 1.0) and points (Int ring size, default 720 —
an hour at the default cadence), or nodeHistory: false to disable the
background node sampler entirely.
strictyaml enforces only the type (Int/Float). After type validation,
JobConfig._validate_numeric_ranges enforces value ranges (plus one
cross-field rule) and raises a ConfigError (prefixed Job <name>:) on
violation. These checks run at load time, not at run time. New in the yacron2
fork.
| Rule | Condition |
|---|---|
saveLimit >= 0 |
always |
maxLineLength > 0 |
always |
killTimeout >= 0 |
always |
concurrencyPolicy is Forbid or Replace
|
only when concurrencyScope: cluster (widening Allow gates nothing, so it is rejected rather than ignored) |
executionTimeout > 0 |
only when executionTimeout is set |
catchupJitterSeconds >= 0 |
always |
startingDeadlineSeconds > 0 |
only when startingDeadlineSeconds is set |
onFailure.retry.maximumRetries >= -1 |
only when a retry block is present |
onFailure.retry.initialDelay >= 0 |
only when a retry block is present |
onFailure.retry.maximumDelay > 0 |
only when a retry block is present |
onFailure.retry.backoffMultiplier > 0 |
only when a retry block is present |
monitorResources.interval >= 0.1 |
always (a sub-100ms cadence would busy-loop the process-table walk) |
0 <= monitorResources.history <= 2000 |
always (bounds what one run adds to a durable ledger record) |
defaults:
shell: /bin/bash # POSIX path; on Windows omit shell (uses cmd.exe) or set a Windows interpreter
utc: false
jobs:
- name: nightly-backup
command: /usr/local/bin/backup.sh
schedule: "0 3 * * *"
captureStdout: true
captureStderr: true
executionTimeout: 3600
onFailure:
retry:
maximumRetries: 3
initialDelay: 5
maximumDelay: 60
backoffMultiplier: 2This wiki documents yacron2. See the README and the changelog.
yacron2 is a fork of gjcarneiro/yacron.
- Getting Started
- Configuration
- Job Behavior
- Integrations
- Reference and Development