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: [email protected]
  mail_size_limit: 10485760
  autosave: 600
  ignore_overtime: false
  print_report: true
  test: false
  error: false

Global keys:

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:

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:

Backward policies:

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:

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:

  1. Load config and restore state.
  2. Start every task whose current state says it can run.
  3. Wait for the next task stop, signal event, autosave, or schedule timeout.
  4. Apply the event, update task state, print reports, reload, save, or exit.
  5. 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