Quickstart
A durable queue, a cron schedule, and a crash-proof workflow in one file — backed by a single SQLite database.
Install#
Scaffold a new project (recommended):
Or add ZenZip to an existing project:
Package name
ZenZip publishes to npm as zenzipjs — the zenzip name is reserved on the registry. The import and public API are still zenzip: import { zenzip } from "zenzipjs". Prebuilt native binaries ship for Windows, macOS (x64 + arm64), and Linux (glibc + musl, x64 + arm64), so npm install needs no Rust toolchain.
There is nothing else to install. No Redis, no Docker compose, no broker. The runtime stores everything in .zenzip/zenzip.db — a SQLite database in WAL mode, created on first start.
Your first app#
One file demonstrating all three primitives shipped so far:
import { zenzip } from "zenzipjs";
const app = zenzip({ dataDir: ".zenzip" });
// 1. A durable queue — retries with backoff, dead-letters when exhausted.
const emails = app.queue<{ to: string; subject: string }>("emails", {
concurrency: 5,
retries: 3,
backoff: { delay: "500ms", maxDelay: "5s" },
});
emails.process(async (job) => {
console.log(`sending to ${job.data.to} (attempt ${job.attempt})`);
if (Math.random() < 0.25) throw new Error("smtp: connection reset");
});
// 2. A persisted schedule — survives restarts, timezone-aware.
app.schedule("digest", { every: "10s" }, async () => {
await emails.push({ to: "digest@example.com", subject: "Your digest" });
});
// 3. A durable workflow — every step is journaled.
const onboard = app.workflow<{ email: string }, string>(
"onboard",
async ({ step, input }) => {
await step.run("welcome", () =>
emails.push({ to: input.email, subject: "Welcome!" }),
);
await step.sleep("wait", "5s"); // survives kill -9
await step.run("follow-up", () =>
emails.push({ to: input.email, subject: "How's it going?" }),
);
return "onboarded";
},
);
await app.start();
await emails.push({ to: "you@example.com", subject: "Hello from ZenZip" });
await onboard.trigger({ email: "you@example.com" });Three things worth noticing:
- Definition before start. Queues, schedules, and workflows are declared first;
app.start()boots the Rust runtime with everything registered. Pushing or triggering before start throws a clear error. - Throw = retry. The flaky email handler above will be retried with exponential backoff up to its retry budget, then dead-lettered — inspect with
emails.deadJobs(). - Typed payloads.
app.queue<T>andapp.workflow<I, O>flow types end to end. Add a zod schema via theschemaoption for runtime validation on push.
Run it#
You'll see the five welcome emails process (some retrying), the digest schedule firing every 10 seconds, and the onboarding workflow sending its follow-up 5 seconds after the welcome.
// Graceful shutdown: stops claiming, drains in-flight handlers.
// SIGINT/SIGTERM handlers are installed automatically (opt out with
// handleSignals: false).
const clean = await app.stop({ timeout: "10s" });Now kill it#
While it runs, kill the process — not Ctrl-C, actually kill it (taskkill /F on Windows, kill -9 elsewhere). Then start it again:
- In-flight queue jobs are redelivered after their lease expires, with the failed attempt counted against the retry budget.
- A workflow mid-sleep resumes at the right time; its completed steps fast-forward from the journal and never re-execute.
- The schedule's next fire time was persisted — missed ticks follow your
catchuppolicy.
This is tested, not aspirational
The repository's CI includes harnesses that SIGKILL worker processes mid-job and mid-workflow (four times in a row, at random points) and assert correct recovery. See Durability & Semantics.
Next steps#
- Queues — retries, DLQ, rate limits, batch consumers
- Workflows — the full step API:
run,sleep,waitForEvent,invoke,all - Durability & Semantics — the guarantees contract
- Configuration — every option on
zenzip()