Tasks & Scheduler
Tasks run long jobs outside page rendering: there is no render deadline and full I/O concurrency is available. Unlike the Queue, a task has built-in status, progress and a result. The scheduler triggers work at set times, from a cron expression.
Writing tasks
A task worker starts once and is reused from one job to the next. Declare your tasks with
defineTask, then start the worker with startTaskWorker, both imported from
@bext-stack/framework/task-worker:
import { defineTask, startTaskWorker } from "@bext-stack/framework/task-worker";
defineTask("generate-report", async (ctx, payload) => {
ctx.log("starting", payload);
const rows = await ctx.db.all("SELECT * FROM sales WHERE month = ?", [payload.month]);
await ctx.progress({ pct: 50 });
const url = await buildPdf(rows);
await ctx.kv.set(`report:${ctx.jobId}`, { url });
return { url }; // becomes the job's `result`
});
startTaskWorker();
Call defineTask at the module's top level, before startTaskWorker(). This file is a worker
entry point, not a route: it runs as its own long-lived process, outside page rendering. The
platform registers your app's worker entry and starts it on demand; running tasks must be enabled
for your app.
The task ctx
ctx carries the full BextSdk (see the SDK Overview) plus task-specific
fields:
On ctx |
Role |
|---|---|
ctx.kv, ctx.db, ctx.queue, … |
Every SDK namespace. |
ctx.appId |
Your app id. |
ctx.jobId |
This job's id, stable even if the job is replayed. |
ctx.signal |
An AbortSignal, aborted when the job's timeout is exceeded. Pass it to fetch and check it in your loops. |
ctx.progress(data) |
Publish progress (any JSON value), visible in the job status. Never throws. |
ctx.log(...) |
Write a log line from the task. |
ctx.sdk |
The same SDK client as a single object. |
A handler's return value becomes the job's result (null if the handler returns nothing). A
thrown error moves the job to the error status with its message.
Delivery is at-least-once: after an interruption, a job can be replayed with the same
ctx.jobId. Use ctx.jobId as an idempotency key before any external side effect (payment,
email, third-party API call).
Running a task and tracking its status
A task run takes these parameters:
| Parameter | Required | Role |
|---|---|---|
task |
yes | Name declared with defineTask |
payload |
no | Data passed to the handler as its second argument |
timeout_secs |
no | Maximum duration, 300 seconds by default and 1800 at most; past it, ctx.signal is aborted |
trigger |
no | Free-form label recording where the run came from ("api" by default, "cron" for scheduled runs) |
The framework client has no method to start a run: runs are started through the platform, for
example by a schedule. Starting a run answers immediately with
{ jobId, status: "queued" }. The work continues in the background; the job status can then be
read, recent jobs listed (50 by default, 500 at most), or a job canceled.
A job status looks like this:
{
"id": "…", "task": "generate-report", "status": "…", "progress": {},
"result": {}, "error": null, "trigger": "…",
"createdAt": "…", "updatedAt": "…", "finishedAt": "…"
}
status is one of queued, running, success, error, timeout or canceled. progress
holds the last value passed to ctx.progress, result the handler's return value, and error
the error message on failure.
Only a queued or running job can be canceled. Canceling moves the job to canceled without
stopping a handler that is already running or aborting ctx.signal: the handler runs to the end,
but its result is discarded and the status stays canceled. Finished jobs are kept for 7 days,
then deleted.
The scheduler
The scheduler registers recurring work. As with queue handlers, the framework client has no call to register a schedule: schedules are set up for your app through the platform. A schedule is defined by:
| Field | Role |
|---|---|
name |
Unique schedule name; a name already used by another app is rejected |
schedule (or cron) |
Cron expression, evaluated in UTC: 0 3 * * * fires at 03:00 UTC every day |
task or handler_url |
What fires: a task declared with defineTask, or a POST to a URL in your app |
payload |
Data passed to the task on each run (task schedules only) |
timeout_secs |
Maximum duration of one task run, 300 seconds by default and 1800 at most |
enabled |
Turns the schedule on or pauses it (on by default) |
Cron expressions take 5 fields, or 6 with seconds first, plus the @hourly, @daily (or
@midnight), @weekly, @monthly and @yearly (or @annually) shortcuts. For the day of the
week, 0 and 7 both mean Sunday. Expressions are validated on registration: an invalid
expression is rejected. Registering a name again updates that schedule. Schedules can be listed
and canceled by name.
An HTTP schedule POSTs this body to its handler_url, where scheduled_for is a Unix
timestamp in seconds:
{ "name": "nightly-report", "app_id": "<app-id>", "scheduled_for": 1767236400 }
The handler_url follows the same rule as queue handlers: a URL on your app's domain, among the
callback URLs the platform has authorised for your app.
Route all your recurring work through the scheduler: it is scoped to your app, validates cron expressions, and every schedule stays visible in the list. See Scheduled Jobs.
Tasks, queue or scheduler
| Use… | When |
|---|---|
| Tasks | A long-running job you want to track: status, progress and a result. No render deadline, full I/O concurrency. |
| Queue | High-volume, fire-and-forget delivery to an HTTP handler. At-least-once, no stored result. |
| Scheduler | Time-triggered work. Fires a task or calls an HTTP handler from a cron expression. |
Where to next
| Page | What it covers |
|---|---|
| Queue | Fire-and-forget delivery and handlers |
| KV Store | Keep results and job records |
| Scheduled Jobs | Scheduled work on the platform |