Run once: a finished deployment can stay RUNNING and block every later deploy of the workflow

Summary

A "Run once" deployment can end up RUNNING with a stopped_time after its run has finished. Every later deploy of the same workflow is then silently ignored: the runner logs Trying to create a workflow that is already running with workflow_definition_id '…'. Ignoring, starts no trigger, and Studio stays on "Deploying" forever. No pod appears in ryaxns-execs.

Seen on staging (26.10.0-rc3, still present in rc5)

Deployment Workflow Created State stopped_time Executions
WorkflowDeployment-1793005256-nz0m3o60 My new workflow 2026-10-26 09:00:56 RUNNING 09:01:01 6, all COMPLETED
WorkflowDeployment-1793005108-tgi0iq23 Print env 2026-10-26 08:58:28 RUNNING 08:58:31

Both came from "Run once". A deploy of "My new workflow" on rc5 at 15:38 was ignored because of the first row.

Cause

stopped_time is only set together with a stopped state (set_stopped_gracefully / set_stopped), so these deployments were stopped and then put back to RUNNING. That is a lost update between two handlers:

  1. is_started (ryax/runner/application/handlers/workflow_deployment_handlers.py, on WorkflowDeployedSuccessfully) loads the deployment. It is not stopped yet, so the #1337 (closed) is_stopped() guard passes.
  2. With "Run once" the trigger runs once and ends within seconds, and stop (graceful) commits STOPPED_GRACEFULLY plus stopped_time.
  3. is_started resumes after awaiting the addon hook and the publish, calls set_running(), and commits. Only state changed in its session, so the result is state = RUNNING with the stopped_time from step 2.

Nothing locks the row, so the guard in step 1 cannot see a stop that commits during step 3.

Expected

A deployment that has been stopped is never set back to RUNNING, whatever order the two events arrive in, and the next deploy of the workflow starts.

Fix

  • Code: ryax-core MR (linked below). is_started takes a row lock on the deployment, so it either sees the stop or the stop waits for it.
  • Existing installs: deployments left in this state need state = 'STOPPED_GRACEFULLY' where state = 'RUNNING' and stopped_time is not null. Consider doing this in a migration.