Scheduled Jobs

The SDK scheduler runs an app's recurring tasks. You register a named entry with a cron expression; on each run, the scheduler starts a task or sends a POST request to a route in your app. Each app can only see and cancel its own entries.

The framework's createSdk client has no scheduler method: as described in SDK → Tasks, entries are set up for your app by the platform. This page describes what an entry contains and what your code must do when it fires.

Operations

Operation Purpose Response
Register Create or update an entry (by its name) { "ok": true }
List List your app's entries, with their last run time { "tasks": [...] }
Cancel Remove an entry by its name { "cancelled": true }, or false if no entry with that name belongs to your app

Registering an existing name again replaces every field of the entry. This is also how you pause or resume it: register it with enabled: false, then enabled: true. On resume, occurrences missed while the entry was paused are caught up as well.

Defining an entry

Field Required Notes
name yes Entry name. Names are unique across the platform: a name already used by another app is refused with 400
schedule (or cron) yes The cron expression, evaluated in UTC
task or handler_url yes (one of them) A task declared with defineTask, or the URL of a route in your app to POST to. If both are given, task wins
payload no Data passed to the task on each run (task entries only)
timeout_secs no Maximum duration of one task run, in seconds (default 300). A value outside 1 to 1800 is brought back to the nearest bound
enabled no false to register a paused entry (default true)

A missing name or cron expression, or an invalid expression, is rejected with 400. Standard 5-field expressions are accepted, as are 6-field expressions with seconds first and the shortcuts @hourly, @daily (or @midnight), @weekly, @monthly and @yearly (or @annually). In the day-of-week field, both 0 and 7 mean Sunday. A new entry waits for its next occurrence: it does not fire at registration.

An entry that calls a route every day at 06:00 UTC:

{
  "name": "daily-report",
  "schedule": "0 6 * * *",
  "handler_url": "https://<app>.inklura.fr/api/cron/daily-report?key=<secret>"
}

And an entry that starts a task every night at 03:00 UTC:

{
  "name": "nightly-report",
  "schedule": "0 3 * * *",
  "task": "generate-report",
  "payload": { "month": "current" }
}

The task must be declared in your app's task worker (see SDK → Tasks). Its handler receives payload as its second argument, and the job is recorded with trigger: "cron".

Note

A handler_url entry needs scheduled HTTP callbacks to be enabled for your app, and the URL must start with one of the callback addresses allowed for it (on its own domain). It may not contain credentials or a fragment (#); a query string is accepted. Otherwise registration is refused with 403. These conditions are checked again before every run.

Listing entries

Each item in tasks includes:

Field Notes
name Entry name
cron The expression as registered
kind http (a handler_url entry) or task
handler_url The URL called, or null
timeout_secs The maximum duration in effect
enabled false if the entry is paused
registered_at When the entry was first registered (Unix timestamp in seconds)
last_run Last occurrence that ran successfully, or null

The call your route receives

With handler_url, the scheduler sends a POST request with a small JSON body:

{ "name": "daily-report", "app_id": "<your app>", "scheduled_for": 1767225600 }

scheduled_for is the time the request was sent, as a Unix timestamp in seconds. It is not the theoretical time of the occurrence: do not use it to recognise an occurrence you have already handled.

Your route has 60 seconds to respond. A 2xx response marks the run as done. Any other response (a redirect included), or no response, leaves that occurrence pending: it is tried again about 30 seconds later, with no limit on attempts, and later occurrences wait until it succeeds. A given entry never runs twice in parallel, but an occurrence can be delivered more than once, so make the route safe to run twice.

Missed occurrences, for example during a service interruption, are not lost: they are caught up one at a time, in order.

Securing the called route

The route is part of your app, so it is responsible for checking that the call is legitimate. The simplest convention is a shared secret in the query string: register the URL with ?key=<secret> and check that key before doing any work.

// src/app/api/cron/daily-report/route.ts
import { createSdk } from "@bext-stack/framework/sdk";

const sdk = createSdk("<app-id>");

export async function POST(req: Request) {
  const key = new URL(req.url).searchParams.get("key");
  if (!key || key !== (await sdk.secrets.get("CRON_SECRET"))) {
    return new Response("forbidden", { status: 403 });
  }
  // …run the job…
  return new Response("ok");
}

Store the value with your app's secrets rather than in code.

Warning

The secret is in the URL: keep it out of logs, rotate it if it leaks, and always use an HTTPS URL on your app's domain.

Long-running scheduled work

The scheduler is there to fire on time, not to run for long: a called route must respond within 60 seconds. If the work is heavy, schedule a task directly (the task field), or have your route add a message to a queue and respond right away.

Platform automations

Time-based steps in the platform's automations (delays in journeys, for example) are scheduled by the platform itself. You do not need to register anything for them.

Related