ZenZip logozenzip

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 writeUnderneath, 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:

TermMeaning
JobOne unit of queued work. Has a payload, attempt count, and (while claimed) a lease.
LeaseA time-bound claim on a job/run. If the worker dies, the lease expires and the work is redelivered — this is how crashes recover.
RunOne execution of a workflow or agent. Has a status (running, sleeping, waitingEvent, waitingChild, completed, failed, cancelled).
StepA durable unit inside a run (step.run / sleep / waitForEvent / invoke). Its result is journaled.
JournalThe recorded results of a run's completed steps. On retry/resume the run fast-forwards through it instead of re-executing.
MemoizationReturning a step's journaled result instead of running it again. The core of durability (and why tool failures don't re-prompt the model).
DLQDead-letter queue: where a job lands after exhausting its retry budget. Inspect and requeue it.
OutboxThe atomic event log: emit persists the event, wakes waiters, and creates triggered runs in one transaction.
Idempotency keyA caller-supplied key that dedupes run creation — the same key never starts two runs.

Which primitive do I reach for?#

I want to…UseGuide
run background work, retried on failureapp.queue()Queues
do something on a clockapp.schedule()Schedules
orchestrate multi-step work that must survive crashesapp.workflow()Workflows
wait for an external signal or human action mid-processstep.waitForEvent()Events
build an LLM agent with toolsapp.agent()Agents
enforce a valid lifecycle for an entityapp.machine()State Machines
fan a signal out to many consumersapp.emit()Events
expose HTTP / receive webhooksapp.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.

lifecycle.ts
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#

ThingGuarantee
Queue & run deliveryat-least-once
Step result recordingeffectively-once (a completed entry is never overwritten)
Step side effectsat-least-once (make external effects idempotent)
Run creation with idempotencyKeyexactly-once per key
State machine transition + its eventatomic (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 use SKIP LOCKED, nodes wake each other via LISTEN/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.