Workflow Architecture

Verified against commit 942a18727c (2026-09-26, DOC-2 of SPRINT_20260926_001). Re-verify by reading the evidence paths listed in the Evidence section.

Service wiring (image, database and role, compose profile, bundle presence) is generated in the service inventory; this dossier does not restate it.

Scope. Architecture and operational contract for the Workflow Engine: its HTTP API, authorization model, PostgreSQL persistence, signal pump, and graph renderer. This dossier is for service implementers and for operators running or troubleshooting workflow instances (including the signal dead-letter runbooks).

Implementation status (owner ruling 2026-08-19; synchronized 2026-08-21): kept for future use, service dormant. Owner-input register item 17 retains the Workflow engine, storage backends and WebService project as supported source. That ruling is retention, not activation. The canonical compose stack does not deploy StellaOps.Workflow.WebService, Router has neither a /api/workflow route nor the former ^/api/v1/workflows(.*) alias, and ReleaseOrchestrator/Scheduler callers are absent. The alias targeted ReleaseOrchestrator even though that service exposed no matching handler; RAR-5 removed it from the gateway source in 41b205cc81 on 2026-08-11. It is no longer a configured surface. Reactivation requires a new explicitly scoped implementation/deployment sprint: P4 ownership of a Workflow database + role on the shared PostgreSQL installation, in-service startup migrations, canonical scopes and client grants, authenticated callers, compose wiring, and a gateway route. None of those are implied by the keep ruling or retained source. The W3-15 disposition and its deferred residue/drop decision are recorded in SPRINT_20260722_020.

The retained Workflow Engine is a general-purpose runtime for definition deployment, instance lifecycle, signal-driven task transitions, dead-letter handling, and authorization-gated task progression. StellaOps.Workflow.WebService/Program.cs still contains Router integration and HTTP host composition so the source remains buildable, but that composition is not current deployment topology.

Retained HTTP API Surface

If the dormant WebService is hosted, its business endpoints are mapped under the /api/workflow route group (WorkflowEndpoints.MapWorkflowEndpoints). Every endpoint requires a named authorization policy (see “Authorization & Scopes”); state-changing endpoints additionally emit an audit record via .Audited(AuditModules.Workflow, …).

Method & PathHandlerPolicyAudited
POST /api/workflow/startStartWorkflowOperateStartInstance
GET /api/workflow/instances/{id}GetInstanceView—
GET /api/workflow/instancesGetInstancesView—
GET /api/workflow/tasks/{id}GetTaskView—
GET /api/workflow/tasksGetTasksView—
POST /api/workflow/tasks/{id}/completeCompleteTaskOperateCompleteTask
POST /api/workflow/tasks/{id}/assignAssignTaskOperateAssignTask
POST /api/workflow/tasks/{id}/releaseReleaseTaskOperateReleaseTask
POST /api/workflow/signals/raiseRaiseSignalOperateRaiseSignal
GET /api/workflow/definitionsGetDefinitions (in-memory catalog)View—
GET /api/workflow/definitions/{id}GetDefinitionById (store-backed)View—
GET /api/workflow/definitions/{id}/render-graphGetRenderGraphView—
POST /api/workflow/definitions/importImportDefinitionAdminImportDefinition
POST /api/workflow/definitions/exportExportDefinitionView—
POST /api/workflow/definitions/import-multiImportDefinitionMultiAdminImportDefinitionMulti
POST /api/workflow/definitions/export-multiExportDefinitionMultiView—
GET /api/workflow/supported-formatsGetSupportedFormatsView—
GET /api/workflow/definitions/{name}/versionsGetDefinitionVersionsView—
POST /api/workflow/definitions/{name}/activateActivateDefinitionAdminActivateDefinition
GET /api/workflow/diagrams/{name}GetDiagramView—
GET /api/workflow/canonical/schemaGetCanonicalSchemaView—
POST /api/workflow/canonical/validateValidateCanonicalView—
GET /api/workflow/functionsGetFunctionCatalogView—
GET /api/workflow/metadataGetServiceMetadataView—
POST /api/workflow/retention/runRunRetentionAdminRunRetention
GET /api/workflow/signals/dead-lettersGetDeadLettersView—
POST /api/workflow/signals/dead-letters/replayReplayDeadLettersAdminReplayDeadLetters
GET /api/workflow/signals/pump/statsGetSignalPumpStatsView—

The retained host also maps operational/diagnostic endpoints outside the group: anonymous GET /healthz (liveness, Predicate => false), anonymous GET /readyz (readiness), the build-info endpoint (BuildInfoEndpointExtensions.MapBuildInfoEndpoint, fallback module workflow), and the OpenAPI document (MapOpenApi().AllowAnonymous()).

The POST /start request body is StartWorkflowRequest (WorkflowName required; WorkflowVersion, BusinessReference, and a Payload dictionary optional) and returns StartWorkflowResponse (WorkflowInstanceId, resolved WorkflowName/WorkflowVersion, NextTasks, WorkflowState). GET /instances and GET /tasks accept their filters as query parameters (workflowName, workflowVersion, workflowInstanceId, businessReferenceKey, status, assignee, includeDetails, actorId). RunRetention is the only endpoint that reads the wall clock (DateTime.UtcNow, explicitly annotated Determinism:Allowed) when no ReferenceUtc override is supplied.

Authorization & Scopes

The dormant WebService source uses a three-tier scope hierarchy defined locally in StellaOps.Workflow.WebService.Authorization.WorkflowPolicies. These scope strings are deliberately not registered in the canonical StellaOps.Auth.Abstractions.StellaOpsScopes catalog and no current client is granted them. They describe the retained endpoint policy shape, not a grantable production contract:

Scope constantValueGrants
ViewScopeworkflow:viewread-only access to definitions, instances, tasks, signals
OperateScopeworkflow:operatestart workflows; complete/assign/release tasks; raise signals (implies view)
AdminScopeworkflow:admindeploy/activate definitions; run retention; replay dead letters (implies operate + view)

Named policies registered via AddWorkflowPolicies:

HasAnyScope reads both space-delimited scope claims (OAuth standard form, StellaOpsClaimTypes.Scope) and individual scp item claims (StellaOpsClaimTypes.ScopeItem), case-insensitively.

On any future reactivation, the gateway must remain the primary trust anchor: it validates bearer tokens, strips them, and forwards a signed identity envelope. The retained service verifies that envelope via UseIdentityEnvelopeAuthentication() and the WorkflowEnvelopeAuthenticationHandler authentication scheme, then runs the standard UseAuthentication()/UseAuthorization() pipeline. The defense-in-depth RequireAuthenticatedUser gates reject any direct in-cluster access that bypasses the gateway with 401. Do not restore callers using anonymous direct HTTP or network bypasses; provision a scoped service identity and signed gateway envelope.

PostgreSQL Persistence Schema

The Postgres backend’s tables are created by the forward-only migration chain embedded in StellaOps.Workflow.Persistence (<EmbeddedResource Include="Migrations/*.sql" />) and applied on startup via AddStartupMigrations (schema workflow, module name Workflow.DataStore.PostgreSQL), registered by AddWorkflowPostgresDataStore.

Fixed names (Q-45). The migration chain is the only DDL authority. The schema and the table names are not configurable: WorkflowTables holds the fixed, schema-qualified names (workflow.wf_instances, and so on), every store statement names its table literally, and PostgresWorkflowSqlBuilder no longer emits DDL. The former SchemaName, ConnectionStringName and eight *TableName options of PostgresWorkflowBackendOptions are removed. SRD_WFKLW is a separate legacy Oracle backend schema and is not a PostgreSQL name. LISTEN/NOTIFY channels default to workflow_signal (NotifyChannel) and workflow_signal_wake_outbox (WakeOutboxNotifyChannel).

Re-verify with the PostgreSQL backend test project, including WorkflowMigrationStartupTests (nonempty discovery, complete-ledger restart parity) and WorkflowRetentionClassificationTests. This source change does not activate the dormant service.

Retention

002_workflow_retention_classes.sql classifies the tables from their writers:

ClassTablesLifecycle
operational-statewf_instances, wf_tasks, wf_task_events, wf_runtime_statespurge_after_utc is set only when an instance or task completes (WorkflowRetention:CompletedPurgeAfterDays, default 180). The WorkflowRetentionService, run by the retention hosted job (WorkflowRetentionHostedJob:Interval, default 24 h), deletes expired instances, their tasks and task events, and the runtime states of the purged instances. Open instances are never deleted.
source-of-truthwf_definitions, wf_signal_queue, wf_signal_dead_letters, wf_signal_wake_outbox, wf_host_locksDeleted only by the domain operation the row describes (delivery, dead-letter replay, wake completion, lock release); never by age. Definitions are never deleted.

AddWorkflowPostgresDataStore also registers the Doctor check doctor.workflow.retention.overdue-purge (WorkflowRetentionBacklogCheck, category Capacity, default severity Warning). It reports rows whose purge_after_utc passed more than two retention intervals ago, and completed rows retained inside the window against a budget (Doctor:Workflow:MaximumRetainedTerminalRows, default 1,000,000). The retained WebService does not expose a doctor-check surface, so the check runs only in a host that adopts one.

Configuration Keys

Section / keyDefaultPurpose
WorkflowBackend:ProviderRetained WebService config: PostgresBackend selection: Oracle, Postgres, or Mongo (WorkflowBackendNames). The shared abstraction retains a legacy Oracle fallback, but the Stella WebService configuration explicitly selects Postgres.
STELLAOPS_POSTGRES_WORKFLOW_CONNECTIONnone (required)The Workflow service’s own database connection. AddWorkflowPostgresDataStore throws when it is absent or blank; there is no ConnectionStrings:* or generic fallback and no DSN ships with the product.
WorkflowBackend:Postgres:*PoolSize / Pooling / ConnectionIdleLifetimeSeconds1 / 100 / true / 300Npgsql pooling.
WorkflowBackend:Postgres:ClaimBatchSize / ClaimTimeoutSeconds / BlockingWaitSeconds32 / 60 / 30Native-driver signal claim tuning.
WorkflowBackend:Postgres:NotifyChannel / WakeOutboxNotifyChannelworkflow_signal / workflow_signal_wake_outboxLISTEN/NOTIFY channel names.
WorkflowRetention:CompletedPurgeAfterDays / OpenStaleAfterDays180 / 30Purge window for completed instances; stale threshold for open ones.
WorkflowRetentionHostedJob:Enabled / Interval / InitialDelay / LockNametrue / 24:00:00 / 00:05:00 / workflow.retentionRetention hosted job.
Doctor:Workflow:MaximumRetainedTerminalRows1000000Budget for doctor.workflow.retention.overdue-purge.
WorkflowSignalDriver:ProviderNativeSignal driver: Native (Postgres LISTEN/NOTIFY) or Redis (WorkflowSignalDriverNames).
WorkflowAq:MaxDeliveryAttempts10Worker retry budget (see “Signal pump retry budget”).
WorkflowAq:DeadLetterQueueNameWF_DLQ_QUser dead-letter queue for the Oracle AQ path.
WorkflowAq:SignalQueueName / ScheduleQueueNameWF_SIGNAL_Q / WF_SCHEDULE_QOracle AQ queue names.
WorkflowAq:ConsumerName / BlockingDequeueSecondsWORKFLOW_SERVICE / 30Oracle AQ consumer + dequeue wait.
ConnectionStrings__WorkflowOracle—Required environment variable for EF design-time Oracle factories; no source fallback is provided. Runtime DI reads ConnectionStrings:WorkflowOracle, then Default.
WorkflowRendering / WorkflowRenderLayoutProviderNamesElkSharpRenderer provider (ElkSharp, ElkJs, Msagl).
Router—Stella Router microservice integration section.

WorkflowRuntimeProviderNames exposes the two runtime-provider identifiers used in persisted state: Stella.InProcess and Stella.Engine.

Runtime Persistence Contract

If reactivated, Workflow WebService runs against PostgreSQL-backed runtime stores outside Development and Testing. WorkflowBackend:Provider must be Postgres, and STELLAOPS_POSTGRES_WORKFLOW_CONNECTION must name the Workflow service’s own database so startup migrations can converge the workflow schema before request handling (the host fails closed without it; WorkflowConnectionStringFailClosedTests). No current canonical host starts this WebService.

Production startup rejects authoritative local fallbacks for runtime state, hosted-job locks, definition storage, signal stores, signal drivers, wake outbox, and signal dead letters. The engine-level null and in-memory store implementations remain available only for explicit local/test harnesses.

Workflow transport defaults are not mocks. When no HTTP, Rabbit, legacy Rabbit, GraphQL, or microservice transport plugin is registered, the engine binds explicit unconfigured transports that return failed execution responses naming the missing plugin. They do not enqueue, call, or acknowledge work, so production workflows fail closed until the required transport plugin is registered by the host.

Definition imports fail closed when no backend-specific definition store is registered. The null definition store never acknowledges writes or activations because that would make workflow deployment look successful without persisting deployable definitions.

PostgreSQL Runtime Bindings

When the Postgres backend is active, the WebService uses PostgreSQL implementations for runtime state, hosted-job locks, projections, definition storage, signal store/claims/scheduler, wake outbox sender/receiver, and signal dead-letter storage. The native signal driver is enabled by WorkflowSignalDriver:Provider = Native.

Renderer Layout Engines

Workflow definitions are visualised through IWorkflowRenderLayoutEngine providers. Three implementations are wired in this repo:

Lifecycle decision (sprint SPRINT_20260429_005_Workflow_renderer_jvm_lifecycle.md)

The sprint was created on a hypothesis that StellaOps.ElkSharp wraps a JVM child process (the upstream Eclipse project ELK is implemented in Java) and that the renderer test host crashed because of JVM forking, deadlocked stdin/stdout buffers, or unhandled JVM aborts. The hypothesis is incorrect: StellaOps.ElkSharp is a pure C# port of the algorithm, not a wrapper. There is no JVM, no Java process, no IPC, and no subprocess in the ElkSharp call path.

The renderer test host did not crash. It was hanging because a single Best-effort layout of the canonical 24-node DocumentProcessingWorkflow takes ~60 s in Release on a typical dev box (the iterative-routing path explores many candidate strategies). The renderer test suite contained several fixtures that performed dozens of such layouts back-to-back, far exceeding VSTest’s default --blame-hang-timeout (3 min). VSTest then collected a hangdump of the alive-but-busy worker process and reported “test host crashed.”

Decisions (no JVM-worker pattern was needed):

There is no Java runtime requirement on the production host for ElkSharp rendering. The ElkJs provider needs Node.js available on the host that runs the renderer. ElkSharp is the default provider used by the engine and tests.

ElkSharp edge-correctness contracts

Sprints SPRINT_20260430_081_Workflow_renderer_elksharp_correctness.md and SPRINT_20260501_053_Workflow_elksharp_aspirational_helpers_followup.md define the current edge-routing contracts for the in-process ElkSharp provider.

Operational notes

Signal pump retry budget (WorkflowSignalDeliveryAttemptTracker)

The signal pump worker (StellaOps.Workflow.Engine.HostedServices.WorkflowSignalPumpWorker) owns the authoritative retry budget for workflow signal envelopes. Sprint SPRINT_20260430_080_Workflow_oracle_aq_retry_budget reworked the budget semantics after the original implementation relied on the transport-reported IWorkflowSignalLease.DeliveryCount, which is not authoritative for Oracle AQ under the worker’s Visibility = OnCommit rollback flow.

Semantics:

Cross-process safety net:

Configuration binding:

Operator runbook for inspecting stuck signals:

Oracle/Redis integration test fixture

The Workflow Oracle data store and Oracle AQ signaling are exercised end-to-end by StellaOps.Workflow.DataStore.Oracle.Tests via WorkflowOracleRedisFixture (sprint SPRINT_20260429_004_Workflow_oracle_test_fixture.md, D-009). The fixture spawns one Oracle Free 23 slim-faststart container and one Redis 7-alpine container per test run (NUnit [SetUpFixture] lifetime), and exposes a BuildServiceProvider(...) helper that mirrors the production host’s DI wiring with the Oracle backend selected.

Key wiring decisions encoded in the fixture, recorded here so they persist beyond the originating sprint:

CI lane: .gitea/workflows/workflow-oracle-tests.yml runs the suite on PRs that touch the Oracle/Redis libraries or tests, with cached docker save archives keyed on the pinned image tags so cold-cache pulls happen once per tag bump rather than per PR.

Test-pattern reference and per-test cleanup discipline: see “Oracle / Redis test fixtures” in .memory/standards/TESTING_PRACTICES.md.

Evidence

ClaimSource
Dormant: no compose service, bundle entry or Router routedevops/compose/docker-compose.stella-services.yml, devops/release/bundle/docker-compose.yml (no workflow service); src/Workflow/AGENTS.md
HTTP host, health, build-info, OpenAPI, Router integrationsrc/Workflow/StellaOps.Workflow.WebService/Program.cs
/api/workflow endpoint group, policies, audit actionssrc/Workflow/StellaOps.Workflow.WebService/Endpoints/WorkflowEndpoints.cs
Local scopes and policies (not in the canonical catalog)src/Workflow/StellaOps.Workflow.WebService/Authorization/WorkflowPolicies.cs, src/Workflow/StellaOps.Workflow.WebService/Authorization/WorkflowEnvelopeAuthenticationHandler.cs
Built-in definitions deployed on startupsrc/Workflow/StellaOps.Workflow.WebService/Bootstrap/WorkflowDefinitionBootstrap.cs, src/Workflow/StellaOps.Workflow.WebService/Definitions/*.json
Migration chain (001 baseline, 002 retention classes)src/Workflow/__Libraries/StellaOps.Workflow.Persistence/Migrations/
Fixed schema and table namessrc/Workflow/__Libraries/StellaOps.Workflow.DataStore.PostgreSQL/WorkflowTables.cs
Connection key, fail-closed resolution, startup migrations, store bindingssrc/Workflow/__Libraries/StellaOps.Workflow.DataStore.PostgreSQL/PostgresWorkflowDataStoreExtensions.cs, src/Workflow/__Libraries/StellaOps.Workflow.DataStore.PostgreSQL/PostgresWorkflowBackendOptions.cs
Retention options and hosted jobsrc/Workflow/__Libraries/StellaOps.Workflow.Engine/Services/WorkflowRetentionOptions.cs, src/Workflow/__Libraries/StellaOps.Workflow.Engine/Services/WorkflowRetentionHostedJobOptions.cs
Doctor check doctor.workflow.retention.overdue-purgesrc/Workflow/__Libraries/StellaOps.Workflow.DataStore.PostgreSQL/WorkflowRetentionBacklogCheck.cs
Backend, signal driver, rendering sectionssrc/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowBackendOptions.cs, WorkflowSignalDriverOptions.cs, WorkflowRenderingAbstractions.cs
Oracle backend connection keyssrc/Workflow/__Libraries/StellaOps.Workflow.DataStore.Oracle/OracleWorkflowDataStoreExtensions.cs, src/Workflow/__Libraries/StellaOps.Workflow.DataStore.Oracle/WorkflowOracleConnectionString.cs, src/Workflow/__Libraries/StellaOps.Workflow.Signaling.OracleAq/OracleManagedAqTransport.cs
Signal pump retry budget, WorkflowAq optionssrc/Workflow/__Libraries/StellaOps.Workflow.Engine/Engine/Hosting/WorkflowAqOptions.cs, src/Workflow/__Libraries/StellaOps.Workflow.Signaling.OracleAq/WorkflowAqOptions.cs
Renderer providers and CI lanessrc/Workflow/__Libraries/StellaOps.Workflow.Renderer.ElkSharp/, src/Workflow/__Libraries/StellaOps.Workflow.Renderer.ElkJs/, src/Workflow/__Libraries/StellaOps.Workflow.Renderer.Msagl/; .gitea/workflows/workflow-renderer.yml, .gitea/workflows/workflow-oracle-tests.yml
Tests backing the persistence contractsrc/Workflow/__Tests/StellaOps.Workflow.DataStore.PostgreSQL.Tests/WorkflowMigrationStartupTests.cs, src/Workflow/__Tests/StellaOps.Workflow.DataStore.PostgreSQL.Tests/WorkflowRetentionClassificationTests.cs, src/Workflow/__Tests/StellaOps.Workflow.WebService.Tests/WorkflowConnectionStringFailClosedTests.cs