Recipes
Custom cron jobs
Stanza's cron scheduler runs in-process — no external scheduler, no separate worker. This recipe shows how to add custom cron jobs to your application.
Where cron jobs live
All cron jobs are registered in the provideCron function in api/main.go. This function receives dependencies from the DI container and returns a configured scheduler:
func provideCron(lc *lifecycle.Lifecycle, db *sqlite.DB, q *queue.Queue, logger *log.Logger) (*cron.Scheduler, error) {
s := cron.NewScheduler(
cron.WithLogger(logger),
cron.WithOnComplete(func(r cron.CompletedRun) {
// persist run history to cron_runs table
}),
)
// register jobs here
lc.Append(lifecycle.Hook{
OnStart: s.Start,
OnStop: s.Stop,
})
return s, nil
}
Adding a simple cron job
Add a new s.Add() call inside provideCron, before the lifecycle hook:
if err := s.Add("cleanup-old-products", "0 2 * * *", func(ctx context.Context) error {
sql, args := sqlite.Delete("products").
Where("deleted_at IS NOT NULL").
Where("deleted_at < ?", time.Now().Add(-30*24*time.Hour).UTC().Format(time.RFC3339)).
Build()
result, err := db.Exec(sql, args...)
if err != nil {
return err
}
if result.RowsAffected > 0 {
logger.Info("purged old products", log.Int64("count", result.RowsAffected))
}
return nil
}); err != nil {
return nil, fmt.Errorf("cron add cleanup-old-products: %w", err)
}
This runs daily at 2:00 AM UTC, removing products that were soft-deleted more than 30 days ago.
Cron expression quick reference
Format: minute hour day-of-month month day-of-week
| Expression | Schedule |
|---|---|
* * * * * | Every minute |
*/5 * * * * | Every 5 minutes |
0 * * * * | Every hour at :00 |
30 * * * * | Every hour at :30 |
0 9 * * * | Daily at 9:00 AM |
0 2 * * * | Daily at 2:00 AM |
0 0 * * 1 | Every Monday at midnight |
0 9 1 * * | First of each month at 9:00 AM |
Combining cron with the job queue
For long-running work, the cron job should enqueue a queue job rather than doing the work directly. This keeps the cron tick fast and lets the queue handle retries:
if err := s.Add("generate-daily-report", "0 8 * * *", func(ctx context.Context) error {
payload, _ := json.Marshal(map[string]string{
"date": time.Now().UTC().Format("2006-01-02"),
})
_, err := q.Enqueue(ctx, "generate-report", payload)
return err
}); err != nil {
return nil, fmt.Errorf("cron add generate-daily-report: %w", err)
}
The cron job fires at 8:00 AM and enqueues the work. A separate queue handler (see Queue jobs) processes it with retries.
Run history
If WithOnComplete is configured on the scheduler, every job execution is recorded in the cron_runs table. The admin panel shows this history automatically — last run time, duration, success/failure status, and error messages.
Runtime management
Cron jobs can be managed through the admin API without restarting:
GET /api/admin/cron — list all jobs with status
POST /api/admin/cron/{name}/trigger — run a job immediately
POST /api/admin/cron/{name}/enable — resume scheduled runs
POST /api/admin/cron/{name}/disable — pause scheduled runs
GET /api/admin/cron/{name}/runs — view execution history
Built-in cron jobs
The standalone app ships with five maintenance cron jobs:
| Name | Schedule | Purpose |
|---|---|---|
purge-completed-jobs | 0 * * * * | Remove completed queue jobs older than 24h |
purge-expired-tokens | 30 * * * * | Delete expired refresh tokens |
purge-stale-api-keys | 0 3 * * * | Remove revoked API keys older than 30 days |
purge-old-cron-runs | 30 3 * * * | Delete run history older than 7 days |
purge-old-audit-log | 0 4 * * * | Archive audit entries older than 90 days |
These keep the SQLite database lean. Add your own jobs following the same pattern.
Tips
- Jobs must be added before
s.Start()is called. - Each job name must be unique — duplicates are rejected.
- The
ctxpassed to your job function is cancelled when the scheduler stops, so respect context cancellation for graceful shutdown. - A job that's already running won't be triggered again by the scheduler until it finishes.
- Log important outcomes — cron jobs run silently. Use
logger.Info()orlogger.Error()to make them observable.