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.
blog/scheduled-jobsHere'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@coIt 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:
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.jsonlCron 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 jsonsystemctl 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
| Concern | nightly.sh in cron | OrchStep |
|---|---|---|
| Step that failed | grep the log | named in the output |
| Flaky step | hand-rolled retry | retry: block |
| Machine-readable run record | interleaved stdout | --output json |
| Exit code | swallowed by a pipe | honest non-zero on failure |
| The crontab line | a wall of logic | one 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
- Quick Start — your first workflow in two minutes
- Error Handling — retry, catch, finally, timeouts
- Variables & Outputs — passing
--varfrom a timer
Got a crontab line doing too much? Point it at a task and let the schedule line get boring.
curl -fsSL https://orchstep.dev/install.sh | sh