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) orGET.timezone: IANA name (e.g.Europe/Madrid); defaults toUTC.
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<Response> {
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<Response> {
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 unlessisCronRequestpasses). - A job declared in
onvibe.jsonwill reappear on the next deploy even if you delete it viadelete_cron; remove it fromonvibe.jsonto delete it permanently. - See the job's recent runs and next run with
list_crons.