Jobs & cron
Run background work and scheduled tasks with the built-in queue.
Updated
Railbase has a built-in background job runner and scheduler — no Redis, no separate worker process. Use it for anything that shouldn't block a request: sending email, generating documents, sweeping expired data.
Define a job
Register a handler under a kind at boot, then enqueue work against that kind at
runtime. The registry (and the store) come up during App.Run, so register from
inside OnBeforeServe — calling app.Jobs() directly in the ExecuteWith
callback returns nil (the subsystem doesn't exist yet):
import "github.com/railbase/railbase/pkg/railbase/jobs"
cli.ExecuteWith(func(app *railbase.App) {
app.OnBeforeServe(func(_ chi.Router) {
// Register: maps a "kind" to a handler. j.Payload is the raw
// JSON the enqueuer passed.
app.Jobs().Register("email.welcome", func(ctx context.Context, j *jobs.Job) error {
return sendWelcome(ctx, j.Payload)
})
})
})
// Enqueue (e.g. from a hook or REST handler — any time after boot):
_, err := app.JobsStore().Enqueue(ctx, "email.welcome",
map[string]any{"user_id": id},
jobs.EnqueueOptions{Delay: 30 * time.Second})
Jobs retry on failure (default 5 attempts). Return jobs.ErrPermanent to fail
without retrying.
Inspect jobs from the CLI
railbase jobs list
railbase jobs show <id>
railbase jobs run-now <id>
railbase jobs cancel <id>
railbase jobs reset <id> # failed/cancelled → pending, attempts=0 (re-run a dead job)
railbase jobs recover # requeue jobs stuck in "running"
Note
Like every command that opens the vault, these need exclusive access to the data file — stop the server first, or you'll get "vault: file is locked by another process". To inspect and manage jobs on a running instance, use the admin console instead (see below).
Built-in maintenance jobs
The core ships a set of housekeeping jobs, pre-scheduled out of the box (see
the _cron collection in the admin to adjust or disable them):
| Kind | Default schedule | What it does |
|---|---|---|
cleanup_sessions / cleanup_admin_sessions / cleanup_record_tokens |
nightly | drops expired credentials |
cleanup_exports |
nightly | removes finished export artifacts |
cleanup_jobs |
nightly | prunes completed/failed _jobs rows so the queue stays fast |
cleanup_notifications |
nightly | deletes read notifications older than 30 days and any older than 90 |
audit_seal / audit_archive |
daily | seals and archives the audit trail |
retry_failed_welcome_emails |
every 30 min | re-queues failed welcome emails (48 h window) |
Unread-but-recent notifications are never touched, and active jobs are never pruned — retention only ever removes terminal, aged rows.
Note
cleanup_orphan_files / orphan_reaper (file metadata/blob orphan sweeps) are
registered and scheduled but are currently no-op placeholders — they do not
yet reclaim orphaned blobs. Don't rely on them for storage hygiene.
Schedule recurring work
Persisted schedules are managed with railbase cron (standard 5-field cron
expressions):
railbase cron upsert nightly-report "0 3 * * *" report.generate
railbase cron list
railbase cron run-now nightly-report
railbase cron disable nightly-report
You can also schedule from a JavaScript hook:
$app.cronAdd("nightly", "0 3 * * *", () => { /* … */ });
Note
Hook crons are in-memory and per-process — they are not persisted to the
_cron table, and they re-register from your hook files on each reload (and on
each replica separately). Use railbase cron / the admin scheduler for durable,
operator-managed schedules; use $app.cronAdd for hook-local timers.
Tip
Scheduled backups are a built-in job kind, managed from the admin's dedicated Scheduler screen (Settings → Scheduler) — scheduling was moved there out of the Backups screen, which is now a read-only archive list. See Backups & restore.
The admin Scheduler screen (Settings → Scheduler) shows the schedule table and lets you run, enable/disable, or edit entries — see Operating from the admin.