Concepts
The mental model in one page: one engine underneath everything, the vocabulary you'll meet, and which primitive to reach for.
One engine underneath everything#
ZenZip has exactly one execution engine. Every feature you use is a projection of it, which is why they all share the same crash recovery, retries, and observability:
| You write | Underneath, it's… |
|---|---|
app.queue().process() | leased, retryable units of work — the engine itself |
app.schedule() | a timer that enqueues onto a hidden queue when it fires |
app.workflow() | a run whose handler drives a step journal |
app.agent() | a workflow whose steps are generated by an LLM loop |
app.machine() | a guarded row + an event emitted on every transition |
Recovery is implemented once. The same lease sweep that redelivers a crashed queue job resumes a crashed workflow and a crashed agent. The Architecture page goes a level deeper; this page is the map.
Vocabulary#
The words that show up across the docs, the dashboard, and errors:
| Term | Meaning |
|---|---|
| Job | One unit of queued work. Has a payload, attempt count, and (while claimed) a lease. |
| Lease | A time-bound claim on a job/run. If the worker dies, the lease expires and the work is redelivered — this is how crashes recover. |
| Run | One execution of a workflow or agent. Has a status (running, sleeping, waitingEvent, waitingChild, completed, failed, cancelled). |
| Step | A durable unit inside a run (step.run / sleep / waitForEvent / invoke). Its result is journaled. |
| Journal | The recorded results of a run's completed steps. On retry/resume the run fast-forwards through it instead of re-executing. |
| Memoization | Returning a step's journaled result instead of running it again. The core of durability (and why tool failures don't re-prompt the model). |
| DLQ | Dead-letter queue: where a job lands after exhausting its retry budget. Inspect and requeue it. |
| Outbox | The atomic event log: emit persists the event, wakes waiters, and creates triggered runs in one transaction. |
| Idempotency key | A caller-supplied key that dedupes run creation — the same key never starts two runs. |
Which primitive do I reach for?#
| I want to… | Use | Guide |
|---|---|---|
| run background work, retried on failure | app.queue() | Queues |
| do something on a clock | app.schedule() | Schedules |
| orchestrate multi-step work that must survive crashes | app.workflow() | Workflows |
| wait for an external signal or human action mid-process | step.waitForEvent() | Events |
| build an LLM agent with tools | app.agent() | Agents |
| enforce a valid lifecycle for an entity | app.machine() | State Machines |
| fan a signal out to many consumers | app.emit() | Events |
| expose HTTP / receive webhooks | app.get() | HTTP |
Rule of thumb
If the work is one shot, it's a queue job. If it has steps, waits, or sleepsthat must survive a restart, it's a workflow. If those steps are chosen by a model, it's an agent.
App lifecycle#
Every app moves through the same phases. The key rule: define before start — registering a queue, workflow, agent, machine, or route after start() throws.
import { zenzip } from "zenzipjs";
const app = zenzip(); // 1. construct — no I/O yet
const q = app.queue("emails"); // 2. DEFINE phase: register everything
q.process(async (job) => { ... }); // queues, workflows, agents, schedules,
app.workflow("order", fn); // machines, routes — all before start()
await app.start(); // 3. START: open store, run migrations,
// boot the Rust engine + dispatchers
await q.push({ to }); // 4. RUNTIME: push / trigger / emit / send
await app.listen({ port: 3000 }); // from anywhere
await app.stop({ timeout: "30s" });// 5. STOP: stop claiming, drain in-flight,
// close the store (idempotent)Guarantees at a glance#
| Thing | Guarantee |
|---|---|
| Queue & run delivery | at-least-once |
| Step result recording | effectively-once (a completed entry is never overwritten) |
| Step side effects | at-least-once (make external effects idempotent) |
| Run creation with idempotencyKey | exactly-once per key |
| State machine transition + its event | atomic (one transaction) |
| Event outbox (emit → waiters + triggers) | atomic (one transaction) |
The full contract — including the step-memoization model, versioning rules, and the idempotency guide — is on Durability & Semantics.
Where state lives#
- Default: one SQLite file (WAL mode) in your
dataDir(.zenzip/zenzip.db). Jobs, schedules, runs, the step journal, events, machines, and agent sessions all live there. Inspect it with any SQLite client; back it up by copying the file. - Multi-node: point at Postgres with one config line (
store: { driver: "postgres", url }). Same API, same semantics — claims useSKIP LOCKED, nodes wake each other viaLISTEN/NOTIFY. See Configuration. - Nothing in memory that matters. A suspended workflow, a scheduled tick, a pending approval — all are rows. Kill the process and they survive.