No sandbox
Workflow SDK takes Date, Math.random, fetch, setTimeout and Node modules away from workflow code. A Qoqi flow keeps the whole language; only reaching the world needs a step.
Durable without a sandbox, a compiler plugin or a host.
Vercel's Workflow SDK compiles "use workflow" functions into a sandbox that is replayed from an event log: a logical clock, a seeded Math.random, no fetch, no timers, no Node modules. Each run is pinned to the deployment that started it. Qoqi Flow is a whole language that never replays, moves to new code at a stage, and runs anywhere.
Workflow SDK takes Date, Math.random, fetch, setTimeout and Node modules away from workflow code. A Qoqi flow keeps the whole language; only reaching the world needs a step.
A Workflow SDK run needs the deployment that started it to stay up for its whole life, or it must be re-run. Qoqi instances move to new code at a stage.
Built for Vercel, with a Postgres World to self-host. Qoqi Flow is one binary, on any machine, from SQLite to PostgreSQL.
A hook raced against a sleep, and a route of your own to resume it; or one wait match.
using hook = createHook<Answer>({ token: `expense:${id}` });const answer = await Promise.race([ hook, sleep("7d").then(() => "expired" as const),]);if (answer === "expired") return { status: "expired" };// and a route of your app, which must check the caller itself:// the token routes the answer, it does not authorize itawait resumeHook(`expense:${id}`, { approved: true, by: "grace" });
let answer = wait match { case let a = self.answers.receive(): a; case time.after(7.days()): null;};if answer == null { return Outcome.Expired;}// $ qoqi send expense:1 answers '{"approved": true, "by": "grace"}'// or POST /api/instances/expense:1/inboxes/answers, with the daemon's token
Workflow SDK swaps the clock and randomness for replayable stand-ins. Qoqi uses the real ones.
export async function renewal(user: User) { "use workflow"; // Runs in a VM and is replayed: Date is a logical clock, // Math.random is seeded, and there is no fetch, no setTimeout, // no Node module. Everything real goes in a "use step" function. if (Date.now() < user.trialEnds) { await sleep("2d"); } const tip = user.tips[Math.floor(Math.random() * user.tips.length)]; await charge(user.id, tip);}async function charge(userId: string, tip: string) { "use step"; // ...}
impl Flow<Void> for Renewal { fn run() { // The clock and chance are plain code: read once, never again if time.now() < self.user.trialEnds { wait time.after(2.days()); } let tip = self.user.tips[random.below(self.user.tips.len())]; charge(self.user.id, tip); }}
| Workflow SDK | Qoqi Flow | |
|---|---|---|
| How it comes back | Replays its event log | Its state thawed, at the line it was at |
| Workflow code | A sandbox: logical clock, seeded random, no fetch, timers or Node modules | The whole language |
| Mistakes | Found when the bundle loads, or when the call runs | Found when it compiles |
| New code under running workflows | Pinned to its deployment, kept up until it ends | Instances move at a stage, checked against flow.lock |
| A step that must never run twice | Steps retried | Settled<T>: never run again; the flow is told Unknown and decides |
| Crash testing | Not provided | everyCrash: the test again, stopped after every write, same ending required |
| Steps in your own services | TypeScript; Python in beta | Any server that speaks the protocol: a signed HTTP POST. SDKs for Go, PHP, TypeScript |
| Where it runs | Vercel; a Postgres World to self-host | Anywhere: one binary |
It is at v0.1.0. The language, the runtime and the daemon work end to end, and they are tested by crashing them on purpose after every write. What it does not have yet is years in production. Start with a flow that matters, not the one that holds the company up.
"use workflow": impl Flow. "use step": step fn. createHook and resumeHook: an Inbox field and qoqi send. sleep: wait time.after(...). Promise.race: wait match. FatalError and RetryableError: Result and flow.retry.
No. Only the flow is Qoqi. A step that needs your code stays in your service: any server that answers Qoqi's protocol, a signed HTTP POST. qoqi gen writes the typed interfaces for Go, PHP and TypeScript; any other language implements the protocol in a page of code.
Anywhere one binary runs. SQLite on one machine; PostgreSQL or MySQL when several daemons share the work. There is no managed cloud yet.
Also compared
curl -fsSL https://qoqi.dev/install.sh | sh