flow
Compare

Qoqi Flow vs Inngest

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.

Try it in five minutes See the code

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.

Rename without paying twice

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.

Tests on a real clock of their own

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.

The code around the steps

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 · outside.ts11 lines
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));  },);
Qoqi Flow · outside.qi14 lines
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 refactor that charges twice

A step's identity is its id. Change it while runs are in flight, and they run it again.

Inngest · rename.ts7 lines
// 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));
Qoqi Flow · rename.qi6 lines
// 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);

Side by side

InngestQoqi Flow
How it comes backYour function called again, saved step results handed backIts state thawed, at the line it was at
Code outside stepsRuns again on every call; nothing checks itRuns once; reaching the world outside a step is refused at compile time
Renaming a stepRe-runs it in runs in flightNever re-runs what an instance did
Waitingstep.sleep, waitForEvent; a run's length capped by planwait match over inboxes, timers and tasks
A step that must never run twiceRetriedSettled<T>: never run again; the flow is told Unknown and decides
TestingTest engine; sleeps and waits mockedTrials on their own clock; everyCrash
Steps in your own servicesSDKs for TypeScript, Python, GoAny server that speaks the protocol: a signed HTTP POST. SDKs for Go, PHP, TypeScript
LicenseSSPL server, Apache 2.0 after three yearsApache 2.0

Questions

Is Qoqi Flow ready for production?

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.

How do Inngest's ideas map?

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().

Do I have to rewrite my services in Qoqi?

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.

Where does it run?

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

Write the process. Let it wait.

$ curl -fsSL https://qoqi.dev/install.sh | sh

Quickstart GitHub