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 WorkflowNotDeployable

and 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)