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.

Warning

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.

Tip

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