SISuperintelligenceDocs

Search docs

Search every page of the documentation.

Configuration

Crons

Call a route of your production deployment on a schedule.

Declare crons

Add them to si.json in the project's root directory and push to the production branch:

si.json
{
  "crons": [{ "path": "/api/cron/cleanup", "schedule": "cron(0 3 * * ? *)" }]
}

When the deployment becomes production, each entry becomes a schedule. Crons need a server, so they're for Next.js projects.

Schedule expressions

Schedules use EventBridge Scheduler expressions, in UTC:

ExpressionRuns
rate(5 minutes)Every 5 minutes
rate(1 hour)Every hour
rate(1 day)Every day
cron(0 3 * * ? *)Every day at 03:00
cron(0/15 * * * ? *)Every 15 minutes, on the quarter hour
cron(0 12 ? * MON-FRI *)Weekdays at 12:00
cron(0 9 1 * ? *)09:00 on the first of each month

cron(…) takes six fields: minutes, hours, day of month, month, day of week and year. One of day of month and day of week must be ?. rate(…) takes a number and minute(s), hour(s) or day(s).

A run can start up to 5 minutes after its scheduled time.

The request

At each run, the platform calls your current production deployment at its own deployment hostname:

POST /api/cron/cleanup HTTP/1.1
Host: <project>-<id>-<org>.deployments.dev.gov.vin
x-si-cron: <SI_CRON_SECRET>
user-agent: si-cron
  • There's no body.
  • The platform waits up to 120 seconds, but the server function stops after 30.
  • A response of 500 or above is retried up to 2 more times. Any other status counts as done.
  • Nothing is called while the project has no production deployment.

Check the secret

The route is public like the rest of your deployment, so check the x-si-cron header against SI_CRON_SECRET, which the platform sets on every server function:

app/api/cron/cleanup/route.ts
import { timingSafeEqual } from "node:crypto"

function fromCron(request: Request) {
  const sent = Buffer.from(request.headers.get("x-si-cron") ?? "")
  const secret = Buffer.from(process.env.SI_CRON_SECRET ?? "")
  return secret.length > 0 && sent.length === secret.length && timingSafeEqual(sent, secret)
}

export async function POST(request: Request) {
  if (!fromCron(request)) return new Response("Unauthorized", { status: 401 })
  // … the scheduled work
  return Response.json({ ok: true })
}

The secret belongs to the project and stays the same across deployments.

Crons follow production

Every deployment records its crons. When a deployment becomes production (a production push, a promote or a rollback), its crons replace the project's schedules, so schedules always match the code that's serving. Preview deployments never receive cron requests.

Planned

  • Pausing a project's crons in a way that survives deploys, and seeing the next run and last result. Coming soon