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".
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.
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
- Queue Workers: background work with retries
- SDK → Tasks: long jobs with status and progress
- Events Overview