Skip to content

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. timeout sets a shorter limit: "10s", "5000ms" or a number of milliseconds. A run that passes it is stopped with cron_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 its input (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.scheduledFor to 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