Skip to main content

REL-WF-004 — Keep Run now and pause state consistent

FieldValue
StatusOpen; source-verified
Architecture stateDeployed behavior
SeverityHigh
Affected surfaceWorkflow list/detail Run now, enable/disable toggle, paused filters, schedules, and recovery
Componentssparqd/oqullus, sparqd/bff-api, sparqd/workflow-backend, Airflow, sparqd/platform-stacks
Associated issueNot 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 persists workflow_paused=false before triggering.
  • The detail handler patches BFF's generic Airflow DAG endpoint directly and updates only the legacy frontend is_paused=false field.
  • The detail toggle renders workflow_paused ?? is_paused. When the persisted backend value is still true, 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 handleTrigger uses endpoint.workflow.unpause(workflow_id); detail handleDetailTrigger uses PATCH endpoint.dags.toggle(dag_id).
  • The detail handler writes only is_paused; the toggle prefers workflow_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_paused only 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-state invocation 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​

  1. Recommended: a backend Run once while paused operation that does not alter effective schedule pause.
  2. 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.
  3. Require explicit unpause as a separate user decision.
  4. Compensating re-pause after trigger failure — acceptable fallback but must handle compensation failure visibly.
  5. Rely on periodic reconciliation — rejected as the primary path because the UI is immediately inconsistent and no deployed schedule was identified.

Component involvement​

  • sparqd/oqullus removes the direct-Airflow detail unpause and renders one backend-confirmed value across list/detail controls.
  • sparqd/bff-api exposes the managed command and should not offer the generic Airflow pause mutation to this product journey.
  • sparqd/workflow-backend provides authoritative pause persistence and the atomic or compensating Run now command.
  • Airflow remains runtime authority and supplies idempotency/audit evidence.
  • sparqd/platform-stacks schedules 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.