127 lines
4.1 KiB
Markdown
127 lines
4.1 KiB
Markdown
---
|
||
title: Schedule Recurring Jobs with the Builtin Cron Scheduler
|
||
impact: MEDIUM
|
||
impactDescription: Avoids external schedulers and correctly integrates background tasks with the PocketBase lifecycle
|
||
tags: 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):**
|
||
|
||
```go
|
||
// ❌ 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()
|
||
```
|
||
|
||
```javascript
|
||
// ❌ JSVM: using setTimeout – not supported in the embedded goja engine
|
||
setTimeout(() => console.log("run"), 120_000); // ReferenceError
|
||
```
|
||
|
||
**Correct – Go:**
|
||
|
||
```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:**
|
||
|
||
```javascript
|
||
// 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 call `RemoveAll()` 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 `$app` but **not** to outer-scope variables (see JSVM scope rule).
|
||
- Go jobs can use `app` directly (not `e.App`) because they run outside the hook/transaction context.
|
||
|
||
Reference: [Go – Jobs scheduling](https://pocketbase.io/docs/go-jobs-scheduling/) | [JS – Jobs scheduling](https://pocketbase.io/docs/js-jobs-scheduling/)
|