Part of Better Data's open operational infrastructure. Use standalone or with Commerce Chain Optimization.

See Commerce Chain Optimization →
Commerce Chain Optimization

Getting Started

Loop Participation

How Commerce Chain modules declare participation in Loop Engine loops — LoopParticipantManifest, LoopIds, and EventNames.

2 min read · Getting Started

Edit this page

What is loop participation?

Every Commerce Chain module declares which loops it participates in and which domain events it handles. That declaration is a LoopParticipantManifest — a plain object you build with constants from @betterdata/loop-definitions.

The host runtime (Loop Engine, or your worker) reads these manifests at startup and routes events to the right module handlers.

The LoopParticipantManifest type

import type { LoopParticipantManifest, LoopParticipantHandler } from "@betterdata/loop-definitions";

// Conceptually:
interface LoopParticipantManifest {
  moduleId: string;
  description?: string;
  handles: LoopParticipantHandler[];
}

interface LoopParticipantHandler {
  event: import("@betterdata/loop-definitions").EventName | string;
  loops: readonly import("@betterdata/loop-definitions").LoopId[];
  description?: string;
}

Use EventName and LoopId types for stronger typing; allow string only while migrating legacy event names.

Canonical loop IDs (LoopIds)

import { LoopIds } from "@betterdata/loop-definitions";

// Supply chain
LoopIds.SCM_PROCUREMENT; // 'scm.procurement'
LoopIds.SCM_FULFILLMENT; // 'scm.fulfillment'
LoopIds.SCM_QUALITY; // 'scm.quality'
LoopIds.SCM_REPLENISHMENT; // 'scm.replenishment'
LoopIds.SCM_INVENTORY; // 'scm.inventory'
LoopIds.SCM_EXECUTION; // 'scm.execution'

// Demand chain
LoopIds.DCM_DEMAND_SIGNAL; // 'dcm.demand-signal'
LoopIds.DCM_DEMAND; // 'dcm.demand'
LoopIds.DCM_ORDER; // 'dcm.order'
LoopIds.DCM_RETURNS; // 'dcm.returns'
LoopIds.DCM_CHANNELS; // 'dcm.channels'

The authoritative list is in the package source: loop-ids.ts.

Canonical event names (EventNames)

import { EventNames } from "@betterdata/loop-definitions";

EventNames.EXECUTION_GOODS_RECEIVED; // 'scm.execution.goods_received.v1'
EventNames.INVENTORY_STOCK_RESERVED; // 'scm.inventory.stock_reserved.v1'
EventNames.PROCUREMENT_PO_CONFIRMED; // 'scm.procurement.po_confirmed.v1'
EventNames.ORDERS_ORDER_CONFIRMED; // 'dcm.orders.order_confirmed.v1'
// …see event-names.ts for the full set

These strings align with Commerce Chain loop definitions and outbox event types.

Writing a participant manifest

import { LoopIds, EventNames } from "@betterdata/loop-definitions";
import type { LoopParticipantManifest } from "@betterdata/loop-definitions";

export const myModuleParticipant: LoopParticipantManifest = {
  moduleId: "scm.my-module",
  description: "Example custom module",
  handles: [
    {
      event: EventNames.EXECUTION_GOODS_RECEIVED,
      loops: [LoopIds.SCM_PROCUREMENT],
      description: "Advance internal state when goods are received"
    }
  ]
};

Manifest vs inbox split

Some packages separate concerns in source:

| File | Purpose | | --- | --- | | loop-manifest.ts | Pure metadata — safe to import anywhere | | loop-inbox.ts | Batch / cursor processing for workers (process*LoopBatch) | | loop-participation.ts | Re-exports both for backward compatibility |

Today, import participants and batch helpers from the package root (for example @betterdata/scm-procurement). Dedicated package.json subpaths for loop-manifest alone may ship in a future release for smaller bundles.

import { procurementLoopParticipant, processProcurementLoopBatch } from "@betterdata/scm-procurement";

Participant validation

@betterdata/loop-actors includes tests that assert every published participant only references known LoopIds and EventNames values.

pnpm test --filter @betterdata/loop-actors