# 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.

```json
{
  "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.

```typescript
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

```json
{ "crons": [{ "name": "reminders", "schedule": "0 9 * * *", "path": "/cron/reminders", "timezone": "Europe/Madrid" }] }
```

```typescript
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 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`.
