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

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

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

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