BLOG/DEVELOPER DAILY
THEME
DEVELOPER DAILY

Scheduled jobs without cron soup

The crontab line is a wall of redirections, lock files, and a script that swallows its own errors. Point cron at one OrchStep task instead — structured steps, JSON output, and an exit code your timer can trust.

Jun 13, 2026 OrchStep Team 5 minROLE: AnySCALE: Any
RUNNABLE DEMO
Full source for this post: blog/scheduled-jobs
VIEW SOURCE

Here's a crontab line from a real machine:

0 3 * * * /opt/app/nightly.sh >> /var/log/nightly.log 2>&1 || echo "fail" | mail -s alert ops@co

It runs nightly.sh, which itself is a set -e script that rotates logs, refreshes some views, and prunes old artifacts. When it breaks at 3am, you find out from a one-line email that says fail, the log is a wall of interleaved stdout, and the cron job exited 0 anyway because the failing command was in a pipe. Cron soup: the schedule, the redirection, the locking, and the error handling all crammed into shell that no one wants to touch.

Cron is good at exactly one thing — running something on a schedule. So let it do that, and point it at an OrchStep task that handles the structure, the output format, and a real exit code.

The job as a task

The nightly work becomes ordered steps. Cron just triggers it:

orchstep.yml
name: nightly
# A task you trigger from cron or a systemd timer.
defaults:
  retention: "30"

tasks:
  # `orchstep run nightly --output json`
  nightly:
    steps:
      - name: rotate
        func: shell
        do: echo "rotating logs older than {{ vars.retention }} days"
      - name: refresh
        func: shell
        do: echo "refreshing materialized views"
        retry:
          max_attempts: 2
          interval: "5s"
          backoff_rate: 2.0
      - name: prune
        func: shell
        do: echo "pruning artifacts, keeping {{ vars.retention }} days"
      - name: done
        func: shell
        do: echo "nightly maintenance complete"

The flaky step — refreshing views while the DB is under load — gets a retry: instead of a hand-rolled loop. Each step is named, so a failure says which one broke. And the whole thing is one task, not a script reaching into other scripts.

The crontab line gets boring

That's the point. The schedule line stops carrying logic:

0 3 * * * cd /opt/app && orchstep run nightly --output json >> /var/log/nightly.jsonl

Cron runs the task. OrchStep handles ordering, retries, and output. The redirection appends a structured JSON record per run instead of interleaved stdout — so /var/log/nightly.jsonl is something you can actually query later.

The exit code your timer can trust

This is the part shell got wrong. If any step fails, the run exits non-zero — full stop, no pipe swallowing it. So your timer's own failure tracking works:

# nightly.service — a systemd timer points here
[Service]
Type=oneshot
WorkingDirectory=/opt/app
ExecStart=/usr/local/bin/orchstep run nightly --output json

systemctl status nightly.service now reflects reality, because the exit code is honest. No || mail duct tape required — though you can still add one, and it'll only fire on real failures.

What you actually gained

Concernnightly.sh in cronOrchStep
Step that failedgrep the lognamed in the output
Flaky stephand-rolled retryretry: block
Machine-readable run recordinterleaved stdout--output json
Exit codeswallowed by a pipehonest non-zero on failure
The crontab linea wall of logicone orchstep run

OrchStep isn't a scheduler — cron and systemd timers are great schedulers, keep them. What it replaces is the soup: the logic, error handling, and output formatting that never belonged in the crontab line.

Where to go next

Got a crontab line doing too much? Point it at a task and let the schedule line get boring.

#CRON#AUTOMATION#OPS#TASK-RUNNER
Try it in two minutes — one binary, no signup.
curl -fsSL https://orchstep.dev/install.sh | sh

RELATED — DEVELOPER DAILY