Scheduled tasks
Anywhere else, the answer to “did my cron job run?” is an email that on a modern server goes nowhere. KLYRN records every run and shows you what it printed.
Adding one
klyrn cron example.com # what is scheduled, and how each one last went
klyrn cron runs 12 # every recorded run of one entry
klyrn cron run 12 # run it now, as the account
The schedule is five cron fields or one of @hourly, @daily,
@weekly, @monthly, @yearly, @reboot.
Anything else is refused with the example, rather than accepted and silently ignored by
cron later.
Entries run as the site's own Unix account. The list also shows the account's other entries (imported ones, and the WordPress cron KLYRN installs in place of WP-Cron) so nothing runs on this account that the page does not show.
What it did
Every run is recorded: when it started, how long it took, what it exited with, and the first 8 KB it printed, from both stdout and stderr. The last 50 runs of each entry are kept.
Worked · 1 ms
hello from cron / and stderr / 2026-09-06
Three outcomes are named rather than left as a number: Worked for exit 0, Exit N for anything else, and Did not start for exit 127 (a missing interpreter or a command that is not there), which is the case hardest to debug without a record, because nothing you wrote ever ran.
Output is still passed through as cron expects, so an operator who has set a
MAILTO keeps receiving it.
Running one on demand
Testing a cron job traditionally means waiting for the hour to turn. Any entry can be run immediately, from the panel or the command line. It runs as the account, through the account's shell, exactly as cron would, and it is recorded like any other run, marked as one you started.
The panel refuses it for a token without the sites scope, and running an
entry is a mutating operation, so a read-only token cannot start one.
How it works, and why it is safe
The command is not in the crontab. Each entry is a script under
/home/<account>/.klyrn-cron/<id>.sh, owned by root and readable
by the account, and the crontab line runs a wrapper that names only the id:
*/5 * * * * sklyrn /opt/klyrn/bin/klyrn cron-exec 1
That is worth doing on its own. A crontab line is a fragile place for user input: an
unescaped % means newline and is fed to the command's stdin, a stray quote
breaks the file, and cron refuses the whole file rather than the one bad line.
Now every line has the same shape.
The wrapper runs as the account and has no privilege of any kind: it cannot read the database and cannot reach the core daemon. It works out which script to run from its own identity, so asking for another account's id simply finds nothing. It runs the job first, then drops a record in a spool directory that account users can write but not list: mode 1733, the same as the staging area. Core sweeps that spool and checks the file's owner really is the account that owns the job before believing a word of it.
Both halves were tested on a live server. cron-exec 1 as a second tenant
answered job 1 is not one of tenant2's; a record forged by that tenant claiming
the same job was refused, and the refusal is in the log:
uid 1005 tried to write history for account sklyrn.
Recording can never break a job. The job runs first, every failure in the recording path is swallowed, and the wrapper exits with the job's own exit code.