Every line runs once
In Inngest, code outside step.run runs on every call back: a query, a log, a tracking event, again and again. A Qoqi flow is never called from the top again.
Every line runs once. Not just the steps.
Inngest calls your function again and again, handing back the results of the steps it saved; the code around them runs each time, and a step is known by its string id. Qoqi Flow thaws the instance at its line: every line runs once, and a deploy never makes a step run twice.
In Inngest, code outside step.run runs on every call back: a query, a log, a tracking event, again and again. A Qoqi flow is never called from the top again.
Rename an Inngest step and runs in flight meet it for the first time, and run it again. A Qoqi instance moves to new code only at a stage; what is behind it is done.
Inngest's test engine asks you to mock sleeps and waits. A Qoqi trial runs the real waits on its own clock, three days in a moment, and everyCrash on top.
Called back after each step, an Inngest function runs its other lines again. A Qoqi flow is thawed, and every reach into the world is a step the compiler checks.
inngest.createFunction( { id: "trial", triggers: { event: "trial/started" } }, async ({ event, step }) => { // Outside a step: runs again each time Inngest calls the function back const user = await db.users.find(event.data.userId); await analytics.track("trial_started", user.id); // tracked again, and again await step.sleep("wait-3-days", "3d"); await step.run("send-tips", () => sendTips(user.email)); },);
impl Flow<Void> for Trial { fn run() { // Every line runs once: the flow is thawed at its line, // not called again from the top let user = findUser(self.userId); track("trial_started", user.id); wait time.after(3.days()); sendTips(user.email); }}// Reaching the world takes a step, and the compiler checks itstep fn track(event: String, user: String) { analytics.track(event, user); }
A step's identity is its id. Change it while runs are in flight, and they run it again.
// Version 1await step.run("charge", () => charge(order));// Version 2, deployed while runs are in flight: a cleaner name.// A step is known by its id, so every run already past// "charge" meets "charge-card" for the first time, and pays again.await step.run("charge-card", () => charge(order));
// Version 2 renames charge to chargeCard. An instance moves to new// code only at a stage: what it did before is done, and is not// done again, whatever its name now.stage shipping;ship(self.order);
| Inngest | Qoqi Flow | |
|---|---|---|
| How it comes back | Your function called again, saved step results handed back | Its state thawed, at the line it was at |
| Code outside steps | Runs again on every call; nothing checks it | Runs once; reaching the world outside a step is refused at compile time |
| Renaming a step | Re-runs it in runs in flight | Never re-runs what an instance did |
| Waiting | step.sleep, waitForEvent; a run's length capped by plan | wait match over inboxes, timers and tasks |
| A step that must never run twice | Retried | Settled<T>: never run again; the flow is told Unknown and decides |
| Testing | Test engine; sleeps and waits mocked | Trials on their own clock; everyCrash |
| Steps in your own services | SDKs for TypeScript, Python, Go | Any server that speaks the protocol: a signed HTTP POST. SDKs for Go, PHP, TypeScript |
| License | SSPL server, Apache 2.0 after three years | Apache 2.0 |
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.
createFunction: impl Flow. step.run: step fn. step.sleep: wait time.after(...). step.waitForEvent: an Inbox field and wait match. step.invoke: flow.start. cancelOn: flow.cancelled().
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