Aviary

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 zod
npm i @dudousxd/nestjs-durable @dudousxd/nestjs-durable-core @dudousxd/nestjs-durable-transport-event-emitter @nestjs/event-emitter zod
yarn add @dudousxd/nestjs-durable @dudousxd/nestjs-durable-core @dudousxd/nestjs-durable-transport-event-emitter @nestjs/event-emitter zod

2. Define your steps

Steps are plain provider methods marked with @Step — there's one always-dispatched ctx.step, and the engine routes each call to a handler by name, giving it a checkpoint and exactly-once execution. Add an optional typed contract (zod input/output) and a retry policy where you want them:

payments.worker.ts
import { Step } from '@dudousxd/nestjs-durable';
import { Injectable } from '@nestjs/common';
import { z } from 'zod';

@Injectable()
export class PaymentsWorker {
  @Step()
  async reserveStock(order: { id: string; total: number }) {
    return { reserved: true };
  }

  @Step({
    name: 'payments.charge-card',
    input: z.object({ orderId: z.string(), amountCents: z.number().int() }),
    output: z.object({ chargeId: z.string() }),
    retries: 3,
  })
  async chargeCard(input: { orderId: string; amountCents: number }) {
    return { chargeId: `ch_${input.orderId}` };
  }

  @Step()
  async ship(order: { id: string; total: number }) {
    return { shipped: true };
  }
}

3. Write the workflow

checkout.workflow.ts
import { Workflow } from '@dudousxd/nestjs-durable';
import type { WorkflowCtx } from '@dudousxd/nestjs-durable-core';
import { PaymentsWorker } from './payments.worker';

@Workflow({ name: 'checkout', version: '1' })
export class CheckoutWorkflow {
  constructor(private readonly payments: PaymentsWorker) {}

  async run(ctx: WorkflowCtx, order: { id: string; total: number }) {
    await ctx.step(this.payments.reserveStock, order);
    const charge = await ctx.step(this.payments.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(this.payments.ship, order);
    return { status: 'shipped', chargeId: charge.chargeId };
  }
}

4. Register the module

app.module.ts
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 {}

5. 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), dispatches its 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 settles

WorkflowService or WorkflowEngine?

WorkflowService is a thin, NestJS-idiomatic facade — its start delegates to the core WorkflowEngine, defaulting the runId and adding conveniences (signal, signalWithStart, waitForRun, publishEvent). Prefer it in application code. WorkflowEngine is also injectable and equivalent for starting runs — reach for it directly when you want the lower-level engine surface (driving runPending, recovery, an explicit runId you supply). Both accept a workflow class (fully typed) or a name string:

// via the service (recommended)
await this.workflows.start(CheckoutWorkflow, order);
// via the engine, with an explicit idempotency key
await this.engine.start(CheckoutWorkflow, order, uuidv7());

That's it — a durable workflow whose steps run in-process, that pauses for human approval and survives restarts. Next:

On this page