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 addsleep $RANDOM.- No per-user
crontab -eshell 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.).