CLI Reference - ptweezy/cronstable GitHub Wiki
This page documents the cronstable command and every argument it accepts, the
cronstable state administration subcommands, the job-facing state commands a
running job uses (state get|set|delete|keys, cursor, lock, artifact,
idempotent, secret, xcom), the mcp, tui, and pair client
subcommands, the runtime model (foreground execution, signal handling, exit
codes), and common invocations. Behavior is taken from
cronstable/__main__.py, cronstable/state_admin.py, cronstable/jobcli.py,
cronstable/mcpcli.py, cronstable/tui.py, and cronstable/paircli.py.
cronstable [-c FILE-OR-DIR] [-l LEVEL] [-v] [--job-set-id] [--version]
[--third-party-licenses] [--sealable-suites]
cronstable init [DIRECTORY]
cronstable state ACTION [options] [-c FILE-OR-DIR]
cronstable state get|set|delete|keys ... [--scope NAME | --global]
cronstable cursor|lock|artifact|idempotent ... [--scope NAME | --global]
cronstable secret get|list ...
cronstable xcom push|pull|list ...
cronstable mcp [--url URL] [--token TOKEN] [--token-env VAR] [--cacert PATH]
[--client-cert PATH --client-key PATH] [--insecure] [--check]
cronstable tui [--url URL] [--token TOKEN] [--token-env VAR] [--cacert PATH]
[--client-cert PATH --client-key PATH] [--insecure] [options]
cronstable pair [--url URL] [--token TOKEN] [--token-env VAR] [--cacert PATH]
[--client-cert PATH --client-key PATH] [--insecure]
[--public-url URL] [--name NAME] [--format qr|link|json]
cronstable service install|remove|start|stop|reload|status|run [options]
cronstable import-taskscheduler PATH... [-o FILE] [--timezone NAME]
Without a subcommand, cronstable is the scheduler daemon described later.
When invoked with no arguments and the default configuration path is absent,
it prints setup guidance and usage help to stdout and exits 0.
With the state subcommand it is an offline administration tool for the
durable state store; see the state subcommand.
The state get|set|delete|keys, cursor, lock, artifact, idempotent,
secret, and xcom commands are a different surface: a running job uses
them to reach the daemon's store through its loopback endpoint. See
job-facing state commands.
The mcp, tui, and pair subcommands are clients of a running daemon's web
listener: the MCP stdio bridge, the terminal dashboard, and the command that
prints the iOS app's pairing QR code. See the
mcp subcommand, the
tui subcommand, and the
pair subcommand.
cronstable runs as a single foreground process. It does not daemonize, does not
fork, and does not write a PID file. Diagnostics go to stdout/stderr through
the standard library logging module.
To run it as a service, place it under a process supervisor: systemd or a
container runtime on POSIX (see
production and container deployment), Task Scheduler or
a service wrapper on Windows (see
running unattended in
running on Windows). To stop a supervised daemon
gracefully, send SIGTERM on POSIX or call the authenticated POST /shutdown
route on any platform (see the HTTP control API).
| Option | Type | Default | Description |
|---|---|---|---|
-c, --config
|
path (file or directory) | platform default[^cfgdefault] | Configuration file, or a directory containing configuration files. From a directory, cronstable loads every *.yml/*.yaml file and every classic crontab (*.crontab, *.cron, or a file named crontab), skipping entries whose name starts with _ or .. See includes, defaults, and multi-file config and classic crontabs. |
-l, --log-level
|
string | INFO |
Root log level: DEBUG, INFO, WARNING, ERROR, or CRITICAL, in upper or lower case. logging module aliases such as WARN and FATAL also resolve. An unknown name is a usage error (exit 2). |
-v, --validate-config
|
flag | off | Parse and validate the configuration, then exit: 0 if valid, 1 on a configuration error. Does not start the scheduler or web server. |
--job-set-id |
flag | off | Parse the configuration, print the job-set ID to stdout, and exit 0. The id hashes selected fields in each job's effective configuration, independently of job order. Exits 1 on a configuration error. |
--version |
flag | off | Print the cronstable version to stdout and exit 0. |
--third-party-licenses |
flag | off | Print the bundled third-party license notices (the LGPL notice for python-zeroconf) and exit 0. |
--sealable-suites |
flag | off | Print each supported push encryption suite, one per line, verified by a test encryption, and exit 0. |
-h, --help
|
flag | — | Print usage (argparse builtin) and exit 0. |
The other command-line surfaces are the
state subcommand described later, which administers
the durable state store; the
job-facing state commands a running job uses; and
the client subcommands mcp,
tui, and pair, which talk to
a running daemon's web listener.
Job schedules, commands, environment, reporting, and the web API are
configured entirely in YAML, not on the command line; see the
configuration reference.
[^cfgdefault]: The default configuration path is platform-specific
(DEFAULT_CONFIG_PATH in cronstable/platform.py): /etc/cronstable.d on
POSIX; on Windows the machine-wide %ProgramData%\cronstable when that
directory holds configuration, otherwise the per-user %APPDATA%\cronstable
(for example, C:\Users\<you>\AppData\Roaming\cronstable, falling back to
the user profile ~ if APPDATA is unset). See
running on Windows.
The argument may be a single file or a directory:
-
File: parsed directly, YAML by default. It is parsed as a classic
crontab when the name says so (
*.crontab,*.cron, or a file namedcrontab, such as acrontab -l > crontabexport), or, for a file with a neutral name such as-c /var/spool/cron/crontabs/root, when the content unmistakably is one. See classic crontabs; the six-field system crontab format of/etc/crontabis not supported. An I/O error (for example, the file does not exist) is reported as a configuration error and exits1. -
Directory: each non-hidden
*.yml/*.yamlor crontab-named entry is parsed in name-sorted order. An empty directory (or one whose files are all skipped) yields an empty configuration with no jobs rather than an error.
With no arguments and no default configuration path, cronstable prints the
resolved path, instructions to run cronstable init, and usage help to stdout,
then exits 0. It creates no files and starts no scheduler. If the default
path exists, a bare cronstable loads it and runs the scheduler; configuration
errors exit 1.
Explicit configuration requests require an existing path. If you pass
--validate-config, --job-set-id, -l, or -c and the resolved path equals
the missing platform default, cronstable prints the following to stderr,
prints the usage help, and exits 1:
cronstable error: configuration file not found at the default location (<default path>). Run `cronstable init` to create a starter configuration there, or point -c/--config at an existing file or directory.
Any other missing path passed with -c logs Configuration error: ... and
exits 1.
cronstable init [DIRECTORY] writes a commented starter configuration,
cronstable.yaml, into DIRECTORY, creating the directory when needed. The
file holds one working job plus the most commonly wanted next steps, left
commented out.
With no argument it targets a root-level -c/--config if you gave one, so
cronstable -c D:\jobs init writes to D:\jobs, the same flag the earlier
not-found error points you at. Otherwise it targets the platform default
configuration path, which makes cronstable init followed by cronstable a
working first run. Naming both a DIRECTORY and a different -c writes to
DIRECTORY and says so on stderr.
It never touches an existing setup. cronstable init refuses a target that is
a file, already holds *.yaml/*.yml/crontab configuration files, already has
a cronstable.yaml, or cannot be read or written, giving the reason and exit
1. Success prints the written path and the command to start the scheduler,
and exits 0.
The log level is applied with logging.basicConfig before the configuration is
loaded, so it governs cronstable's own startup and runtime logging. The value is
upper-cased and resolved against the standard level names. DEBUG, INFO,
WARNING, ERROR, and CRITICAL work in upper or lower case, as do the
aliases the logging module defines (WARN, FATAL). If the level is
misspelled, cronstable exits 2 with a usage error naming the valid choices.
A logging: section in the configuration can reconfigure logging after startup
through logging.config.dictConfig; see
logging configuration.
Validation parses and schema-checks every file (parse_config_with_sources,
the same parse the scheduler runs) without constructing the scheduler. On
success it logs Configuration is valid. and exits 0. On any ConfigError
(schema violation, unknown time zone, invalid numeric range, missing user/group,
include cycle, multiple web/logging sections, and other faults) it logs
Configuration error: <detail> and exits 1. The scheduler loop and web
server never start in this mode.
The earlier default-path special case still applies: the check runs before
the configuration is parsed, so validating while the configuration argument
equals the platform default (DEFAULT_CONFIG_PATH) and that path is absent
exits 1 with the not-found message rather than the
Configuration error: ... message.
Parses and schema-checks the resolved configuration exactly like
--validate-config, without constructing the scheduler, then prints the
job-set ID to stdout and exits 0. The id
is an order-independent hash of selected fields in each job's effective
configuration. Matching values for those fields produce the same id regardless
of file order or how the jobs are split across files. It describes configured
jobs, including those that are not currently executing.
The GET /job-set-id endpoint serves the same value, and cluster
peers compare it (see
clustering and leader election). The
job-set ID page has the full treatment: what the hash covers,
what it excludes, and why.
Because the configuration is fully parsed first, a configuration error exits
1, and the default-path special case applies
exactly as it does for --validate-config.
Prints the version string (for example, 1.2.60) to stdout and exits 0. This
check runs before the configuration is touched, so --version succeeds even
when no configuration exists.
cronstable state ACTION [options] [-c FILE-OR-DIR]
cronstable state administers the durable state store defined by the
configuration's state: section. That is the daemon-side store on disk or on a
shared mount, not the web dashboard's browser-side IndexedDB
run ledger, a separate, purely client-side feature. Every action works offline,
straight from the configuration, with no running daemon required.
Actions that read or copy out of the store (backup, check,
migrate-schema) and the gc pass stay safe against a running daemon,
because records are immutable and copies/reads never lock. A backup taken
mid-write is an approximate point-in-time snapshot rather than an exact one.
Restoring or migrating into a store a daemon is actively using is not
safe (see state restore / state migrate).
Each action accepts its own -c/--config, with the same meaning and default
as the daemon flag, so both positions work: cronstable -c /etc/cronstable.d state gc
and cronstable state gc -c /etc/cronstable.d are equivalent. (-c between state
and the action name is not accepted.) If the resolved configuration has no
state: section, or cannot be read, the action prints
cronstable state error: <detail> to stderr and exits 1. The
default-path special case does not apply here.
| Action | Description |
|---|---|
backup |
Write a .tar.gz backup of the store. |
restore |
Restore a backup into the store. |
migrate |
Copy the store to another path or mount (local disk <-> S3 Files / EFS). |
gc |
Garbage-collect state of unreferenced jobs. |
check |
Verify the store is usable and print an inventory. |
migrate-schema |
Rewrite records of older known record schemes. |
cronstable state backup -o FILE.tar.gz [-c FILE-OR-DIR]
Writes a gzipped tar of the store's namespace to -o/--output (required).
The archive carries the full store: the immutable records (records/), the
mutable documents (docs/: KV entries, cursors, idempotency claims, and
dag_run documents), the content-addressed artifact payloads (blobs/), and the
lease files (leases/). A lease file is the only home of its fence counter, so
dropping it would re-issue fence values.
Deliberately not carried: tmp/ (transient write debris) and quarantine/
(poison records; forensics stay with the source store). The archive is created
owner-only (mode 0600): it flattens captured job output, KV values, and
artifact payloads into a single file.
Against a live daemon, state backup skips a file that disappears mid-backup
(a prune, a lease rewrite), by design. Exits 1 when the store directory does
not exist (nothing to back up).
cronstable state restore FILE.tar.gz [--force] [-c FILE-OR-DIR]
Extracts a backup archive into the configured store. It refuses to restore
into a store that already contains data and exits 1; pass --force to
merge the archive into it. Restoring is not safe while a daemon uses the
store. Stop the daemon first.
Archive members are sanitized: only plain files that extract strictly inside
the store are honored (no absolute paths, no .. escapes, no symlinks or
devices). Each file lands with mode 0600 through a temp sibling plus atomic
replace, so a concurrent reader never sees a torn record.
When merging into a populated store, .lock side-files are skipped (a live
daemon may hold an OS lock on that very inode). A lease file replaces the
current one only when its archived fence counter is provably not older (a
fence-max merge). Regressing a fence would re-issue fence values already handed
out. The kept-lease count is reported.
cronstable state migrate --dest PATH [--dest-deployment-id ID] [--force]
[-c FILE-OR-DIR]
Copies the store to another path or mount. A local directory and an Amazon
S3 Files / EFS mount share one on-disk layout, so migration in either
direction is a faithful file copy. --dest (required) is the destination
state.path; --dest-deployment-id selects a different namespace at the
destination (default: keep the current one).
Each file lands through a temp sibling plus atomic rename, so a reader of the destination never observes a torn record. That matters when cutting over to a shared mount that other nodes already watch.
Refused with exit 1: migrating a store onto (or into) itself, and a
destination namespace that already holds records or leases unless --force is
given. Overwriting a live destination's lease files would regress their fence
counters under any daemon already using that store.
After a successful copy, point state.path (and deploymentId, if you
changed it) at the new location to cut over.
cronstable state gc [--dry-run] [-c FILE-OR-DIR]
Runs one manual garbage-collection pass with the same rules as the daemon's automatic periodic pass, removing:
- The streams of jobs (and artifact scopes) that no recent manifest references
and whose newest record is older than
state.gcGraceSeconds. - Counter and manifest streams of unmanifested hosts.
- Provably dead per-run DAG advance leases, the only lease class ever deleted.
- Orphaned lock side-files idle past the grace.
- Crashed write-temp files.
- Quarantined records older than the grace.
It then sweeps artifact payload blobs no surviving record references.
It prints what was removed (or, with --dry-run, what would be), the
kept-stream count, and either the reclaimed orphan-blob count or, if the blob
sweep did not run, the reason: an unenumerable artifact stream or an
unreadable record keeps every blob.
Run documents of removed DAGs are left to the running daemon's own pass, which
alone knows what it owns. Like the automatic pass, it defers (exit 0, with a
message) until the store's manifest history spans one full grace window: a
store that cannot yet prove absence deletes nothing. When GC is disabled
(gcGraceSeconds <= 0), the command reports that there is nothing to collect
and exits 1.
cronstable state check [-c FILE-OR-DIR]
Verifies the store is usable (starting the backend probes writability) and
prints an inventory: the store path, backend, namespace, topology,
shared-locking mode, the number of streams and records (broken down by stream
prefix, such as runs, logs, retries), and the quarantined-record count.
If the store cannot be started or probed, the command exits 1.
cronstable state migrate-schema [--dry-run] [-c FILE-OR-DIR]
Rewrites records written under older known record-scheme versions to the
current one, and reports how many records were converted, already current,
unknown, unreadable, or failed. v1 is the only scheme so far, so today this
reports and converts nothing; it becomes useful only after a future scheme
bump. Records with unknown versions are left in place. The daemon leaves
such a record in place too: a read warns and skips it, and a
strict read fails on it. --dry-run counts
without rewriting.
Every action exits 0 on success and 1 on any error: a missing or invalid
configuration, no state: section, an I/O failure, or a refusal (restoring
into a nonempty store without --force, migrating a store onto itself, GC
with gcGraceSeconds disabled). Error and refusal messages go to stderr;
configuration and I/O errors print as cronstable state error: <detail>.
Success summaries and inventories stay on stdout, so piped output is clean.
cronstable state with no action prints a pointer to cronstable state --help and
exits 2, the same code argparse itself uses for usage errors (an unknown
option, or a missing required one such as backup without -o).
Alongside the earlier offline state admin actions, a running job's command
line reaches the daemon's durable store through the job-facing commands
state get|set|delete|keys, cursor, lock, artifact, idempotent,
secret, and xcom.
These thin clients of the
loopback state endpoint read
the injected CRONSTABLE_STATE_URL / CRONSTABLE_STATE_TOKEN and speak HTTP
over the standard library (no aiohttp, no event loop), so they start instantly
and need no configuration file. They use no proxy and follow no redirect,
because each request carries the run's token. Behavior comes from
cronstable/jobcli.py.
The state job actions share the cronstable state name with the
admin actions; the action name selects the handler,
and the job commands take no -c. Outside a job the injected environment is
absent and every command exits 1 with cronstable <command>: job state connection is unavailable: CRONSTABLE_STATE_URL is not set. ... (xcom
reports that the workflow task context is unavailable).
This section keeps to each command's synopsis, flags, and exit codes. The
semantics live in durable state, which also
covers the enabling state.jobApi configuration.
Every KV, cursor, artifact, lock, and idempotency command acts in a scope, a
namespace defaulting to the calling job's own name, and takes two mutually
exclusive override flags: --scope NAME (act in the named scope) and
--global (act in the shared global scope, for deliberate cross-job
coordination). secret and xcom take neither flag: a run's secrets are
always its own, and the daemon injects the DAG run's XCom scope.
The commands share one exit-code convention, made for shell branching:
| Code | Meaning |
|---|---|
0 |
Success (or, for idempotent, the claim was fresh). |
1 |
An error (a transport or store failure). |
2 |
Usage error (argparse; such as a command invoked with no action). |
3 |
A lock acquire / lock run did not get the lock. |
4 |
The looked-up key, cursor, artifact, secret, or XCom key does not exist (state delete of an absent key too). |
5 |
The idempotent key was already claimed (a duplicate). |
The commands count only a 2xx reply from the state endpoint as success. For
any other status, a command prints one line to stderr, prints nothing to
stdout, and exits 1. That includes a redirect status with no Location
header, such as a 302 from a proxy at CRONSTABLE_STATE_URL, which the
command reports as the state endpoint returned HTTP 302. Two cases differ:
the 404 for a get or pull of a missing item is code 4, and lock run
keeps its command's exit code when the release that follows fails.
cronstable state get KEY [--scope NAME | --global]
cronstable state set KEY VALUE [--json] [--scope NAME | --global]
cronstable state delete KEY [--scope NAME | --global]
cronstable state keys [--scope NAME | --global]
--json stores VALUE as a parsed JSON document rather than a string.
cronstable cursor get NAME [--scope NAME | --global]
cronstable cursor advance NAME VALUE [--force] [--scope NAME | --global]
advance refuses to move the monotonic cursor backward; --force sets it
unconditionally (a deliberate rewind).
cronstable lock acquire NAME [--permits N] [--wait --timeout S] [--ttl S] [--scope NAME | --global]
cronstable lock run NAME [--permits N] [--wait --timeout S] [--ttl S] [--scope NAME | --global] -- COMMAND...
cronstable lock release TOKEN
acquire prints a hold token, or exits 3 when no permit is free. --wait
first blocks up to --timeout seconds (default 0, so --wait alone makes
one pass and gives up). release takes the token, and no scope flags. run
holds the lock while COMMAND... (everything after --) runs and exits with
the command's own exit code, or 3 if the lock was not acquired.
--timeout takes a finite number of seconds, and a negative value counts as
0. A value of nan or inf is a usage error (exit 2), with or without
--wait. With --wait, the command gives the daemon 30 seconds more than
--timeout to reply, up to 2,147,483 seconds in all (about 24.9 days).
--permits accepts 1 (the default, a mutex) through 1024; outside that
range the command exits 1. --ttl overrides the lease TTL (default
state.jobApi.lockTtlSeconds). The -- separator belongs to lock run
alone: cronstable splits the trailing command off at the first -- before
argument parsing, so a bare -- in any other invocation is rejected with
`--` is only valid before a `lock run` command and exits 2.
cronstable artifact put NAME [FILE] [--scope NAME | --global]
cronstable artifact get NAME [-o FILE] [--scope NAME | --global]
cronstable artifact list [--scope NAME | --global]
put reads FILE, or stdin when FILE is omitted or -; get writes to
-o FILE, or stdout when omitted or -.
cronstable idempotent KEY [--ttl S] [--release] [--scope NAME | --global]
--ttl S expires the claim after S seconds (0, the default, is a
permanent claim); --release drops the claim instead of making it.
cronstable secret get NAME
cronstable secret list
No flags. get exits 4 if no secret of that name is staged for this run.
cronstable xcom push --key KEY [FILE]
cronstable xcom pull --task TASK --key KEY [--map-index I] [-o FILE]
cronstable xcom list
Pass data between the tasks of a
DAG run. Outside a
task the DAG scheduler launched, the commands print a clean error and exit
nonzero. push reads FILE or stdin; pull writes to -o FILE or stdout,
with --map-index I selecting one instance of a
mapped upstream.
cronstable mcp [--url URL] [--token TOKEN] [--token-env VAR]
[--cacert PATH] [--client-cert PATH --client-key PATH]
[--insecure] [--protocol-version REV] [--timeout SECONDS]
[--check]
cronstable mcp runs the MCP stdio bridge: a thin standard-library client that
connects a desktop MCP client (stdio transport) to a running daemon's /mcp
endpoint (--url, default http://127.0.0.1:8080). Like the job-facing
commands it needs no configuration file and never imports the daemon graph. The
MCP page documents the bridge, every flag (including
--protocol-version and --timeout), and client setup. See
listener TLS for the TLS flags
(--cacert, --client-cert, --client-key, --insecure).
cronstable tui [--url URL] [--token TOKEN] [--token-env VAR]
[--cacert PATH] [--client-cert PATH --client-key PATH]
[--insecure] [--theme NAME] [--tv] [--job NAME]
[--boot | --no-boot] [--ascii] [--poll SECONDS]
cronstable tui opens the terminal dashboard (the
web dashboard's keyboard-driven TUI sibling) against a running
daemon's web listener (--url, default http://127.0.0.1:8080). It requires
an interactive terminal. The terminal dashboard page
documents every option, key, and panel. See
listener TLS for the TLS flags.
cronstable pair [--url URL] [--token TOKEN] [--token-env VAR]
[--cacert PATH] [--client-cert PATH --client-key PATH]
[--insecure] [--public-url URL] [--name NAME]
[--format qr|link|json]
cronstable pair prints the QR code that pairs the iOS app with a running
daemon (--url, default http://127.0.0.1:8080), for a server without the
dashboard page or a shell without a browser. It is a standard-library client
like cronstable mcp: it needs no configuration file and never imports the
daemon graph. The code contains the token that the command presents, and
--public-url sets the address that the phone dials. The
pairing from the terminal page documents every option, how
the command chooses the address, and the terminal dashboard's panel. See
listener TLS for the TLS flags.
cronstable import-taskscheduler PATH... [-o FILE] [--timezone NAME]
Converts Windows Task Scheduler XML exports into cronstable jobs and exits.
PATH may be an export file, a directory of *.xml, or - for standard
input. The configuration goes to stdout or -o; a report of everything that
could not be carried across goes to stderr, so the two separate cleanly:
schtasks /query /XML ONE > tasks.xml
cronstable import-taskscheduler tasks.xml -o jobs.yaml 2> report.txt
cronstable -v -c jobs.yamlExit 0 when something converted, 1 when the input could not be read or
nothing usable came out, 2 for a usage error. It runs on any platform, not
only Windows.
Review the output before loading it: exporting a task does not unregister it. Expect a whole-machine export to convert only in part, because most registered tasks on a stock Windows install are COM-handler or event-driven internals. Importing from Task Scheduler has the full mapping table and the reason behind every refusal.
cronstable service install [--name NAME] [-c FILE-OR-DIR]
[--start-type auto|delayed|demand]
[--log-level LEVEL] [--log-file PATH | --no-log-file]
[--console] [--restart-delay SECONDS] [--no-restart]
cronstable service start|stop [--name NAME] [--timeout SECONDS]
cronstable service remove|reload|status [--name NAME]
cronstable service run [--name NAME] [-c FILE-OR-DIR] [options]
Windows only. On any other platform every action prints one line and exits 2.
cronstable service install registers the scheduler with the Service Control
Manager so it starts at boot and runs whether or not anyone is logged on, and
needs an elevated prompt. reload makes the running service reparse its
configuration immediately, the same forced reload SIGHUP performs on POSIX.
run is the entry point the SCM itself invokes and is not meant to be typed.
When you run it by hand, it says so and exits 2.
Exit codes for the whole subcommand: 0 success, 1 a Windows failure
(the message names the fix), 2 a refusal or a usage error.
The Windows service page covers the full command set, the recovery-action behavior, where a service logs, the configuration-path rule, and the installation format that cannot host a service.
When started normally (no --version, no --validate-config, no
--job-set-id, no subcommand, with a usable configuration), cronstable:
- Configures logging from
-l. - Resolves and parses the configuration (
-c), exiting1on error. - Installs shutdown handlers. On POSIX these are bound to
SIGINTandSIGTERMon the event loop, andSIGHUPis bound to an immediate configuration reload. On Windows cronstable instead usessignal.signalforSIGINT(Ctrl-C) andSIGBREAK(Ctrl-Break) plus a heartbeat timer, because the Proactor loop has noadd_signal_handler. On Windows, cronstable also installs a native console-control handler that turns console close and OS shutdown into the same graceful drain (bounded by the OS's own grace period). Logoff is deliberately not one of them; see graceful shutdown on Windows. - Runs the asyncio scheduler loop in the foreground until shutdown.
The scheduler checks the configuration files about once a minute and reparses
them when a file's modification time or size changes, so edits take effect
without a restart. The next check picks up an edit that lands while a reload
is parsing. On POSIX, SIGHUP forces an immediate reparse (on Windows,
cronstable service reload). A configuration that
becomes invalid after a successful start is logged and ignored, and the
previously loaded jobs keep running. When -c names a pipe or a FIFO, the
once-a-minute check does not read it again, and only a reload request
does. See architecture and internals.
SIGINT (Ctrl-C) and SIGTERM are both bound to the same graceful-shutdown
path: they set an internal stop event. The scheduler loop notices the event,
stops scheduling new job runs, logs Shutting down (after currently running jobs finish)..., and then cronstable:
- Cancels all pending retry timers.
- Waits for currently running jobs to finish.
- Stops the HTTP control server if it is running (logged as
Stopping http server).
On shutdown, cronstable does not force-kill its own running jobs. Individual
jobs have their own killTimeout behavior when they are stopped; see
concurrency and timeouts. Sending a second signal
does not change the shutdown sequence.
A deployment that cannot deliver a signal (a supervised or console-less daemon)
gets the same graceful drain from the authenticated POST /shutdown route; see
the HTTP control API. If you need an immediate, ungraceful stop,
end the process with SIGKILL on POSIX or Task Manager / taskkill /F on
Windows; either skips the drain, and on Windows also leaves any spawned job
trees running.
On Windows, press Ctrl-C (SIGINT) to trigger the same graceful shutdown: it
finishes the currently running jobs first, exactly as SIGTERM does on POSIX,
and jobs run in their own console process groups so the keystroke never reaches
them directly.
Ctrl-Break (SIGBREAK) drains the daemon the same way, but a console-generated
break also reaches the jobs sharing the console, so prefer Ctrl-C. Closing the
console window and OS shutdown trigger the drain too, on the few seconds of
grace Windows grants. Logging off does not: an unattended daemon receives that
event for every user on the machine and would stop on the first RDP sign-out.
See running on Windows.
| Code | Condition |
|---|---|
0 |
Setup guidance for a bare invocation with no default configuration; --version, --third-party-licenses, or --sealable-suites printed; --validate-config succeeded; --job-set-id printed; --help; a state action succeeded; or normal shutdown after a signal. |
1 |
Configuration error (parse/schema/validation failure or unreadable configuration); a missing default path when arguments request configuration loading; an init refusal; a state action failed (see state exit codes); or the reader of a command's output closed the pipe, as head does, which also prints cronstable: cannot write the output to stderr. |
2 |
Usage error (argparse builtin): unknown option or missing required option (such as state backup without -o); an invalid --log-level value; a --timeout that cronstable mcp or cronstable lock refuses; cronstable state invoked with no action; or a -- separator in any invocation other than lock run (see lock). |
Run with a single configuration file in the foreground:
cronstable -c /tmp/my-crontab.yamlRun against a configuration directory (the conventional container entrypoint):
cronstable -c /etc/cronstable.dOn Windows the configuration path uses Windows paths, and the machine-wide
default directory is %ProgramData%\cronstable (falling back to
%APPDATA%\cronstable until it holds a configuration file):
cronstable.exe -c C:\ProgramData\cronstableSee running on Windows for Windows-specific CLI behavior (default configuration path, default shell, shutdown semantics, running unattended).
Validate a configuration and exit (suitable for CI or a container healthcheck/preflight):
cronstable -v -c /etc/cronstable.dIncrease log verbosity:
cronstable -l DEBUG -c /tmp/my-crontab.yamlPrint the version:
cronstable --versionBack up the durable state store defined by a configuration (the -c may
equally go before state):
cronstable state backup -o /backups/cronstable-state.tar.gz -c /etc/cronstable.dFor installation and packaging details (pip, PyInstaller binary, Docker), see installation. For deploying cronstable as a long-running service, see production and container deployment. For Windows-specific CLI behavior (default configuration path, default shell, Ctrl-C / Ctrl-Break shutdown), see running on Windows.