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"}}'
Tip

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