{"record":{"id":"909692167db1137f","repo":"windmill-labs/windmill","slug":"the-flow-is-not-in-a-suspended-state-anymore","errorCode":null,"errorMessage":"the flow is not in a suspended state anymore","messagePattern":"the flow is not in a suspended state anymore","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"backend/windmill-api/src/jobs.rs","lineNumber":5436,"sourceCode":"        // We need the approval_conditions which are in flow_status column.\n        // Re-fetch just flow_status (without COALESCE fallback) for the auth check.\n        let flow_status_only: Option<serde_json::Value> =\n            sqlx::query_scalar(\"SELECT flow_status FROM v2_job_status WHERE id = $1\")\n                .bind(&job_id)\n                .fetch_optional(&mut **tx)\n                .await?\n                .flatten();\n\n        let flow = FlowInfo {\n            id: flow.id,\n            flow_status: flow_status_only,\n            suspend: flow.suspend,\n            script_path: flow.script_path,\n            email: flow.email,\n        };\n        Ok((flow, job_id, true))\n    } else {\n        Err(anyhow::anyhow!(\"the flow is not in a suspended state anymore\").into())\n    }\n}\n\npub async fn cancel_suspended_job(\n    authed: Option<ApiAuthed>,\n    opt_tokened: OptTokened,\n    Extension(db): Extension<DB>,\n    Path((w_id, job_id, resume_id, secret)): Path<(String, Uuid, u32, String)>,\n    Query(approver): Query<QueryApprover>,\n    QueryOrBody(value): QueryOrBody<serde_json::Value>,\n) -> error::Result<StatusCode> {\n    resume_suspended_job_internal(\n        value,\n        db,\n        w_id,\n        job_id,\n        resume_id,\n        approver,","sourceCodeStart":5418,"sourceCodeEnd":5454,"githubUrl":"https://github.com/windmill-labs/windmill/blob/e474e8803ce2ff5c2df09a58dab51d45f5c922ca/backend/windmill-api/src/jobs.rs#L5418-L5454","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","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"],"exampleFix":"// before\nawait api.resume(wId, flowId, payload); // throws if already resumed\n// after\nconst run = await api.getJob(wId, flowId);\nif (run.state === 'suspended') await api.resume(wId, flowId, payload);","handlingStrategy":"try-catch","validationCode":"const run = await client.getJobRun(wId, flowJobId);\nif (run.state !== 'suspended') {\n  console.info('flow already resumed/completed, skipping approval');\n  return;\n}","typeGuard":"function isSuspended(run) {\n  return run != null && run.state === 'suspended' && run.suspend != null;\n}","tryCatchPattern":"try {\n  await client.resume(wId, flowJobId, payload);\n} catch (e) {\n  if (/not in a suspended state anymore/.test(e.message)) {\n    // benign: another approver resumed first; treat as success\n    return;\n  }\n  throw e;\n}","preventionTips":["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"],"tags":["flow","resume","state-conflict","approval"],"backgroundTag":"invalid-state-transition","analyzedSha":"e474e8803ce2ff5c2df09a58dab51d45f5c922ca","analyzedAt":"2026-09-03T12:38:19.024Z","contentChangedAt":"2026-09-03T12:38:19.024Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}