You know the feeling. You need to run a backup script every day at 9:11am, so you open a terminal, type crontab -e, and stare at a blinking cursor waiting for you to conjure five asterisks and numbers in the correct ancient order.
11 9 * * *
Is that minute-then-hour, or hour-then-minute? Did you just schedule this for 9:11am or for the 9th minute of every hour that happens to also be... you get the idea. You copy-paste it into an online cron parser to check your own work. You feel bad about it. We all feel bad about it.
Cron syntax is one of those things every backend developer has "learned" a dozen times and forgotten a dozen times, because there's zero semantic connection between what you type and what it means. */15 * * * * doesn't look like "every 15 minutes" — it looks like line noise that happens to work.
pboss — the Bun-powered process manager — ships a cron system that fixes this without throwing away the power of raw cron when you actually need it.
The problem with crontab -e
Standard cron has a few real, practical downsides beyond "the syntax is opaque":
- No persistence layer of its own. It's a system-level daemon, separate from whatever's managing your app processes.
-
No visibility. Want to know when a job last ran, whether it succeeded, or when it's running next? Better go dig through
/var/logor set up your own logging by hand. - No easy one-shot scheduling. Need something to run once, next Tuesday at 11pm, and never again? You're writing a workaround, not using cron the way it was designed.
- Machine downtime silently eats jobs, and figuring out what got skipped is a manual, aggravating exercise.
If you're already running pboss to manage your app processes, none of this needs to be a separate problem you solve separately.
Enter human-readable schedules
Here's the same 9:11am backup job in pboss:
pboss cron run everyday@9:11 "bun /srv/backup.ts"
Read that out loud. It says exactly what it does. No mental math, no lookup table, no "wait, is Sunday 0 or 7 in this implementation."
A few more, so you get the shape of the grammar:
# Every Sunday at 10:10am, named explicitly
pboss cron run every-sunday@10:10 "sh /srv/cleanup.sh" --name cleanup
# One-shot: fires once, at a specific date and time, then it's done
pboss cron run on-date@24-10-2026-23:10 "node migrate.js"
The grammar covers the cases you actually hit in real projects:
| You want... | You write... |
|---|---|
| Every day at midnight | everyday |
| Every day at a specific time | everyday@9:11 |
| Every N seconds | every-15-seconds |
| Every hour, or every hour at :30 |
everyhour / everyhour@30
|
| Every week on a given weekday |
every-sunday@10:10, everyMonday@10:10, onSunday@23:10
|
| Every month, or a specific day of month |
everymonth / every-15th@10:10
|
| Every N hours or N days |
every-6-hours / every-2-days@8
|
| Once, later today or tomorrow |
today@23:10 / tomorrow@8:00
|
| Once, on a specific calendar date | on-date@24-10-2026-23:10 |
A couple of details that show this was designed by someone who's actually been burned by cron before:
-
Hour 24 rolls to the next day.
everyday@24:30means 00:30 tomorrow, so you can express "half an hour after midnight" without the off-by-one anxiety. -
Keywords are forgiving.
on-date@,onDate@, andon_date@are all the same thing — hyphens, underscores, and camelCase are interchangeable, so you don't have to remember one exact casing convention. -
Dates are calendar-validated. Try to schedule
on-date@31-02-2026and you get a clear rejection instead of a job that silently never fires. -
A past
today@…time gets rejected with a suggestion, not silently scheduled into a black hole. - Missed jobs are skipped, not queued up. If the daemon or machine was down, a recurring job just reschedules to its next real future occurrence — same behavior you'd expect from classic cron, so nothing runs in a confusing backlog burst when the box comes back up.
The escape hatch: raw cron still works
This is the part that matters most, honestly. Nobody wants a "friendly" tool that boxes you in the moment your schedule gets weird. So pboss doesn't replace cron expressions — it accepts them:
pboss cron run "*/5 * * * *" "curl -s https://example.com/ping"
You can even go beyond standard 5-field cron with a 6-field expression, where the first field is seconds:
pboss cron run "*/10 * * * * *" "node heartbeat.js"
So the mental model is: reach for the friendly syntax by default, and drop into raw cron only for the genuinely gnarly schedules where a plain-English phrase can't capture what you need.
Managing jobs like they're first-class citizens
Because cron jobs live in the daemon itself (persisted to ~/.pboss/cron.json, surviving reboots), you get the same kind of visibility you'd expect for a managed process:
pboss cron list
┌────┬─────────┬─────────────┬──────────────────┬───────────────────────────┬──────┬──────┬──────────┐
│ id │ name │ schedule │ command │ next run │ runs │ last │ status │
├────┼─────────┼─────────────┼──────────────────┼───────────────────────────┼──────┼──────┼──────────┤
│ 1 │ backup │ everyday@9 │ bun backup.ts │ 2026-09-07 09:00 Mon │ 14 │ ✓ │ ● online │
│ 2 │ cleanup │ every-sunday│ sh cleanup.sh │ 2026-09-13 00:00 Sun │ 3 │ ✓ │ ● online │
└────┴─────────┴─────────────┴──────────────────┴───────────────────────────┴──────┴──────┴──────────┘
Peek at upcoming runs without waiting around:
pboss cron next backup --count 5
Force a run right now, without touching the schedule:
pboss cron trigger backup
And clean up when a job's done:
pboss cron remove backup
Every run gets logged automatically to ~/.pboss/logs/cron/<name>.log, with a header (job name, schedule, working directory), the command's full combined output, and a footer with exit code and duration. That's debugging information you'd otherwise have to wire up yourself.
Declaring crons alongside your apps
If you're already using an ecosystem file to manage your processes, cron jobs slot right in next to them:
export default {
crons: [
{
name: "backup",
schedule: "everyday@2:00",
command: "bun /srv/backup.ts",
},
{
name: "report",
schedule: "every-15th@10:10",
command: "sh /srv/report.sh",
},
{
// paused until you're ready
name: "maintenance",
schedule: "every-sunday@5:00",
command: "sh /srv/maintenance.sh",
enabled: false,
},
],
apps: [
/* … */
],
};
Re-running the ecosystem file updates schedules in place — jobs are matched by name, so you can tweak a schedule and redeploy without duplicating jobs.
Or drive it all from code
import { pboss } from "pboss";
const job = await pboss.cronAdd("everyday@9:11", "bun backup.ts", { name: "backup" });
for (const j of await pboss.cronJobs()) {
console.log(`${j.name} — ${j.description} (runs: ${j.runCount})`);
}
await pboss.cronTrigger("backup"); // run now
await pboss.cronRemove("backup"); // remove
Under the hood
Jobs execute through the system shell (/bin/sh -c on Unix, cmd /c on Windows), so pipes and redirects behave exactly like you'd expect:
pboss cron run everyday@3 "bun report.ts | mail -s 'daily report' ops@example.com"
And the scheduler itself isn't naively polling every minute and hoping — it sleeps until the next actual scheduled run with a periodic watchdog rescan, so timing stays accurate to the second even across clock adjustments.
The takeaway
Cron syntax isn't going anywhere — it's load-bearing infrastructure for the entire internet, and pboss doesn't pretend otherwise; raw expressions are still a first-class option. But for the 90% of scheduling you actually do — "run this every day," "run this every Sunday morning," "run this once, next Tuesday" — there's no reason to keep translating your intent into five cryptic fields by hand.
pboss cron run everyday@9:11 "bun /srv/backup.ts"
That's it. That's the whole horror story, and it has a happy ending.
Want to try it: check the cron docs.
Top comments (0)