Workflow Architecture

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 (2026-07-06): source retained, service dormant. The Workflow engine, storage backends, WebService project, and workflow PostgreSQL schema remain in source for possible future use. The canonical compose stack does not deploy StellaOps.Workflow.WebService, Router has no /api/workflow route, and ReleaseOrchestrator/Scheduler callers were removed. The gateway route ^/api/v1/workflows(.*) is configured to translate to ReleaseOrchestrator (Router .../appsettings.json:130), not to this service — though note that RO does not currently register any /api/v1/workflows handler (a grep of src/ReleaseOrchestrator/** finds the route only in the gateway config and a Doctor probe, not in RO’s endpoint map), so that path is a configured-but-unserved gateway route today, not a live surface either place. Reactivation requires an explicit product decision plus canonical scopes, client grants, authenticated callers, compose wiring, and a gateway route; none of those are implied by the retained source.

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 baseline embedded in StellaOps.Workflow.DataStore.PostgreSQL (<EmbeddedResource Include="Migrations\**\*.sql" />) and applied on startup via AddStartupMigrations (module name Workflow.DataStore.PostgreSQL).

The canonical Stella PostgreSQL schema is workflow. PostgresWorkflowBackendOptions.DefaultSchemaName is the single default used by both runtime qualification and migration registration, matching the workflow.-qualified baseline. SRD_WFKLW is a separate legacy Oracle backend schema and must not be used as the PostgreSQL default. Because the shipped baseline uses qualified DDL, arbitrary PostgreSQL schema overrides are not a supported migration contract.

The configurable table names (defaults) live in PostgresWorkflowBackendOptions: RuntimeStatesTableName = wf_runtime_states, HostedJobLocksTableName = wf_host_locks, InstancesTableName = wf_instances, TasksTableName = wf_tasks, TaskEventsTableName = wf_task_events, SignalQueueTableName = wf_signal_queue, DeadLetterTableName = wf_signal_dead_letters, WakeOutboxTableName = wf_signal_wake_outbox. LISTEN/NOTIFY channels default to workflow_signal and workflow_signal_wake_outbox.

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.
WorkflowBackend:Postgres:ConnectionStringNameWorkflowPostgresConnection-string key; falls back to Default for auto-migration.
WorkflowBackend:Postgres:SchemaNameworkflowCanonical PostgreSQL data and migration-ledger schema. Do not override while the baseline remains workflow.-qualified.
WorkflowBackend:Postgres:*PoolSize / Pooling / ConnectionIdleLifetimeSeconds1 / 100 / true / 300Npgsql pooling.
WorkflowBackend:Postgres:ClaimBatchSize / ClaimTimeoutSeconds / BlockingWaitSeconds32 / 60 / 30Native-driver signal claim tuning.
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__WorkflowOracleRequired 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).
RouterStella 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 ConnectionStrings:WorkflowPostgres or ConnectionStrings:Default must be configured so startup migrations can converge the workflow schema before request handling. 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 docs/code-of-conduct/TESTING_PRACTICES.md.