DurableFlow is a server-only (Node.js runtime) durable async "flow" runtime built on BullMQ + Redis.
It works for:
- Plain Node.js apps (Express/Fastify/etc.)
- Next.js (App Router route handlers + server actions) in the Node.js runtime
npm install durableflow bullmq ioredis- Run state:
durableflow:run:<runId> - Step result idempotency:
durableflow:run:<runId>:step:<stepName> - Signals:
durableflow:signal:<runId>:<signalName>
You can override the durableflow prefix via createRuntime({ keyPrefix: "..." }).
Define a flow:
import { defineFlow, step, sleep, waitForSignal } from "durableflow/server";
defineFlow<{ name: string }, void>("hello", async (input) => {
const msg = await step("build_message", async () => `hello ${input.name}`);
await step("log", async () => console.log(msg));
await sleep(1000);
const sig = await waitForSignal<{ ok: boolean }>("approved", { timeoutMs: 10_000 });
await step("after_signal", async () => console.log("approved:", sig.ok));
});Start a worker process:
import { createRuntime } from "durableflow/server";
const runtime = createRuntime({ redis: process.env.REDIS_URL ?? "redis://localhost:6379" });
await runtime.startWorker();Start a run from anywhere on the server:
const runId = await runtime.client.start("hello", { name: "world" });Send a signal (e.g. from a webhook handler):
await runtime.client.signal(runId, "approved", { ok: true });DurableFlow is NOT for the browser or Edge runtime.
- Workers must run as a separate Node.js process/service (do not start BullMQ workers inside Next.js request handlers).
- Ensure route handlers run in the Node.js runtime (not Edge). If needed, set:
export const runtime = "nodejs";export const runtime = "nodejs";
import { createRuntime } from "durableflow/server";
const durableflow = createRuntime({ redis: process.env.REDIS_URL! });
export async function POST(req: Request) {
const input = await req.json();
const runId = await durableflow.client.start("hello", input);
return Response.json({ runId });
}export const runtime = "nodejs";
import { createRuntime } from "durableflow/server";
const durableflow = createRuntime({ redis: process.env.REDIS_URL! });
export async function POST(
req: Request,
{ params }: { params: { runId: string; name: string } }
) {
const payload = await req.json();
await durableflow.client.signal(params.runId, params.name, payload);
return Response.json({ ok: true });
}defineFlow(name, handler)createRuntime(config) => { startWorker(), stopWorker(), client }client.start(flowName, input, options?) => runIdclient.signal(runId, signalName, payload)client.getRun(runId) => RunStateclient.cancel(runId)step(name, fn, opts?)sleep(ms)waitForSignal(name, opts?) => payload
npm install
npm test
npm run buildStart Redis:
docker run --rm -p 6379:6379 --name durableflow-redis redis:7-alpineRun the basic example:
npx tsx examples/node-basic.tsRun the HTTP server example:
npx tsx examples/server.tsThen in another terminal:
curl -s -X POST http://localhost:8787/start -H "content-type: application/json" -d '{"userId":"u1"}'Take the returned runId, then:
curl -s -X POST "http://localhost:8787/signal/<runId>/webhook" -H "content-type: application/json" -d '{"message":"hello"}'- v0.1 supports linear flows only.
- Step results and signal payloads must be JSON-serializable.
- No filesystem access at runtime.