Cron jobs
Cron jobs run one of your functions on a schedule. Declare them in flatstack/cron.ts:
import { cron } from "@flatstack/server";
export default cron({
"0 * * * *": "cleanupExpired", // every hour, on the hour
"*/15 * * * *": { fn: "syncFeeds", timeout: "30s" }, // every 15 minutes
});
Each key is a schedule; each value names a function exported from flatstack/functions/*.ts, or gives the function and a timeout.
Schedules
Schedules have five fields: minute, hour, day of month, month and day of week. They’re always in UTC, so daylight saving never moves, skips or repeats a run: 30 1 * * * runs at 01:30 UTC every day, including the days clocks change.
| Field | Values | Names |
|---|---|---|
| Minute | 0–59 | |
| Hour | 0–23 | |
| Day of month | 1–31 | |
| Month | 1–12 | jan–dec |
| Day of week | 0–7 (0 and 7 are Sunday) | sun–sat |
Each field takes *, a value (5), a list (1,15), a range (9-17) and a step (*/10, 0-30/5, 5/20). When both day fields are set, a day matching either runs the job. @hourly, @daily (or @midnight), @weekly, @monthly and @yearly (or @annually) work too.
Limits
| Limit | Hobby | Pro | Studio |
|---|---|---|---|
| Shortest gap between a job’s runs | 15 minutes | 5 minutes | 1 minute |
Unclaimed sandboxes have Hobby’s limit. On every plan:
- A run can last up to 30 seconds.
timeoutsets a shorter limit:"10s","5000ms"or a number of milliseconds. A run that passes it is stopped withcron_timeout. - An environment can have up to 50 jobs.
- A run’s CPU time is limited as for requests (50, 200 or 500 ms by plan).
- The last 100 runs of each job are kept.
The gap is checked when you deploy: a schedule like */5 * * * * on Hobby stops the deploy with 422 cron_interval_too_short, with details.jobs listing each job’s shortest gap and details.minimumMinutes your plan’s. If you later move to a plan with a longer minimum, or roll back to a deploy whose jobs run more often, those jobs are skipped (see below) until you change their schedules.
How a run works
The scheduler checks every job each minute. A due job calls its function with no signed-in user: ctx.user is null, and ctx.cron holds the run’s schedule, scheduledFor (the UTC minute it was due) and attempt. Everything else in ctx works as in a request.
- The function must have
auth: "none"or"optional", and itsinput(if any) must accept no value, for example with.default(). Otherwise the deploy fails, since the job could never run. - What the function returns is the run’s output, stored as JSON. Fields named like secrets (
token,apiKey,password, …) are stored as[redacted], and output over 4 KB is cut. - A run that throws, times out or crashes is retried 3 times, after 30 seconds, 1 minute and 2 minutes. A run the function can’t accept (a missing function, or input it requires) isn’t retried. Use
ctx.cron.scheduledForto make a job safe to run twice.
When jobs don’t run
Jobs follow the same rules as requests to your app (How your app is served). The run is recorded as skipped, with the reason as an error code:
| Code | When |
|---|---|
account_locked |
A payment failed and your account is read-only. |
account_cancelled |
Your subscription ended and the account is in its read-only grace period. |
requests_capped, cpu_capped |
Your account used all its requests or CPU time this period. Jobs run again when the period resets. |
cron_interval_too_short |
The job runs more often than your current plan allows. |
project_deleted, not_deployed |
The project was deleted, or the environment has nothing deployed. |
A suspended account’s jobs don’t run and aren’t recorded. When database or file storage is full, jobs still run; writes that add data fail with 507, as in requests.
Deploying
flatstack deploy reads flatstack/cron.ts and sends its jobs with the bundle. The jobs go live with the deploy, in the same step as the code: a failed deploy leaves the previous jobs running. A job whose schedule stays the same keeps its run history; a job removed from cron.ts is deleted with its history. A deploy without cron.ts has no jobs. Rolling back puts back the jobs of the deploy you roll back to.
API
| Request | Who | What it returns |
|---|---|---|
GET /v1/projects/{id}/cron/jobs |
Everyone | { jobs }: each job’s id, environment, schedule, fn, timeoutMs, intervalMinutes, nextRunAt, lastRun and deployId. ?environment= narrows to one. |
GET /v1/projects/{id}/cron/runs |
Everyone | { runs, nextCursor }, newest first: each run’s id, jobId, environment, scheduledFor, status (running, retrying, succeeded, failed, skipped), attempts, durationMs, output, error ({ code, message } or null), nextAttemptAt, startedAt and finishedAt. Filter with ?environment=, ?jobId= and ?status=; ?limit= is 1–100 (default 50); pass nextCursor back as ?cursor= for the next page. |
A deploy token sees only its own project, and only its environment if it was created for one. The deploy API takes the jobs as cron (see Deploying).
As Markdown: apps/cron.md