NestflowJS LogoNestflowJS

Custom Adapter

Build your own adapter by extending BaseWorkflowAdapter to run workflows on any computing service.

NestflowJS doesn't tie you to any computing model, you can technically make it run on any computing service or self-hosted as long as its runtime supports NestJS thanks to flexible adapter system.

Build your own adapter by extending BaseWorkflowAdapter — the abstract class that owns the orchestration loop and dispatches each TransitResult to a handler method you implement.

Extending BaseWorkflowAdapter

import { BaseWorkflowAdapter } from 'nestflow-js/adapter';
import { OrchestratorService } from 'nestflow-js/core';
import type { IWorkflowEvent, TransitResult } from 'nestflow-js/core';

The base class has two type parameters:

ParameterMeaning
TContextYour adapter's execution context (e.g. HTTP request, durable context, queue message)
TResultThe value returned when the workflow reaches a final state

Override these five methods:

MethodCalled whenReturn
executeTransitEach loop iteration — run orchestrator.transit() with optional retry/checkpointingTransitResult
onFinalWorkflow reached a terminal stateTResult (ends the loop)
onIdleEntity is idle, waiting for external callbackNext IWorkflowEvent
onContinuedAuto-transition foundNext IWorkflowEvent
onNoTransitionNo clear next step — needs explicit eventNext IWorkflowEvent

Example: HTTP Adapter

An adapter that drives workflows via REST API, returning immediately on idle/no-transition states:

import { BaseWorkflowAdapter } from 'nestflow-js/adapter';
import { OrchestratorService } from 'nestflow-js/core';
import type { IWorkflowEvent, TransitResult } from 'nestflow-js/core';

interface HttpContext {
  requestId: string;
}

interface HttpWorkflowResult {
  status: string;
  state: string | number;
  message?: string;
}

class HttpWorkflowAdapter extends BaseWorkflowAdapter<HttpContext, HttpWorkflowResult> {
  constructor(orchestrator: OrchestratorService) {
    super(orchestrator);
  }

  protected async executeTransit(
    event: IWorkflowEvent,
    _iteration: number,
    _ctx: HttpContext,
  ): Promise<TransitResult> {
    // Simple — no retry or checkpointing needed for synchronous HTTP
    return this.orchestrator.transit(event);
  }

  protected onFinal(
    result: Extract<TransitResult, { status: 'final' }>,
  ): HttpWorkflowResult {
    return { status: 'completed', state: result.state };
  }

  protected async onIdle(
    result: Extract<TransitResult, { status: 'idle' }>,
  ): Promise<IWorkflowEvent> {
    // HTTP adapters typically don't wait — throw or return a response
    throw { status: 'waiting', state: result.state, message: 'Awaiting callback' };
  }

  protected async onContinued(
    result: Extract<TransitResult, { status: 'continued' }>,
  ): Promise<IWorkflowEvent> {
    return result.nextEvent;
  }

  protected async onNoTransition(
    result: Extract<TransitResult, { status: 'no_transition' }>,
  ): Promise<IWorkflowEvent> {
    throw { status: 'waiting', state: result.state, message: 'No auto-transition available' };
  }
}

Wire it up in a NestJS controller:

@Controller('workflow')
export class WorkflowController {
  private adapter: HttpWorkflowAdapter;

  constructor(orchestrator: OrchestratorService) {
    this.adapter = new HttpWorkflowAdapter(orchestrator);
  }

  @Post('events')
  async handleEvent(@Body() body: { event: string; urn: string; payload?: any }) {
    return this.adapter.run({
      event: body.event,
      urn: body.urn,
      payload: body.payload,
      attempt: 0,
    });
  }
}

Example: EventBridge Adapter

Process events from AWS EventBridge with fire-and-forget semantics:

import { BaseWorkflowAdapter } from 'nestflow-js/adapter';
import { OrchestratorService } from 'nestflow-js/core';
import type { IWorkflowEvent, TransitResult } from 'nestflow-js/core';

class EventBridgeAdapter extends BaseWorkflowAdapter<void, void> {
  constructor(orchestrator: OrchestratorService) {
    super(orchestrator);
  }

  protected async executeTransit(event: IWorkflowEvent): Promise<TransitResult> {
    return this.orchestrator.transit(event);
  }

  protected onFinal(): void {
    // Fire-and-forget — nothing to return
  }

  protected async onIdle(
    result: Extract<TransitResult, { status: 'idle' }>,
  ): Promise<IWorkflowEvent> {
    // Publish an event for external systems to pick up
    throw new Error(`Entity paused at ${result.state} — publish callback event externally`);
  }

  protected async onContinued(
    result: Extract<TransitResult, { status: 'continued' }>,
  ): Promise<IWorkflowEvent> {
    return result.nextEvent;
  }

  protected async onNoTransition(
    result: Extract<TransitResult, { status: 'no_transition' }>,
  ): Promise<IWorkflowEvent> {
    throw new Error(`No transition from ${result.state} — publish event externally`);
  }
}

The Raw Pattern

If you prefer not to extend the base class, the underlying pattern is straightforward — call transit() in a loop and switch on the result:

async function runWorkflow(
  orchestrator: OrchestratorService,
  initialEvent: IWorkflowEvent,
): Promise<TransitResult> {
  let currentEvent = initialEvent;

  while (true) {
    const result = await orchestrator.transit(currentEvent);

    switch (result.status) {
      case 'final':
        return result;
      case 'idle':
        return result;
      case 'continued':
        currentEvent = result.nextEvent;
        break;
      case 'no_transition':
        return result;
    }
  }
}

When to Use What

AdapterUse when
DurableLambdaEventHandlerLong-running workflows that span multiple Lambda invocations, workflows with idle states that need callbacks
HTTP adapterSimple request/response workflows, synchronous processing
EventBridge adapterEvent-driven architectures, fan-out patterns
Custom loopCron-based processing, batch jobs, custom infrastructure