flow
v0.1.0 · early, and open source

Durable workflows,
as plain programs.

Write the process from top to bottom: wait three days, ask a manager, charge a card, retry, give up. Qoqi Flow keeps it alive through crashes, restarts and new versions of its own code, and never runs a finished step twice.

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

Get started Read the docs

trial.qi running
impl Flow<Void> for Trial {    fn run() {        welcome(self.user);        wait time.after(3.days());        if !hasCreatedAProject(self.user) {            sendTips(self.user);        }        wait time.after(10.days());   // a deploy meanwhile? it goes on        if !isSubscribed(self.user) {            offerDiscount(self.user, 20);            wait time.after(1.days());            endTrial(self.user);        }    }}
trial:ada day 0
  1. stepwelcome("ada")sent
  2. wait3 daysfrozen in the store
  3. stephasCreatedAProject("ada")false
  4. stepsendTips("ada")sent
  5. wait10 daysfrozen in the store
  6. deploytrial.qi, version 2it stays at its line
  7. stepisSubscribed("ada")true
  8. endsubscribeddone

Two weeks, written as two weeks. A new version was deployed while it waited: this trial went on from its line, on the code it started with; the trials started after run the new one.

Why

A process that waits is usually taken apart by hand.

For a payment, a person, a webhook, a date. Nobody can read it from top to bottom any more, and the bugs live in the gaps. Qoqi Flow keeps it a program.

Taken apart

  • the line it is ata status column
  • its variablesa JSON field
  • its sleepa delayed job
  • its retriesa counter, and a cron
  • its crash recoverya sweeper

With Qoqi Flow

  • the line it is atthe line it is at
  • its variablesits variables
  • its sleepwait time.after(3.days())
  • its retriesa for loop
  • its crash recoverynothing to write

What it does

A language and a runtime, made for flows.

Testing

Three days pass in a moment. Every crash, tried.

A trial runs a flow on a clock of its own. trial.replace(step, fn) stands a function in for a step, so a test reaches no real service. Then qoqi test.

Testing flows
approvals_test.qi
// The same scenario again, with the process stopped after// each of its writes: it has to end the same every timefn testAnAnswerSurvivesCrashes() {    flow.test(Expense { id: "3", author: "lin" }).everyCrash(fn(trial: flow.Trial<Decision>) {        trial.go();        trial.advance(2.days());       // two days, in a moment        trial.send("answers", Answer { approved: true, why: "" });    });}

Your application

Steps your service carries out, typed from the flow.

A step declared by a queue has no body in the flow: the daemon hands it to your application, signed with the queue's secret, and keeps what it answers. The declaration says how long it may take, and which step undoes it.

Steps in your application
shop.qi
step fn charge(customer: String, cents: Int) -> Result<String, Decline>    within 30.seconds() by payments undo refund;
shell
$ qoqi gen go  shop.qi --package steps > steps/steps_gen.go$ qoqi gen php shop.qi --namespace 'App\Flow' --out src/Flow$ qoqi gen ts  shop.qi > steps.ts

One binary

Served, started, answered. In a minute.

The compiler, the daemon, an HTTP API, a web console, a test runner, a formatter and a language server, all in qoqi. The store is SQLite, PostgreSQL or MySQL; daemons on several machines share one.

shell
$ qoqi serve &$ qoqi deploy approvals.qi$ qoqi start Expense expense:1 '{"id": "1", "author": "lin"}'$ qoqi send expense:1 answers '{"approved": true, "why": ""}'$ qoqi result expense:1Approved("grace")$ qoqi ui

How it compares

Not a library added to a language.

Temporal, Restate, Inngest, DBOS, Workflow SDK and othersQoqi Flow
What it isA library or service added to an existing languageA language and runtime made for flows
Recovery after a crashA history played again; the code must be deterministicThe state frozen and thawed
Reaching the world outside a stepFound when it runs, or not at allRefused when it compiles
A new version of the codeA run is pinned to the version that started it, or patched by handPinned until the instance reaches a stage, where it moves, checked against flow.lock
Testing crashesLeft to youeveryCrash

What they have and Qoqi Flow does not yet: years in production, managed hosting, and SDKs for writing the flows themselves in many languages.

Qoqi Flow against each of them

Write the process. Let it wait.

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

Quickstart Star on GitHub