crdo
CRDO
A small cron-like scheduler written in Crystal. Inspired by this HN comment.
CRDO runs named tasks from a YAML file, keeps lightweight run state between process restarts, writes per-command logs, and can reload its config while tasks are still running.
AI
Codex has been used heavily in the refactoring of this tool. Commits using AI are denoted with (AI tool name) in the author field.
Usage
Create ~/.crdo.yml, or pass another file:
shards build
./bin/crdo
Run one or more named tasks immediately without reading or writing scheduler state:
./bin/crdo --now backup
./bin/crdo --now backup verify-backup
Preview command execution by prefixing each command with echo:
./bin/crdo --test --file ./sample.crdo.yml
Run specs with:
crystal spec
Controls
Send signals to a running CRDO process:
kill -HUP <pid> # reload config
kill -USR1 <pid> # print full schedule report
kill -USR2 <pid> # print running task report
kill -INT <pid> # save state and exit after running tasks finish
On reload, unchanged running tasks are kept. Changed running tasks are marked as retiring and their replacements are deferred until the old run exits. Deleted running tasks are also allowed to finish before being removed.
global.print_report: false suppresses automatic task start/stop lines, but USR1 always prints the full schedule report.
Config
The root YAML object must contain global and one or more task entries.
global:
workdir: .
include:
- sample.include.crdo.yml
mail: user@example.com
mail_size_limit: 10485760
autosave: 600
ignore_overtime: false
print_report: true
test: false
error: false
Global keys:
workdir: required base directory for relative command paths andcron_logs.include: optional string or list of YAML files to merge into the root config.mail: optional address for failure and overtime mail.mail_size_limit: optional total attachment limit, in bytes, for failure mail.autosave: optional state save interval in seconds. Defaults to 600.ignore_overtime: skip overtime warnings for tasks that run past their interval.print_report: print automatic start/stop lines when tasks change state.test: prepend commands withecho.error: in test mode, treat completed commands as failed for dependency testing.
Includes are resolved relative to the config file. Include files may define tasks only; they cannot define another global section.
Tasks
Each task is keyed by name:
backup:
every: 1h
timeout: 30m
group: $exclusive
vars:
target: /srv/backup
error_body: "Check disk space and rerun with crdo --now backup."
error_command: /usr/bin/tmux new-window -d -n backup-error /bin/sh
commands:
- /usr/bin/rsync -a /home/ $target/
- /usr/bin/true
Task keys:
commands: required array of commands. Commands are executed directly, not through a shell, unless you explicitly use/bin/sh -c.every: interval schedule. Supportss,m,h, andd.timeout: optional maximum wall-clock runtime for the whole task. Supportss,m,h, andd.when: wall-clock schedule such as13:00,mon 08:30,jan 1 00:00, or comma lists likemon,wed,fri 02:00.when_policy: optional policy for missed/repeated wall-clock slots.use_stop_time: withevery, measure the next run from the prior stop time instead of start time.parent: require another task to complete successfully before this task can run once.group: serialize tasks with the same group.$exclusiveprevents any other task from running at the same time.disabled: keep the task loaded but never run it.vars: task-local string replacements for$namestyle variables.error_body: extra body text for failure mail.error_command: command to launch after a task failure.
A task must specify exactly one of every or when.
Wall-Clock Policy
when_policy controls how when schedules behave across clock jumps.
daily:
when: 01:00
when_policy:
forward: after
backward: once
commands:
- /bin/true
Forward policies:
skip: missed slots are skipped.after: a missed slot runs after the scheduler notices the forward jump.
Backward policies:
once: repeated local-time slots run once.repeat: repeated local-time slots may run again.
when_policy: true means forward: after and backward: once.
CRDO evaluates when schedules against wall-clock time. NTP corrections, manual clock changes, daylight-saving transitions, and VM suspend/resume can therefore skip, delay, or repeat local-time slots according to the selected when_policy. Use every for monotonic interval-style work that should not be tied to calendar time.
Logs And State
CRDO writes command logs under:
<workdir>/cron_logs/<task>/<yyyy-mm-dd>/<hh-mm-ss>/
Each command writes:
<jobnum>.cmdline: JSON command argv.<jobnum>.stdout: captured stdout.<jobnum>.stderr: captured stderr.<jobnum>.rc: process exit code.
If failure mail cannot be delivered, CRDO writes mailfail in the task log directory and includes recent mail failures in the USR1 report.
If mail_size_limit is set, CRDO attaches log files until the configured byte limit is reached and lists skipped files in the message body.
Scheduler state is saved next to the config as <config>.state. --now skips state restore and save.
How It Works
CRDO has one runtime loop:
- Load config and restore state.
- Start every task whose current state says it can run.
- Wait for the next task stop, signal event, autosave, or schedule timeout.
- Apply the event, update task state, print reports, reload, save, or exit.
- Repeat until shutdown is requested and all running tasks have stopped.
The scheduler intentionally does not persist dependency readiness across restarts. After a restart, parent tasks must run successfully again before child tasks become eligible.
Possible Future Work
- Retry-after-error delay.
- Randomized start delay.
- Configurable shutdown grace period before killing running tasks.
- Multiple parents or groups per task.
- Run tasks as another user.
crdo
- 1
- 1
- 0
- 0
- 0
- 12 days ago
- January 17, 2024
Sat, 25 Jul 2026 15:17:20 GMT