4.1 KiB
4.1 KiB
title, impact, impactDescription, tags
| title | impact | impactDescription | tags |
|---|---|---|---|
| Schedule Recurring Jobs with the Builtin Cron Scheduler | MEDIUM | Avoids external schedulers and correctly integrates background tasks with the PocketBase lifecycle | cron, scheduling, jobs, go, jsvm, extending |
Schedule Recurring Jobs with the Builtin Cron Scheduler
PocketBase includes a cron scheduler that starts automatically with serve. Register jobs before calling app.Start() (Go) or at the top level of a pb_hooks file (JSVM). Each job runs in its own goroutine and receives a standard cron expression.
Incorrect (external timer, blocking hook, replacing system jobs):
// ❌ Using a raw Go timer instead of the app cron – misses lifecycle management
go func() {
for range time.Tick(2 * time.Minute) {
log.Println("cleanup")
}
}()
// ❌ Blocking inside a hook instead of scheduling
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
for {
time.Sleep(2 * time.Minute)
log.Println("cleanup") // ❌ blocks the hook and never returns se.Next()
}
})
// ❌ Removing all cron jobs wipes PocketBase's own log-cleanup and auto-backup jobs
app.Cron().RemoveAll()
// ❌ JSVM: using setTimeout – not supported in the embedded goja engine
setTimeout(() => console.log("run"), 120_000); // ReferenceError
Correct – Go:
package main
import (
"log"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/core"
)
func main() {
app := pocketbase.New()
// Register before app.Start() so the scheduler knows about the job at launch.
// MustAdd panics on an invalid cron expression (use Add if you prefer an error return).
app.Cron().MustAdd("cleanup-drafts", "0 3 * * *", func() {
// Runs every day at 03:00 UTC in its own goroutine.
// Use app directly here (not e.App) because this is not inside a hook.
records, err := app.FindAllRecords("posts",
core.FilterData("status = 'draft' && created < {:cutoff}"),
)
if err != nil {
app.Logger().Error("cron cleanup-drafts", "err", err)
return
}
for _, r := range records {
if err := app.Delete(r); err != nil {
app.Logger().Error("cron delete", "id", r.Id, "err", err)
}
}
})
// Remove a job by ID (e.g. during a feature flag toggle)
// app.Cron().Remove("cleanup-drafts")
if err := app.Start(); err != nil {
log.Fatal(err)
}
}
Correct – JSVM:
// pb_hooks/crons.pb.js
/// <reference path="../pb_data/types.d.ts" />
// Top-level cronAdd() registers the job at hook-load time.
// The handler runs in its own goroutine and has access to $app.
cronAdd("notify-unpublished", "*/30 * * * *", () => {
// Runs every 30 minutes
const records = $app.findAllRecords("posts",
$dbx.hashExp({ status: "draft" })
);
console.log(`Found ${records.length} unpublished posts`);
});
// Remove a registered job by ID (useful in tests or feature toggles)
// cronRemove("notify-unpublished");
Cron expression reference:
┌─── minute (0 - 59)
│ ┌── hour (0 - 23)
│ │ ┌─ day-of-month (1 - 31)
│ │ │ ┌ month (1 - 12)
│ │ │ │ ┌ day-of-week (0 - 6, Sunday = 0)
│ │ │ │ │
* * * * *
Examples:
*/2 * * * * every 2 minutes
0 3 * * * daily at 03:00
0 0 * * 0 weekly on Sunday midnight
@hourly macro equivalent to 0 * * * *
Key rules:
- System jobs use the
__pb*__ID prefix (e.g.__pbLogsCleanup__). Never callRemoveAll()or use that prefix for your own jobs. - All registered cron jobs are visible and can be manually triggered from Dashboard > Settings > Crons.
- JSVM handlers have access to
$appbut not to outer-scope variables (see JSVM scope rule). - Go jobs can use
appdirectly (note.App) because they run outside the hook/transaction context.
Reference: Go – Jobs scheduling | JS – Jobs scheduling