Deploying an already-deploying workflow returns 400 instead of 409 with the current state
Symptom
Starting a workflow that is already starting or running returns HTTP 400
with {"error": "Workflow can't be deployed at this status"}, rather than
succeeding idempotently or returning 409 Conflict with the current state.
Reported against 26.9.0.
Root cause — confirmed in the code
studio/ryax/studio/domain/workflow/entities/workflow.py:645-651:
def deploy(self) -> None:
if self.validity_status is not WorkflowValidityStatus.VALID:
raise WorkflowNotValidException
if self.deployment_status not in [
WorkflowDeploymentStatus.NONE,
WorkflowDeploymentStatus.ERROR,
]:
raise WorkflowNotDeployableand studio/ryax/studio/infrastructure/api/controllers/workflow_controller.py:227:
except (WorkflowNotValidException, WorkflowNotDeployable) as err:
raise ApiError(status_code=400, body={"error": err.message})Two different conditions are collapsed onto one status code:
| Condition | Meaning | Right code |
|---|---|---|
WorkflowNotValidException |
the request can never succeed as sent | 400 |
WorkflowNotDeployable |
the request is fine, the resource is in the wrong state | 409 |
A client cannot tell "your workflow is broken" from "it is already running", and the body carries no machine-readable state to recover from.
Why it matters for automation
A controller reconciling desired state has to issue deploy and interpret the
result. Today it must string-match the error message to decide whether to treat
the call as a success. The demo's GPU controller re-reads the workflow state
instead (vllm-broker 0.2.1), which is a race: the state can change between
the read and the deploy.
Suggested fix
Return 409 for WorkflowNotDeployable, and include the current
deployment_status in the body so a client can act without a second request.
Consider making deploy idempotent when the workflow is already DEPLOYED —
that is the behaviour a reconciler actually wants, and it removes the race
rather than reporting it.
Note this is an API behaviour change: the Breaks API label may apply, and the
front-end's handling of the 400 should be checked before changing the code.
Found while migrating the CAPE demo to Ryax 26.9.0: ryax-tech/public/cape-tools/cape-agentic-infra#2 (comment 3905714558)