REL-WF-004 — Keep Run now and pause state consistent
| Field | Value |
|---|---|
| Status | Open; source-verified |
| Architecture state | Deployed behavior |
| Severity | High |
| Affected surface | Workflow list/detail Run now, enable/disable toggle, paused filters, schedules, and recovery |
| Components | sparqd/oqullus, sparqd/bff-api, sparqd/workflow-backend, Airflow, sparqd/platform-stacks |
| Associated issue | Not opened |
Where the problem appears
Open a paused workflow detail page and select Run now. Compare the Airflow
is_paused value, workflow-backend workflow_paused value, and visible toggle.
Repeat from the Workflow list and with trigger failure after unpause.
Concrete observed case
The two Run now surfaces use different state paths:
- The list handler calls workflow-backend
POST /workflows/{id}/unpause, which patches Airflow and persistsworkflow_paused=falsebefore triggering. - The detail handler patches BFF's generic Airflow DAG endpoint directly and
updates only the legacy frontend
is_paused=falsefield. - The detail toggle renders
workflow_paused ?? is_paused. When the persisted backend value is stilltrue, it wins over the locally updated Airflow value, so the toggle can continue to show disabled/paused while Airflow is unpaused.
Normal Ready-detail polling returns the persisted workflow record and does not
observe Airflow pause state. Backend list enrichment also deliberately avoids
overwriting workflow_paused. The internal Airflow reconciliation endpoint can
repair drift, but no scheduled caller was identified in platform-stacks main.
Both surfaces trigger in a second request. If trigger fails after a successful unpause, Airflow remains unpaused and a schedule can resume even though no manual run was created.
Expected condition
Represent manual run as one server-owned intent with one authoritative pause result. Prefer Run once while paused without changing the schedule. If the product intentionally unpauses first, every surface must use the managed workflow command, persist the confirmed result, update both frontend aliases from the response, and compensate or report when triggering fails.
Cause and evidence
- Oqullus list
handleTriggerusesendpoint.workflow.unpause(workflow_id); detailhandleDetailTriggerusesPATCH endpoint.dags.toggle(dag_id). - The detail handler writes only
is_paused; the toggle prefersworkflow_paused. - The manual detail toggle already uses workflow-backend and updates both fields, demonstrating the consistent path that Run now bypasses.
- Workflow-backend updates
workflow_pausedonly through its managed pause/unpause command or internal reconciliation. Ready detail reads are database-only. - Workflow-backend tests explicitly prove list runtime enrichment does not overwrite the backend-owned pause projection.
- No
reconcile-airflow-stateinvocation was found in platform-stacks main. - Unpause and trigger remain sequential independent requests with no compensation on trigger failure.
Why it matters
Users can see contradictory enable/disable state across Airflow, Workflow list, and Workflow detail. Paused filters can remain stale indefinitely, and a failed manual run attempt can unexpectedly reactivate scheduled execution, creating data, compute, and cost side effects.
Existing controls and workarounds
Until corrected, do not use Run now from detail on a paused workflow. If scheduled execution should resume, unpause explicitly through the visible toggle and confirm both list and detail agree before running. Otherwise keep the workflow paused and trigger through an approved operator path that does not alter schedule state.
Alternatives considered
- Recommended: a backend Run once while paused operation that does not alter effective schedule pause.
- Minimal consistency correction: make detail Run now use the same managed unpause endpoint as the list and update state only from its response. This fixes projection drift but not unpause-trigger partial failure.
- Require explicit unpause as a separate user decision.
- Compensating re-pause after trigger failure — acceptable fallback but must handle compensation failure visibly.
- Rely on periodic reconciliation — rejected as the primary path because the UI is immediately inconsistent and no deployed schedule was identified.
Component involvement
sparqd/oqullusremoves the direct-Airflow detail unpause and renders one backend-confirmed value across list/detail controls.sparqd/bff-apiexposes the managed command and should not offer the generic Airflow pause mutation to this product journey.sparqd/workflow-backendprovides authoritative pause persistence and the atomic or compensating Run now command.- Airflow remains runtime authority and supplies idempotency/audit evidence.
sparqd/platform-stacksschedules the reconciliation safety sweep if that fallback remains part of the supported design.
Reproduction and verification
Follow WF-S014.
Resolution conditions
Close when list and detail use one managed command, Airflow and
workflow_paused remain equal after success/failure/reload, every failure
ordering preserves the user's intended pause state, at most one run is created,
and any compensation failure is visible.
Validation and blockers
Source root cause is verified. Live capture is still required for the deployed request sequence and for whether an external reconciliation job exists outside the reviewed platform-stacks source.