The workflow lived in a human-readable DSL, written in Groovy. That DSL is a document-object model of the domain: a DOM of loan application and Document, not a browser DOM. Each published version was a row in PostgreSQL, and a loan application pinned the workflow and rules versions it started on. TypeScript rules ran against those shapes and wrote facts back onto the loan application. A rule that found a missing fact did not stall; it created the next task, automatic or user task.
User tasks were a first-class workflow state, not an overlay on an automatic pipeline. Automatic failures came from integrations, from the loan application or a Document, from missing data, from errored automation, and from stuck workflows. When Q's confidence on a Document was low, when a rule demanded a decision, or when those automatic paths and their fallbacks had all failed, that work became a user task in the Next.js console. For classification, extraction, and redaction I built a canvas toolkit that kept every drawn region stable in document space through zoom, rotation, and page changes. Automation redactions were soft-removed so the audit trail survived.
Redis Streams carried the work and PostgreSQL held the workflow state, so catch-up and audit did not fight over one store. A mock server let React developers ship user task screens, and Playwright, Cypress, and Cucumber run in parallel, without waiting on a full workflow graph. Log lines carried correlation ids and event types, never names, account numbers, or document contents.