Durable, replay-safe workflow orchestration for TypeScript & Node.js β powered by BullMQ and Redis.
Generator-based workflows that combine the elegance of Laravel Workflow with the reliability of Temporal.
Durabull brings generator-based workflow orchestration to TypeScript.
Author workflows as async *execute() coroutines, orchestrate idempotent activities, and run them safely on top of BullMQ and Redis β with full deterministic replay guarantees.
npm install durabull bullmq ioredis
# or
pnpm add durabull bullmq ioredisimport { Durabull } from 'durabull';
const durabull = new Durabull({
redisUrl: process.env.REDIS_URL ?? 'redis://127.0.0.1:6379',
queues: {
workflow: 'durabull-workflow',
activity: 'durabull-activity',
},
serializer: 'json',
pruneAge: '30 days',
// Optional: Queue routing for multi-tenant support
// The context object is passed from WorkflowStub.make(WorkflowClass, { context: { ... } })
queueRouter: (workflowName, context) => {
const tenant = context?.tenantId;
return tenant ? {
workflow: `tenant-${tenant}-workflow`,
activity: `tenant-${tenant}-activity`,
} : undefined;
},
// Optional: Lifecycle hooks for observability
lifecycleHooks: {
workflow: {
onStart: async (id, name, args) => console.log(`Workflow ${name} started`),
onComplete: async (id, name, output) => console.log(`Workflow ${name} completed`),
onFailed: async (id, name, error) => console.error(`Workflow ${name} failed`, error),
},
},
// logger: optional structured logger with info/warn/error/debug methods
});
durabull.setActive();import { Activity } from 'durabull';
export class SayHello extends Activity<[string], string> {
tries = 3;
timeout = 5; // seconds
async execute(name: string): Promise<string> {
return `Hello, ${name}!`;
}
}import { Workflow, ActivityStub } from 'durabull';
import { SayHello } from './SayHello';
export class GreetingWorkflow extends Workflow<[string], string> {
async *execute(name: string) {
const message = yield ActivityStub.make(SayHello, name);
return message;
}
}import { WorkflowStub } from 'durabull';
import { GreetingWorkflow } from './GreetingWorkflow';
const wf = await WorkflowStub.make(GreetingWorkflow);
await wf.start('World');
console.log(await wf.output()); // "Hello, World!"Expose workflows via HTTP using createWebhookRouter.
import { createWebhookRouter, TokenAuthStrategy } from 'durabull';
import { GreetingWorkflow } from './GreetingWorkflow';
const router = createWebhookRouter({
authStrategy: new TokenAuthStrategy('my-secret-token'),
});
router.registerWebhookWorkflow('greeting', GreetingWorkflow);
// Use with Express/Fastify/etc.
app.post('/webhooks/*', async (req, res) => {
const response = await router.handle({
method: req.method,
path: req.path,
headers: req.headers,
body: req.body,
});
res.status(response.statusCode).send(response.body);
});| Capability | Description |
|---|---|
| π§© Generator-based workflows | Use async *execute() and yield for deterministic orchestration. |
| βοΈ Idempotent activities | Encapsulate retries, backoff, and heartbeats for safe IO. |
| β³ Deterministic replay | Rebuild workflow state from event history β crash-safe and restart-safe. |
| π¬ Signals & Queries | Interact dynamically with live workflows via decorators. |
| π§΅ Saga & Compensation | Built-in support for distributed transactions. |
| β± Timers & Await | Durable timers via WorkflowStub.timer() and WorkflowStub.await(). |
| π©Ί Observability | Full event history, heartbeats, and pruning controls. |
| πͺ Webhooks | Trigger workflows and signals via HTTP with pluggable auth. |
Extend Workflow and implement async *execute().
Use ActivityStub.make() or ActivityStub.all() to orchestrate sequential or parallel work.
import { Workflow, ActivityStub } from 'durabull';
import { ChargeCard, EmailReceipt } from './activities';
export class CheckoutWorkflow extends Workflow<[string, number], string> {
async *execute(orderId, amount) {
const chargeId = yield ActivityStub.make(ChargeCard, orderId, amount);
yield ActivityStub.make(EmailReceipt, orderId, chargeId);
return chargeId;
}
}Extend Activity and implement execute().
Configure retry logic and call this.heartbeat() for long-running jobs.
import { Activity, NonRetryableError } from 'durabull';
export class ChargeCard extends Activity<[string, number], string> {
tries = 5;
timeout = 15;
async execute(orderId, amount) {
const res = await paymentGateway.charge(orderId, amount);
if (!res.ok) throw new NonRetryableError(res.error);
return res.chargeId;
}
}Run any example from the /examples directory:
npm run example:greeting # Basic workflow + activity- π
DOCS.mdβ Full Durabull guide for TypeScript users - π‘
examples/β Complete working examples - π§ͺ
tests/β Jest test suite covering all core behaviors
We welcome contributions!
- Fork the repository
- Create a new branch (
feat/my-feature) - Write tests and ensure
npm testpasses - Lint code (
npm run lint) - Open a pull request with a clear description
Durabull is open-source software licensed under the MIT License. Β© 2025 Durabull contributors.