Tasks & Scheduler
The task-executor runs long jobs off the render isolate — there is no render deadline and full I/O concurrency is available. Unlike the Queue, tasks have built-in status, progress, and results. The scheduler triggers work on a cron schedule.
The warm task-executor
A worker process is spawned warm and reused. You author tasks with defineTask and boot
the worker with startTaskWorker 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();
The task ctx
ctx spreads the full BextSdk plus task-specific
fields:
On ctx |
What it is |
|---|---|
ctx.kv, ctx.db, ctx.queue, … |
The full BextSdk (all SDK namespaces). |
ctx.appId |
The owning app id. |
ctx.jobId |
This job's id. |
ctx.signal |
An AbortSignal — aborted on cancel/timeout. |
ctx.progress(data) |
Report progress (readable via /tasks/status). |
ctx.log(...) |
Log from the task. |
startTaskWorker() binds a loopback port and prints BEXT_TASK_READY <port> on stdout;
the Rust supervisor spawns it with bun run and keeps it warm.
Running a task
Invoke a task from a route (or anywhere on the host) with POST /__bext/sdk/tasks/run:
curl -s http://127.0.0.1/__bext/sdk/tasks/run \
-H "X-Bext-App-Id: <app-id>" \
-H "Content-Type: application/json" \
-d '{"task":"generate-report","payload":{"month":"2026-06"}}'
# -> 202 {"jobId":"…","status":"queued"}
| Endpoint | Body / query | Notes |
|---|---|---|
/tasks/run |
{ task, payload?, timeout_secs?, trigger? } |
202 { jobId, status: "queued" } |
/tasks/progress |
{ id, data } |
Report progress (also via ctx.progress). |
/tasks/status |
?id= |
Returns the job record (below). |
/tasks/list |
?limit= |
Recent jobs. |
/tasks/cancel |
{ id } |
Cancel a job (aborts ctx.signal). |
/tasks/worker/register |
— | Register a task worker. |
/tasks/worker/unregister |
— | Remove a task worker. |
/tasks/status?id= returns:
{
"id": "…", "task": "generate-report", "status": "…", "progress": {},
"result": {}, "error": null, "trigger": "…",
"createdAt": "…", "updatedAt": "…", "finishedAt": "…"
}
The scheduler (cron)
The scheduler registers recurring jobs. Cron expressions are validated — a bad expression
returns 400.
| Endpoint | Body / notes |
|---|---|
/scheduler/register |
{ name, schedule|cron, kind?, command|handler_url|task, payload?, cwd?, timeout_secs?, enabled? } |
/scheduler/list |
GET |
/scheduler/cancel |
POST |
kind is inferred from what you provide:
| You supply | Inferred kind |
|---|---|
task |
task-executor (runs a defineTask) |
handler_url |
http (POSTs the URL) |
| neither | command (runs a shell command) |
curl -s http://127.0.0.1/__bext/sdk/scheduler/register \
-H "X-Bext-App-Id: <app-id>" \
-H "Content-Type: application/json" \
-d '{"name":"nightly-report","cron":"0 3 * * *","task":"generate-report","payload":{"month":"current"}}'
Prefer the bext scheduler over system cron. Route recurring work through the scheduler
(or the task-executor) rather than a host crontab — it is app-scoped, cron expressions are
validated, and jobs are visible via /scheduler/list. See Scheduled Jobs.
Tasks vs queue vs scheduler — which to use
| 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 a public handler. At-least-once, no result store. |
| Scheduler | Time-triggered work — cron. Fires a task, an HTTP handler, or a command on a schedule. |
Where to next
| Page | What it covers |
|---|---|
| Queue | Fire-and-forget delivery and workers |
| KV Store | Persist results / job records |
| Scheduled Jobs | Operating cron jobs on the platform |