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.
npm install reflow-tsNode 18 or later, and BunSQLite in a file next to your app, or PostgreSQLZod, Valibot, ArkType, or any Standard SchemaMIT licensed
The real Reflow engine, running in your browser.
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.
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.
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 })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.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.
npx reflow-ts dashboard ./app.dbonFailure.prev is the previous step's output.| Reflow | Temporal | Inngest | |
|---|---|---|---|
| What you run | Your app, and a SQLite file or your Postgres | A cluster, a database, and workers | A hosted service and an SDK |
| Type safety | End to end, inferred | Partial | Partial |
| Good for | Apps, CLIs, and agents, from one process to many machines | Very large, multi-language systems | Serverless 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.