Cron Jobs

M8. systemd-user timers + a command allowlist.

Model

A Cron Job is:

  • 5-field cron schedule (min hour day month dow).
  • An owner user (must have a Linux account).
  • A command from the allowlist (php, wp, plus a curated set per-app).
  • An optional name (display label).

Internally each cron row becomes:

  • ~/.config/systemd/user/jabali-cron-<id>.service — the command.
  • ~/.config/systemd/user/jabali-cron-<id>.timer — the schedule.

Both are owned by the user. loginctl enable-linger <user> runs at user creation so timers fire without an active session.

Why not crontab?

systemd-user timers give:

  • Per-job journal logs (journalctl --user -u jabali-cron-<id>).
  • OnFailure= hook → notification dispatcher.
  • RandomizedDelaySec= — natural jitter without operators having to add sleep $RANDOM.
  • No per-user crontab -e shell access (we don’t grant interactive shell to panel users).

Command allowlist

Only commands the admin has marked allowed can be scheduled. The default allowlist (/internal/cronvalidate/) is the shared validator used by both the REST API and the CLI (Cron Job Intake — the single ingest path, per CONTEXT.md). Custom shell scripts are not allowed by default — admins can extend the allowlist.

CLI

jabali cron list --user <id>
jabali cron add --user <id> --schedule "0 3 * * *" --command "wp cron event run --due-now --url=https://example.com"
jabali cron update <job-id> --schedule "*/15 * * * *"
jabali cron delete <job-id>
jabali cron run-now <job-id>     # synchronous, ignores schedule

Failures

If a job exits non-zero, the OnFailure=jabali-cron-notify@%n.service unit fires and the notifications dispatcher (M14) sends an alert via the user’s configured channels (in-app bell, email, etc.).