Getting Started
Run your first durable workflow in an existing NestJS app — install the module, write a workflow, register it, and start a run. Zero infrastructure with the event-emitter transport.
This guide gets a workflow running in an existing NestJS app in a few minutes — no broker, no database, using the in-process event-emitter transport and an in-memory store. Swap those for BullMQ and an ORM store when you're ready to go to production.
1. Install
pnpm add @dudousxd/nestjs-durable @dudousxd/nestjs-durable-core @dudousxd/nestjs-durable-transport-event-emitter @nestjs/event-emitter zodnpm i @dudousxd/nestjs-durable @dudousxd/nestjs-durable-core @dudousxd/nestjs-durable-transport-event-emitter @nestjs/event-emitter zodyarn add @dudousxd/nestjs-durable @dudousxd/nestjs-durable-core @dudousxd/nestjs-durable-transport-event-emitter @nestjs/event-emitter zod2. Define a remote step (optional)
A step can run locally (ctx.step) or on a worker (ctx.remote). Declare remote steps with a
typed contract:
import { remoteStep } from '@dudousxd/nestjs-durable-core';
import { z } from 'zod';
export const chargeCard = remoteStep({
name: 'payments.charge-card',
input: z.object({ orderId: z.string(), amountCents: z.number().int() }),
output: z.object({ chargeId: z.string() }),
retries: 3,
});3. Write the workflow
import { Workflow } from '@dudousxd/nestjs-durable';
import type { WorkflowCtx } from '@dudousxd/nestjs-durable-core';
import { chargeCard } from './checkout.steps';
@Workflow({ name: 'checkout', version: '1' })
export class CheckoutWorkflow {
async run(ctx: WorkflowCtx, order: { id: string; total: number }) {
await ctx.step('reserveStock', async () => ({ reserved: true }));
const charge = await ctx.remote(chargeCard, { orderId: order.id, amountCents: order.total });
const approval = await ctx.waitForSignal<{ approved: boolean }>(`approve:${order.id}`);
if (!approval.approved) return { status: 'rejected', chargeId: charge.chargeId };
await ctx.step('ship', async () => ({ shipped: true }));
return { status: 'shipped', chargeId: charge.chargeId };
}
}4. Implement the step handler
Any provider method can handle a remote step in-process — decouple it from the workflow:
import { DurableStep } from '@dudousxd/nestjs-durable';
import { Injectable } from '@nestjs/common';
@Injectable()
export class PaymentsWorker {
@DurableStep('payments.charge-card')
async charge(input: { orderId: string; amountCents: number }) {
return { chargeId: `ch_${input.orderId}` };
}
}5. Register the module
import { DurableModule } from '@dudousxd/nestjs-durable';
import { InMemoryStateStore } from '@dudousxd/nestjs-durable-core';
import { EventEmitterTransport } from '@dudousxd/nestjs-durable-transport-event-emitter';
import { Module } from '@nestjs/common';
import { EventEmitter2, EventEmitterModule } from '@nestjs/event-emitter';
import { CheckoutWorkflow } from './checkout.workflow';
import { PaymentsWorker } from './payments.worker';
@Module({
imports: [
EventEmitterModule.forRoot(),
DurableModule.forRootAsync({
inject: [EventEmitter2],
useFactory: (emitter: EventEmitter2) => ({
store: new InMemoryStateStore(),
transport: new EventEmitterTransport(emitter),
}),
}),
],
providers: [CheckoutWorkflow, PaymentsWorker],
})
export class AppModule {}6. Start a run
Inject WorkflowService and start the workflow. start enqueues the run and returns
immediately with { runId, status: 'pending' } — the HTTP handler never blocks on workflow logic.
A worker executes the body (by default, the same instance, on a microtask), runs the local + remote
steps, then suspends on the approval signal; a webhook resumes it:
constructor(private readonly workflows: WorkflowService) {}
async checkout(order: Order) {
const { runId } = await this.workflows.start('checkout', order); // → { status: 'pending' }
return runId; // respond now; the worker runs the workflow
}
// later, from your approval webhook:
async approve(orderId: string) {
await this.workflows.signal(`approve:${orderId}`, { approved: true }); // → completes & ships
}Need the outcome inline? await this.workflows.waitForRun(runId) resolves once the run settles
— a terminal state (completed/failed/cancelled/dead) or suspended:
const { runId } = await this.workflows.start('checkout', order);
const result = await this.workflows.waitForRun(runId); // resolves when the run settlesThat's it — a durable workflow whose remote step runs in-process, that pauses for human approval and survives restarts. Next:
- Durability & replay — the one rule the model imposes.
- Transports — move steps to another process or a Python worker.
- State stores — persist to Postgres with your ORM.
- Observability — the control plane, OTel and Telescope.
Durable
Durable workflows for NestJS — write a workflow as plain code; every step is checkpointed, so it survives crashes and deploys. Steps can run across apps and languages, with a built-in control plane.
Durability & replay
How checkpoint-and-replay makes a workflow survive crashes — and the one rule it imposes. The workflow body must be deterministic; all side effects live in steps.