Cron Jobs Run code on a schedule. The platform calls a path of your app on a cron schedule; your app handles it in its normal handler. Typical uses: send reminder emails, clean up stale rows, refresh a cache, send a daily digest. There is no separate "cron handler" — a cron run is just an HTTP request to your app that carries an auth token and a marker header. Verify it with isCronRequest(req). Two ways to define jobs 1. Declarative — onvibe.json (recommended, version-controlled) Add an onvibe.json file at the project root. It is reconciled on every deploy: jobs are created/updated, and jobs removed from the file are deleted. { "crons": [ { "name": "reminders", "schedule": "0 9 * * *", "path": "/cron/reminders", "timezone": "Europe/Madrid" } ] } - name: unique per project, lowercase letters/digits/hyphens. - schedule: standard 5-field cron (min hour day-of-month month day-of-week). - path: a path of your app to call, must start with /. - method: POST (default) or GET. - timezone: IANA name (e.g. Europe/Madrid); defaults to UTC. 2. Imperative — create_cron tool For dynamic management. Jobs created this way are independent from onvibe.json (they are not removed by a deploy). create_cron({ project_id, name, schedule, path, method?, timezone? }) list_crons({ project_id }) delete_cron({ project_id, name }) run_cron({ project_id, name }) // trigger once now, for testing Handling the trigger in your app Gate the cron logic with isCronRequest(req) so public traffic can't trigger it. import { isCronRequest } from "./.onvibe/helpers.ts"; export default async function handler(req: Request): Promise { const url = new URL(req.url); if (url.pathname === "/cron/reminders" && isCronRequest(req)) { await sendReminders(); // scan DB, send emails, etc. return new Response("ok"); } // ...rest of the app } isCronRequest(req) returns true only when the request carries the X-Onvibe-Cron header AND the correct APP_TOKEN bearer (set by the platform). cronName(req) returns the job name if you serve several jobs from one path. Limits & semantics - Granularity: 1 minute (standard cron). No sub-minute schedules. - Per-run timeout: ~60s. Keep jobs short; for heavy work, do it in batches. - Overlap: if a run is still going when the next is due, the new one is skipped. - Missed runs: after downtime, a job fires once (missed slots are not replayed), then resumes its normal schedule. - At-least-once: design handlers to be idempotent (a run may rarely repeat). Example: daily expiry reminders { "crons": [{ "name": "reminders", "schedule": "0 9 * * *", "path": "/cron/reminders", "timezone": "Europe/Madrid" }] } import { isCronRequest } from "./.onvibe/helpers.ts"; async function handler(req: Request): Promise { const url = new URL(req.url); if (url.pathname === "/cron/reminders" && isCronRequest(req)) { const { rows } = await pool.query( "SELECT email, name FROM items WHERE expires_on = CURRENT_DATE + 3", ); for (const r of rows) { // send an email to r.email (see onvibe://docs/email when available) } return new Response(sent ${rows.length}); } return new Response("not found", { status: 404 }); } export default handler; Notes - Test a job without waiting for its schedule with run_cron (or just hit the path yourself — but it will return early unless isCronRequest passes). - A job declared in onvibe.json will reappear on the next deploy even if you delete it via delete_cron; remove it from onvibe.json to delete it permanently. - See the job's recent runs and next run with list_crons.