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). Defining jobs: onvibe.json Jobs are declared, never created imperatively. 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. Changing jobs: download → edit → deploy onvibe.json holds the complete set of jobs, so never write it from memory: get_config({ project_id }) // the live onvibe.json + its version lock // edit the crons array, KEEP version as-is deploy({ project_id, files: [{ path: "onvibe.json", content: edited }] }) The deploy is rejected (CONFIG_OUT_OF_DATE) if the config moved since you downloaded it — call get_config again, re-apply your edit and redeploy. See onvibe://docs/config. Two runtime tools complete the picture (they don't define jobs): run_cron({ project_id, name }) // trigger once now, for testing get_project({ project_id }) // schedule, next_run and last_run of each job 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). - To delete a job, remove it from the crons array and deploy — the deploy makes the live jobs match the file exactly. - See each job's schedule, next run and last run with get_project.