Docs Index
workflow central operator.md

Proof Card: WorkflowCentral Operator Service

Status: beta Last verified: 2026-05-17 · main baseline b8a438ae (PR-OP-3 merge); PR-OP-3-followup (PR #805) on top adds the deferred T13.2-T13.6 integration tests + the ?status=active multi-status filter + the WorkflowInstanceMissingError invariant-breach WARN. Stamp re-bumps when the followup PR merges.

Claim

WorkflowCentralOperatorService promotes the WorkflowCentral task-completion workflow from in-memory Map.set mutation to a durable, audited, atomic state machine. Operators complete workflow_central_tasks rows via POST /api/workflow-central/tasks/:id/complete. completeTask is a single-stage atomic transition (pending → completed) — no two-stage lease, since advanceWorkflow (the cascade planner) has zero external writes. A six-step sequence enforces ordering: (1) pre-tx read for not_found / already_dispositioned / invalid_action disambiguation, (2) pure engine.planCascade returning a CascadePlan, (3) atomic db.transaction(tx) that UPDATEs parent status='completed' AND INSERTs all cascade child rows (rollback on any child failure → cascade_failed), (4) post-commit best-effort engine.applyVolatileState updating in-memory instance.currentStepId + stepHistory (try/catch surfaces volatile_state_applied: false without rolling back the DB), (5) safeAudit outside the transaction, (6) discriminated CompleteTaskResult return. Sibling operator actions cancelInstance, delegateTask, startInstance live on WorkflowCentralService (D9 split) and use the same safeAudit + DB-canonical-first ordering. Status is beta because production credentials + a Squire-side workflow load test are still pending.

Source

Tests

Live vs Fixture

Known Gaps

Reference-Based Payload (Phase 1, ADR-019)

Status: beta (alongside the existing completeTask claim — promotion to production requires the Phase 1 §Rollout dual-write window to close + backfill verified in production for ≥2 weeks).

Claim

WorkflowCentralOperatorService.getTaskForOperator(tenantId, taskId) returns a discriminated TaskRenderModel over three payload modes — resolved (refs into client ERP fetched live via ConnectorManager.getConnector + BaseConnector.read(entityType, id) with a 30s WorkflowPayloadCache), ephemeral (gated expiresAt-bounded inline data), legacy (pre-backfill fallback exposing task.data). Per-ref connector failures NEVER bubble to whole-render HTTP status — they're carried inside the 200-response resolution[i].error per the partial-success contract (load-bearing for cross-system compose). Whole-render conditions map at route-level: 404 NotFound, 410 EphemeralPayloadExpired, 403 EphemeralPayloadNotAllowed, 400 invalid :id shape. Audit emits via redactWorkflowPayloadForAuditreferences and evaluationHints pass through; ephemeral.data is ALWAYS dropped before serialization.

Source

Tests

Total: ~109 Phase-1 tests + 209 pre-existing workflowCentral suites all passing.

Live vs Fixture

Known Gaps (Phase 1)

60-second verification

# Validators + audit redaction
npm test -- --testPathPatterns="payload/WorkflowPayload.test.ts$"

# Resolver + cache
npm test -- --testPathPatterns="payload/(WorkflowPayloadResolver|WorkflowPayloadCache).test.ts$"

# Migration 043 + repo round-trip
npm test -- --testPathPatterns="(043-add-workflow|WorkflowCentralRepository)"

# Operator render + audit redaction
npm test -- --testPathPatterns="WorkflowCentralOperatorService"

# Confirm audit details NEVER carry resolved field values or ephemeral data
grep -nE "redactWorkflowPayloadForAudit" src/services/workflowCentral/WorkflowCentralOperatorService.ts
# Expect every audit-emit site that touches task.payload to invoke this helper

The fourth grep should show the helper called in every audit-emit branch of getTaskForOperator — the load-bearing invariant that no payload values reach the audit row.

Plan: docs/plans/2026-05-17-governance-without-hosting-data-plan.md ADR: docs/adr/ADR-019-workflow-governance-without-hosted-data.md

Updates in PR-OP-3 (instance durability)

Known gaps (carried + new)

(Pre-existing PR-OP-2 gaps unchanged; new in PR-OP-3:)

Verification (60-second AI-reviewer recipe)

# Unit tests for all three new services + migration. Codex R2 SHOULD-FIX:
# uses `npm test` so the project's jest config + TypeScript transform are
# loaded (raw `npx jest --runTestsByPath` fails on `import type` because
# no transform config is picked up). Jest 30 uses `--testPathPatterns`
# (plural) at the CLI.
npm test -- --testPathPatterns="workflowCentral|041-create-workflow-central"

# Route-level integration (in-memory SQLite + full DI graph)
npm run test:integration -- --testPathPatterns="workflowCentral-"

# Confirm SELECT-then-UPDATE (NOT UPDATE...RETURNING) on delegate path (R4 F-01 lock)
grep -nE "\.returning\(\[.*assignee" src/services/workflowCentral/WorkflowCentralRepository.ts \
  && echo "REGRESSION — RETURNING clause present, post-update value bug reintroduced" \
  || echo "OK — no UPDATE...RETURNING on assignee_id"

# Confirm 6-step ordering + audit OUTSIDE tx
grep -nE "db\.transaction|safeAudit|applyVolatileState" \
  src/services/workflowCentral/WorkflowCentralOperatorService.ts | head -15

# Confirm DLP key absence in audit details (no comment / data / completion_comment keys)
grep -nE "'comment'|'data'|completion_comment" src/services/workflowCentral/WorkflowCentralOperatorService.ts \
  | grep -v '// ' || echo "OK — no DLP-sensitive keys in audit-details builder"

The first grep proves delegatePendingTask does NOT use UPDATE ... RETURNING assignee_id — Codex experimentally verified that pattern returns the POST-update value on Kysely 0.28.17 + better-sqlite3. The implementation uses SELECT-then-UPDATE inside the same tx, capturing previousAssigneeId from the pre-update SELECT row.

The second grep should show db.transaction(...) opening BEFORE applyVolatileState calls AND BEFORE every safeAudit call site — the audit ALWAYS fires after the tx settles (whether commit or rollback) per spec R1 F-01.

The third grep should produce zero matches outside of comments — the audit-details builder only includes structural fields (task_id, instance_id, workflow_id, etc.) per spec R1 F-14 / R2 F-08. Sensitive content lives on the task row's completion_comment / data columns only.