ctx.context.timestamp = Date.now() | Non-deterministic, breaks test reproducibility | ctx.context.timestamp = ctx.now.toISOString() |
ctx.context.data = new Map() | Not JSON-serializable | Use plain object {} |
ctx.metadata.status = "done" | ctx.metadata is deepFreezed — under strict mode the assignment throws TypeError; in non-strict contexts it is silently ignored | Write to ctx.context.status instead |
Mandatory event with commands but no errorState | Command failure throws CommandFailureError | Add errorState OR mark non-critical command bestEffort: true |
return { ok: true } after network error catch | Hides infrastructure failure | If non-critical, mark bestEffort: true and return ok:false honestly. If critical, re-throw. |
Compensation command catches and returns { ok: true } | Lies in audit trail; loses error detail | (v1.0.0) Mark bestEffort: true and return the real ok:false — chain still continues |
Math.random() > 0.5 ? "a" : "b" in command | Non-deterministic behavior | Use deterministic logic or external service |
| State context and command writing same key without awareness | State context silently wins on merge | Document or avoid key conflicts |
(v1.0.0) WorkflowModule.forRootAsync({ observers: [...] }) at top level | Field was removed; observers no longer wire up | Return observers from useFactory inside the factory config |
(v1.0.0) forRootAsync injects an observer that's only in the consuming module's providers | DI scope error at startup ("Nest can't resolve dependencies of WORKFLOW_MODULE_OPTIONS") | Put the observer in its own module, export it, add the module to forRootAsync.imports |
Business-critical work in a WorkflowObserver.onEnter | Observer is post-commit + at-most-once + error-contained — failures are silently logged | Move into a workflow command that runs inside the transaction |
WorkflowCommandRef.metadata set but handler never reads ctx.commandMetadata | Dead config — looks intentional, does nothing | Either consume in the handler or remove the metadata |
(v1.1.0) Guard mutates ctx.context (e.g., ctx.context.checkedAt = ctx.now.toISOString()) | ctx.context is a deepFreezed clone; mutation throws under strict mode and never persists | Move the write into a command that runs after the guard passes |
| (v1.1.0) Guard calls an external service or DB | Repeats on every timeout sweep; non-idempotent I/O leaks | Compute the predicate from subject + ctx.context only, or precompute the value into context via a command |
(v1.1.0) errorState used to catch a precondition that should be a guard | errorState runs commands first and records a "failure"; guard rejection is short-circuit + business-meaningful | Replace with guard: { name: "..." }; remove the routing errorState if it had no other purpose |
(v1.1.0) Test asserts rejectedBy === guard.name (implementation name) | The runtime reports the declared eventDef.guard.name (ref name); aliasing registries diverge | Assert against the ref name from the workflow definition |
(v1.1.0) WorkflowModule.forRoot({ guards: [...], guardRegistry: ... }) — both passed | Mutually exclusive; the module throws synchronously | Pick one: array form for the convenience path, prebuilt registry for DI/lazy loading |
(v1.1.0) Poller logs only processed/failed, never rejected | Guard-rejected timeouts disappear from observability | Surface result.rejected in logs/metrics/dashboards alongside processed |
(v1.1.0) Workflow definition references guard.name not in any registered guard, with no knownGuardNames validation | Fails at first use as WorkflowError, not at startup | Pass knownGuardNames to the validator OR rely on guards: [...] (which auto-checks) |
(v1.1.0) Custom adapter persists rejected_by on append but maps it as null on read | WorkflowHistoryRecord.rejectedBy should be undefined when absent, not null | Map null → undefined in the read path; same convention as errorMessage |