# Cron jobs
Source: https://sovaria.in/docs/cron-jobs

Run a function, call an HTTPS endpoint or run SQL on a schedule, in your own time zone, with retries, overlap control and a run history.

A cron job runs something on a schedule: a deployed **function**, an **HTTPS request** to any public URL, or one **SQL statement** on your database. Each run is a durable background job with retries, a timeout and a history (succeeded, failed, skipped, missed), so a job never runs twice for the same time even when several servers see it due.

(Diagram: How a cron job runs)

## Schedule expressions

Five fields — `minute hour day-of-month month day-of-week` — or a macro. The console and `sov functions schedules preview` show the next runs before you save.

| Expression | Runs |
|---|---|
| `*/5 * * * *` | Every 5 minutes |
| `@hourly` (`0 * * * *`) | At the start of every hour |
| `0 9 * * MON-FRI` | At 09:00 on weekdays |
| `30 2 1 * *` | At 02:30 on the 1st of every month |
| `0 9 1 * MON` | At 09:00 on the 1st **and** on every Monday (day of month and day of week together mean either) |
| `@daily`, `@weekly`, `@monthly`, `@yearly` | Midnight every day, Sunday, 1st, 1 January |

- Lists `1,15`, ranges `1-5`, steps `*/10`, `10-50/20` and `5/15` (from 5, every 15); names `JAN`–`DEC` and `SUN`–`SAT`; `7` is Sunday too.
- Dates that never happen (`0 0 30 2 *`) are refused when you save; rare ones (`0 0 29 2 *`) are fine.
- **Time zones**: pick any IANA zone (`Asia/Kolkata`, `Europe/London`, `America/New_York`); the default is UTC. Daylight saving is handled: a time skipped when clocks go forward runs once just after the change (02:30 runs at 03:30), a time that happens twice runs once — except expressions with `*` in the hour field, which keep their interval through the repeated hour.

## Create a cron job

CLI:
```bash
sov functions schedules preview '0 9 * * MON-FRI' --tz Asia/Kolkata
sov functions schedules create '0 3 * * *' -f cleanup --tz Asia/Kolkata --overlap skip --name "Nightly cleanup"
sov functions schedules create @hourly --url https://hooks.example.com/ping -H 'Authorization: @secret:HOOK_TOKEN'
sov functions schedules create '*/15 * * * *' --sql 'SELECT refresh_leaderboard()' --timeout 20
sov functions schedules run <schedule-id>        # run it now
sov functions schedules runs <schedule-id>       # history
```

JavaScript:
```js
await sov.api.schedules.create({
  cronExpression: '0 3 * * *', timezone: 'Asia/Kolkata', functionName: 'cleanup',
  payload: { olderThanDays: 30 }, overlap: 'skip', maxAttempts: 3, name: 'Nightly cleanup',
})
const { next_runs } = await sov.api.schedules.preview({ cronExpression: '@daily', timezone: 'Europe/London' })
```

Python:
```python
await client.api.schedules.create(
    cron_expression="@hourly", target_type="http",
    target={"url": "https://hooks.example.com/ping", "method": "POST",
            "headers": {"Authorization": {"$secret": "HOOK_TOKEN"}}, "body": {"source": "cron"}},
)
```

HTTP:
```http
POST  /api/v1/fn/schedules            {"cron_expression": "0 3 * * *", "timezone": "Asia/Kolkata",
                                       "function_name": "cleanup", "payload": {...}, "overlap": "skip"}
POST  /api/v1/fn/schedules/preview    {"cron_expression": "0 9 * * MON-FRI", "timezone": "UTC", "count": 5}
PATCH /api/v1/fn/schedules/{id}       {"enabled": false}           # only the fields sent change
POST  /api/v1/fn/schedules/{id}/run                                # run now
GET   /api/v1/fn/schedules/{id}/runs?limit=50&before=…
```

Console:
```bash
# Cron Jobs → New cron job (or a function → Schedules)
```

## Targets

| Target | Settings | How it runs |
|---|---|---|
| Function | `function_name`, `payload` (the event) | Like a background call. `context.caller.role` is `"schedule"`; `context.schedule` has `id`, `name`, `scheduled_at`, `manual`, `attempt`; `context.idempotency_key` is `sched:<id>:<slot>`. |
| HTTPS | `url`, `method`, `headers`, `body`, `timeout_sec` | Leaves through the egress proxy: public host names only (private, loopback and metadata addresses are refused). A non-2xx answer is a failed try; the status and the first 2 KB of the answer are kept. |
| SQL | `sql`, `params`, `key_secret` | One SELECT / INSERT / UPDATE / DELETE (call a database function with `SELECT my_job()`) through your project's SQL API with the service-role key stored as the secret `key_secret` (default `SOVARIA_SERVICE_ROLE_KEY`). It runs as your project's database role — never a superuser — with `statement_timeout` set to the job's timeout. |

## Retries, timeouts and overlap

- **Tries per run** (`max_attempts`, default 3, up to your plan's tries per event): a failed try is retried with backoff; the last failure is kept under Dead letters.
- **Timeout** (`timeout_sec`): lowers the run's limit below the function's or plan's; for SQL it is the statement timeout.
- **Overlap** — `allow` (default) runs every slot; `skip` skips a slot while the previous run is still going and logs it as Skipped.
- **Run now** queues one run immediately without changing the regular times (it follows the overlap rule).
- Slots missed while the scheduler was down are logged once as **Missed**, and the latest one runs late — they are not replayed one by one.

## Secrets

Never put a key in a payload, header or body: write `{"$secret": "NAME"}` instead. The value is filled in when the job runs (from the function's secrets for functions, the project and environment secrets for HTTPS and SQL), and logs show only the name. Plain values under names like `password`, `token` or `api_key` are shown as `[redacted]`; sending `[redacted]` back on an edit keeps what is stored. Payloads are at most 256 KB.

## Environments and the kill switch

A schedule belongs to the environment it was made in and runs that environment's function; staging never lists or changes production's schedules. Deleting a function removes its schedules in that environment only. If Sovaria support switches background runs off for an organization (abuse, a runaway loop), its slots are skipped and logged until they are switched back on.

## Compared with Supabase Cron

|  | Sovaria | Supabase Cron (pg_cron + pg_net) |
|---|---|---|
| Targets | Function, HTTPS, SQL | SQL, database function, HTTP, Edge Function |
| Time zones | Any IANA zone, DST-aware | UTC only |
| Retries and timeouts | Per job, with dead letters | None built in |
| Overlap control | `skip` per job | None |
| Run now, preview, readable summary | Yes | No |
| History | Status incl. skipped and missed, duration, tries, response preview, logs | Status and output |
| Secrets | `$secret` references, redacted in logs | Vault lookups in SQL |

## Plans

|  | Free | Pro | Ultra | Team | Enterprise |
|---|---|---|---|---|---|
| Cron jobs per project (`fn.cron_jobs`) | 2 | 25 | 100 | 500 | 2,000 |
| Tries per run (`fn.trigger_max_attempts`) | 3 | 8 | 10 | 12 | 15 |

Each run counts as one invocation (`fn.invocations_month`) and, for functions, its compute. Run history is kept as long as your plan keeps function logs (`fn.log_retention_hours`). See [Plan limits](https://sovaria.in/docs/plan-limits).
