flow
Compare

Qoqi Flow vs Restate

Waiting for months is the point, not a smell.

Restate replays a journal, so every side effect goes through ctx.run and the clock through ctx.date, by discipline. Running invocations stay on their deployment, and its docs advise against handlers that run for days. A Qoqi flow waits as long as the process takes, moves to new code at a stage, and the compiler checks the rules.

Try it in five minutes See the code

Long waits are what flows do

An approval, a trial, a renewal: weeks, months. Restate advises splitting such handlers. In Qoqi, wait time.after(90.days()) is one line.

Nothing to wrap

Forget a ctx.run in Restate and the call runs again on every replay, unrecorded. In Qoqi, a call outside a step does not compile.

Apache 2.0, one binary

Restate's server is under the BSL 1.1. Qoqi Flow is Apache 2.0, one binary on SQLite, PostgreSQL or MySQL.

Wait for an answer, or seven days

A durable promise, a timeout that throws, and a second handler to resolve it; or one wait match.

Restate · wait.ts15 lines
run: async (ctx: restate.WorkflowContext, expense: Expense) => {  let answer: Answer;  try {    answer = await ctx.promise<Answer>("answer").get().orTimeout({ days: 7 });  } catch (e) {    if (e instanceof restate.TimeoutError) return { status: "expired" };    throw e;  }  // ...},// and a handler of its own to deliver the answeranswer: async (ctx: restate.WorkflowSharedContext, answer: Answer) => {  await ctx.promise<Answer>("answer").resolve(answer);},
Qoqi Flow · wait.qi7 lines
let answer = wait match {    case let a = self.answers.receive(): a;    case time.after(7.days()): null;};if answer == null {    return Outcome.Expired;}

Plain code, because nothing replays

ctx.date, ctx.rand, ctx.run: Restate needs its own version of everything that changes between runs.

Restate · rules.ts12 lines
run: async (ctx: restate.WorkflowContext, user: User) => {  // Replayed from its journal: not Date.now(), ctx.date.now()  if ((await ctx.date.now()) < user.trialEnds) {    await ctx.sleep({ days: 2 });  }  // not Math.random(): ctx.rand  const tip = user.tips[Math.floor(ctx.rand.random() * user.tips.length)];  // Forgot ctx.run? It runs again on every replay, unrecorded  await fetch("https://hooks.example.com/renewals", { method: "POST", body: user.id });  await ctx.run("charge", () => charge(user.id, tip));},
Qoqi Flow · plain.qi11 lines
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);    }}

Side by side

RestateQoqi Flow
How it comes backReplays its journalIts state thawed, at the line it was at
Code between stepsDeterministic: ctx.date, ctx.rand, ctx.runAnything
A call outside a stepNot journaled; runs again on replayRefused at compile time (F0001)
Waiting for weeks or monthsAdvised against: split into delayed callsWhat flows are for
New code under running workStays on its deployment, kept up until it endsInstances move at a stage, checked against flow.lock
A step that must never run twicectx.run runs again if it was in progressSettled<T>: never run again; the flow is told Unknown and decides
Crash testingalwaysReplay, to find non-determinismeveryCrash: the test again, stopped after every write, same ending required
Steps in your own servicesAn SDK in each service's languageAny server that speaks the protocol: a signed HTTP POST. SDKs for Go, PHP, TypeScript
LicenseBSL 1.1 serverApache 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 Restate's ideas map?

Workflow: impl Flow. ctx.run: step fn. Durable promise or awakeable: an Inbox field. ctx.sleep: wait time.after(...). orTimeout and RestatePromise.race: wait match. Deployments: stage.

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