Cron jobs are scheduled background tasks that run on a timer, independent of HTTP requests. Write them in Dart, and Zonai compiles them into the crons worker.
Use cron jobs for: purging old data, sending periodic digests, flagging stale records, running maintenance tasks.
Cron mutations that change rows will also push updates to any open /db/stream* / db.listen subscriptions watching those queries. See Streaming.
How Jobs Run#
The cron worker runs each job on its configured Schedule. Jobs run one at a time — if a job is still running when its next scheduled time arrives, that run is skipped (see
Catch-Up Logic).
Errors thrown from run() are caught, logged, and do not crash the server.
The CronJwt Identity#
When a cron job calls get, mutate, or email, it uses an internal system identity. This identity has admin-level access — it passes most authorization checks by default, but rules still execute.
The _cron_jobs Table#
Zonai records each job run in the internal _cron_jobs table:
- Job name, start time, end time
- Status: success or error
- Error message if the job failed
Query this table to audit job history or debug failures.
Running on Demand#
Each job's name property is its stable identifier for on-demand runs — not only for _cron_jobs
history. Trigger a job immediately by name, without waiting for its schedule:
-
Dev TUI:
zonai dev, then pressjto run a job by name - HTTP API:
POST /crons/run?name=<name>(admin JWT required)
See Running Jobs Manually for details.
Creating a Job#
Create a file in cronsPath (any name), extend CronJob, and export a main()
function:
import 'package:zonai_schema/zonai_schema.dart';
final class CleanupOldLogsJob extends CronJob {
CleanupOldLogsJob()
: super(name: 'cleanup-old-logs', schedule: Schedule.parse('0 3 * * *'));
@override
Future<void> run() async {
final cutoff = DateTime.now().subtract(const Duration(days: 30));
mutate.delete.many(
tableName: '_log',
updates: [],
where: Lt('created_at', cutoff),
);
logger.info('Queued deletion of logs older than $cutoff');
}
}
CleanupOldLogsJob main() => CleanupOldLogsJob();
