Skip to content
pilots
Dashboard
All pages

Scheduled jobs

A cron here is a request on a schedule. The platform calls the path at the right minute, waking the machine if it is asleep, and the machine sleeps again afterwards. A job that runs for a minute a day costs a minute a day.

Declaring one

compose.pilots.yaml
x-pilots:
  schedules:
    - cron: "0 5 * * *"        # five fields, UTC, or @hourly @daily @weekly @monthly
      path: /jobs/digest
    - cron: "@hourly"
      cmd: /app/bin/tick       # a command, for work with no route

Five fields and UTC, or one of the named expressions. A path job is an ordinary request. A cmd job runs a command instead, for work that has no route, and it runs as the app user from that user's home, so spell the path out in full.

Or in the config your app already has

A cron needs no pilots-specific file. A vercel.json is read for any app, whatever it is written in and whether or not it brought a Dockerfile, so a Rails or Django service carrying one gets its crons too.

vercel.json
{
  "crons": [
    { "path": "/api/digest", "schedule": "0 5 * * *" }
  ]
}

A webjs app can carry the same list in its own manifest, and that one wins where both are present.

package.json
{
  "webjs": {
    "crons": [
      { "path": "/jobs/digest", "schedule": "0 5 * * *" }
    ]
  }
}

What the handler sees

A GET carrying a header that says which expression fired. The public edge strips that header from every request arriving from outside, so checking for its presence is the whole authentication, with no secret to store or rotate.

export function GET(req) {
  if (!req.headers['x-pilot-cron']) return new Response('no', { status: 403 })
  return runDigest()
}

What to build for

  • A job can fire twice in rare cases, a host restart or a deploy inside its minute, so make it idempotent.
  • A job still running when its next minute comes is skipped rather than overlapped.
  • One replica fires for a service, however many replicas it has.
  • A path job needs a machine that can wake, so auto_start: false together with the default suspend behaviour is refused at create: the job could never run. A cmd job is not a request and takes neither.
  • To remove every cron, deploy with an empty list. An absent key keeps the previous release's schedules.

On a sandbox

A machine that is not a service takes a schedule of its own at create.

pilot machine create nightly --schedule "0 5 * * * GET /jobs/digest"
pilot machine create backup --schedule "@hourly /usr/local/bin/backup.sh"
You want Reach for
A job with a route, in a service schedules with a path
A job with no route schedules with a cmd
Something that must run continuously rather than on a schedule a resident replica (services and releases)