A minimal, standalone task executor that runs tasks on the runtime backend picked at build time. Unlike the full OpenWorkers runner, this executor:
- Has minimal dependencies
- Only implements
fetch()(no KV, Storage, Database bindings) - Supports multiple task sources: CLI, NATS, PostgreSQL
Perfect for background jobs, scheduled tasks, or lightweight serverless workloads.
Exactly one backend feature must be selected; the build fails otherwise.
| Backend | Feature | Snapshots | Guest fetch() |
Known limitations |
|---|---|---|---|---|
| V8 | v8 |
yes | yes | Needs a prebuilt librusty_v8 or a V8 source build |
| JSC | jsc |
no | yes | System JavaScriptCore; a bridge wires export default to the task listener |
| QuickJS | quickjs |
no | yes | Task rides on a fetch event; enforces no CPU limit |
| Boa | boa |
no | yes | Task rides on a fetch event; enforces no CPU limit; bodies round-trip as lossy UTF-8 |
| Nova | nova |
no | no | No host operations at all, and no timers; the deadline is not enforced; no Temporal, see below |
| Wasm | wasm |
no | yes | Guests are wasi:http/proxy components, not JavaScript; see Wasm guests |
One lockfile serves every backend, so nova gives up Temporal: v8 152 needs
temporal_capi >= 0.2.3, which pulls icu_calendar 2.2.1, while the temporal_rs 0.1.2
behind nova_vm 1.0's default temporal feature compiles only against icu_calendar 2.1.
nova_vm 1.0.0 in turn does not build without that feature, which is why the
backend takes its engine from
openworkers/nova,
the 1.0.0 release plus the seven cfg attributes that gate it.
| Feature | Description | Default |
|---|---|---|
nats |
NATS message queue listener | yes |
database |
PostgreSQL queue with pg_notify | no |
# Build against a backend
cargo build --release --features v8
# Build with database support
cargo build --release --features v8,database
# The V8 build takes a prebuilt archive rather than building V8 from source
RUSTY_V8_ARCHIVE=~/rusty-v8-prebuilt/librusty_v8_ptrcomp_release_aarch64-apple-darwin.a \
RUSTY_V8_SRC_BINDING_PATH=~/rusty-v8-prebuilt/src_binding_ptrcomp_release_aarch64-apple-darwin.rs \
cargo build --release --features v8For optimal performance and stability, generate a V8 snapshot before running:
cargo run --features v8 --bin snapshotThis creates a snapshot at /tmp/openworkers-runtime-snapshot.bin containing pre-compiled JavaScript APIs (URL, Headers, Request, Response, etc.).
Without a snapshot:
- Slower cold starts (~2-3ms vs ~100us)
- APIs are compiled on every request
Docker: The Docker image includes the snapshot automatically.
The other backends have no snapshot support, and the snapshot binary is only built with --features v8.
Execute a single JavaScript file:
# Simple execution
task-executor run script.js
# With JSON payload
task-executor run script.js --payload '{"name": "world"}'
# With timeout (ms)
task-executor run script.js --timeout 5000
# Quiet mode (suppress console.log)
task-executor run script.js --quietExample script:
export default {
async task(event) {
const response = await fetch("https://api.example.com/data");
const data = await response.json();
return {
input: event.payload,
result: data,
};
},
};Listen for tasks on a NATS subject:
task-executor listen \
--nats nats://localhost:4222 \
--subject tasks \
--root ./workers \
--timeout 30000{
"script": "hello.js",
"payload": { "name": "world" },
"timeout": 5000
}Note: Scripts must exist in the
--rootdirectory. Nested paths like"script": "workers/task.js"are allowed.
Listen for tasks from a PostgreSQL table using pg_notify:
task-executor db-listen \
--database-url postgres://user:pass@localhost/mydb \
--table ow_tasks \
--root ./workers \
--timeout 30000| Variable | Description | Default |
|---|---|---|
DATABASE_URL |
PostgreSQL connection URL | (required) |
TASK_TABLE |
Name of the tasks table | ow_tasks |
Apply this schema to your database (adjust table name as needed):
CREATE TABLE ow_tasks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
script TEXT NOT NULL,
payload JSONB,
status TEXT NOT NULL DEFAULT 'pending',
result JSONB,
error TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ
);
-- Index for efficient pending task lookup
CREATE INDEX idx_ow_tasks_pending ON ow_tasks(created_at) WHERE status = 'pending';
-- Notification trigger
CREATE OR REPLACE FUNCTION notify_ow_task_created() RETURNS TRIGGER AS $$
BEGIN
PERFORM pg_notify('ow_tasks_created', NEW.id::text);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER ow_task_notify_insert
AFTER INSERT ON ow_tasks
FOR EACH ROW EXECUTE FUNCTION notify_ow_task_created();Note: If you use a custom table name, update the trigger to notify on
{table_name}_created.
-- Script must exist in the --root directory
INSERT INTO ow_tasks (script, payload)
VALUES ('hello.js', '{"name": "world"}');
-- Nested paths are allowed
INSERT INTO ow_tasks (script, payload)
VALUES ('workers/process.js', '{"data": [1, 2, 3]}');- pending -> Task created, waiting for pickup
- running -> Task claimed by executor,
started_atset - completed -> Success,
resultcontains return value - failed -> Error,
errorcontains message
Scripts export a default object with a task(event, env, ctx) method. Module syntax is
lowered to a classic script before evaluation, so export default becomes
globalThis.default.
export default {
async task(event, env, ctx) {
// event.payload is the JSON payload passed to the task
// event.taskId, event.source and event.attempt describe the invocation
// Use fetch() for HTTP requests
const response = await fetch("https://api.example.com");
// Return value is stored as the task result
return {
status: "done",
data: await response.json(),
};
},
};A listener works too, and takes precedence over the default export:
addEventListener("task", (event) => {
event.respondWith({ success: true, data: { received: event.payload } });
event.waitUntil(reportLater());
});A returned value that carries a boolean success is taken as the task result itself;
anything else becomes its data.
The wasm backend runs WebAssembly components, not JavaScript. A component has no task
export, so a task is delivered as a synthetic POST http://task.invalid/ to
wasi:http/incoming-handler: the payload is the request body, the response body is the
task result, and x-ow-task-id, x-ow-task-attempt, x-ow-task-source and
x-ow-task-origin carry the metadata. Outbound wasi:http/outgoing-handler requests go
through the same fetch handler as the JavaScript backends.
(cd examples/task-worker && cargo build --target wasm32-wasip2 --release)
cargo run --features wasm --bin task-executor -- \
run examples/task-worker/target/wasm32-wasip2/release/task_worker.wasm \
--payload '{"n":21}'What a guest gets depends on the backend; this is the V8 set.
| API | Description |
|---|---|
fetch() |
Standard Fetch API for HTTP requests |
Request / Response |
Fetch API request/response classes |
Headers |
HTTP headers manipulation |
URL / URLSearchParams |
URL parsing and manipulation |
TextEncoder/Decoder |
UTF-8 encoding/decoding |
atob() / btoa() |
Base64 encoding/decoding |
crypto.randomUUID() |
Generate random UUIDs |
crypto.getRandomValues |
Cryptographic random values |
AbortController |
Request cancellation |
Blob / FormData |
Binary data and form handling |
console.log/warn/error |
Logging (printed to stderr) |
setTimeout/setInterval |
Timers (within task execution) |
Enable debug logging with:
RUST_LOG=debug task-executor run script.jsThe database listener supports running multiple instances concurrently. Tasks are claimed using SELECT FOR UPDATE SKIP LOCKED, ensuring each task is processed exactly once.
# Terminal 1
task-executor db-listen --database-url $DATABASE_URL
# Terminal 2
task-executor db-listen --database-url $DATABASE_URL
# Both will process tasks without conflicts# Run the suite against a backend
cargo test --features v8
# The wasm tests need the example component built first
(cd examples/task-worker && cargo build --target wasm32-wasip2 --release)
cargo test --features wasm
# Run only database tests (requires PostgreSQL)
cargo test --features v8,database db_Database tests use .env.test for configuration:
# .env.test
DATABASE_URL=postgres://postgres:postgres@localhost/postgresTests create isolated tables (test_tasks_{uuid}) and clean up automatically.
MIT