windmill-labs/windmill · warning
the flow is not in a suspended state anymore
Error message
the flow is not in a suspended state anymore
What it means
A flow can only be resumed while its job is in the suspended state (suspend column set). This error is raised when a resume request arrives after the flow already resumed, completed, failed, or was canceled, so the suspension no longer exists.
Source
Thrown at backend/windmill-api/src/jobs.rs:5436
// We need the approval_conditions which are in flow_status column.
// Re-fetch just flow_status (without COALESCE fallback) for the auth check.
let flow_status_only: Option<serde_json::Value> =
sqlx::query_scalar("SELECT flow_status FROM v2_job_status WHERE id = $1")
.bind(&job_id)
.fetch_optional(&mut **tx)
.await?
.flatten();
let flow = FlowInfo {
id: flow.id,
flow_status: flow_status_only,
suspend: flow.suspend,
script_path: flow.script_path,
email: flow.email,
};
Ok((flow, job_id, true))
} else {
Err(anyhow::anyhow!("the flow is not in a suspended state anymore").into())
}
}
pub async fn cancel_suspended_job(
authed: Option<ApiAuthed>,
opt_tokened: OptTokened,
Extension(db): Extension<DB>,
Path((w_id, job_id, resume_id, secret)): Path<(String, Uuid, u32, String)>,
Query(approver): Query<QueryApprover>,
QueryOrBody(value): QueryOrBody<serde_json::Value>,
) -> error::Result<StatusCode> {
resume_suspended_job_internal(
value,
db,
w_id,
job_id,
resume_id,
approver,View on GitHub (pinned to e474e8803c)
Solutions
- Treat this as expected for late/duplicate approvals — no action needed; the flow already moved on
- Fetch the flow run status first and only resume when state is 'suspended'
- Design approval steps with one approver or use a form/approval UI that disables after first click
- If a different outcome is needed after resume, add post-resume logic in the flow rather than re-resuming
Example fix
// before await api.resume(wId, flowId, payload); // throws if already resumed // after const run = await api.getJob(wId, flowId); if (run.state === 'suspended') await api.resume(wId, flowId, payload);
Defensive patterns
Strategy: try-catch
Validate before calling
const run = await client.getJobRun(wId, flowJobId);
if (run.state !== 'suspended') {
console.info('flow already resumed/completed, skipping approval');
return;
} Type guard
function isSuspended(run) {
return run != null && run.state === 'suspended' && run.suspend != null;
} Try / catch
try {
await client.resume(wId, flowJobId, payload);
} catch (e) {
if (/not in a suspended state anymore/.test(e.message)) {
// benign: another approver resumed first; treat as success
return;
}
throw e;
} Prevention
- Design approval steps for idempotent late clicks
- Fetch fresh run state before resuming; never cache suspension state
- Use timeout branches so flows don't wait forever on one approver
When it happens
Trigger: Two approvers clicking the approval link — the second resume arrives after the first succeeded; resuming via a resume token after the flow timed out or errored; scriptmatic resume with an outdated job handle.
Common situations: Approval emails/Slack messages reused by multiple teammates; retries after a slow resume call; resuming long-running approval links after the flow already proceeded on a default branch (e.g. timeout resume).
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- resume request already sent
- You can't use this function in a standalone script or flow s
- This function can only be called as a flow step
- You can't use 'request_interactive_slack_approval' function
- WAC step key "{requested_key}" is already used in this workf
AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03).
Data as JSON: /api/errors/909692167db1137f.
Report an issue: GitHub.