Skip to content

Steps that survive a crash.

Reflow runs multi-step workflows in your TypeScript app and saves each step's result the moment it finishes. Kill the process half-way and the run carries on from the next step — nothing to deploy beside your app.

sh
npm install reflow-ts

Node 18 or later, and BunSQLite in a file next to your app, or PostgreSQLZod, Valibot, ArkType, or any Standard SchemaMIT licensed

Crash it

The real Reflow engine, running in your browser.

One order, run two ways. Start it, then crash the server while it ships.

Charge cardShip orderSend receiptCard charged
Plain coderemembers nothing0×
With Reflowsaves each step0×

The same run, in the dashboard

Reflow's real dashboard, reading the Reflow run above. Open the order workflow and pick a run to see each step, when it ran, and what it returned.

What you write

A workflow is a sequence of steps. Each returns what the next needs, and the engine saves it before moving on — that is the whole trick.

typescript
import { createWorkflow, createEngine } from 'reflow-ts'
import { SQLiteStorage } from 'reflow-ts/sqlite-node'
import { z } from 'zod'

const order = createWorkflow({
  name: 'order',
  input: z.object({ orderId: z.string(), amount: z.number() }),
  label: (input) => input.orderId,
})
  .step('charge', async ({ input, idempotencyKey }) => {
    const charge = await stripe.paymentIntents.create({ amount: input.amount, currency: 'usd' }, { idempotencyKey })
    return { chargeId: charge.id }
  })
  .step('ship', {
    retry: { maxAttempts: 5, backoff: 'exponential' },
    handler: async ({ prev }) => ({ tracking: await warehouse.ship(prev.chargeId) }),
  })
  .sleep('wait-a-day', '1d')
  .step('ask-for-review', async ({ input, prev }) => {
    await email.send(input.orderId, `How was it? ${prev.tracking}`)
  })

const engine = createEngine({ storage: new SQLiteStorage('./app.db'), workflows: [order] })
await engine.start()
await engine.enqueue('order', { orderId: 'ORD-1001', amount: 5000 })
  1. charge is saved before ship starts, so a crash after it never charges the card again. A crash during it runs it again, with the same idempotencyKey, so Stripe returns the first charge instead of making a second.
  2. ship retries with growing waits. The attempt count is saved too, so a restart mid-wait does not start the count over.
  3. wait-a-day holds no worker for the day. The run sleeps in the database and wakes on time, whichever process is running then.
  4. label names each run in the dashboard.

On your own runs

The dashboard above is the one that ships in the package. Point it at a database file, or mount it in your app behind your own auth — see the dashboard guide.

sh
npx reflow-ts dashboard ./app.db

What else it does

Sleep for days
A sleeping run holds no worker and survives restarts; it wakes on time.
Wait for an event
Pause until an approval, a webhook, or a reply arrives, with a timeout.
Retry with backoff
Per step, linear or exponential, with per-attempt timeouts.
Run steps in parallel
Branches run together; the first real failure cancels the rest.
Schedule runs
On an interval or a cron expression, fired once across all your processes.
Compensate on failure
Undo what a failed run already did, with onFailure.
Typed end to end
Inputs come from your schema; each step's prev is the previous step's output.
From one file to many machines
Workers share a SQLite file, or a Postgres database across machines; each run is claimed by exactly one.
Limit one workflow
At most so many runs of a workflow at once, across every process — for rate-limited APIs.

When to reach for it

ReflowTemporalInngest
What you runYour app, and a SQLite file or your PostgresA cluster, a database, and workersA hosted service and an SDK
Type safetyEnd to end, inferredPartialPartial
Good forApps, CLIs, and agents, from one process to many machinesVery large, multi-language systemsServerless functions

Reflow is for apps that need multi-step work to be reliable without running workflow infrastructure. Start on SQLite and move to Postgres when workers span machines, without changing a workflow. It is not built for workflows written in several languages, for sub-second dispatch at very high volume, or for teams already on Temporal.

Released under the MIT License.