{"record":{"id":"36fdd45b71dd0832","repo":"windmill-labs/windmill","slug":"resume-request-already-sent","errorCode":null,"errorMessage":"resume request already sent","messagePattern":"resume request already sent","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"backend/windmill-api/src/jobs.rs","lineNumber":5124,"sourceCode":"    let (flow_info, is_flow_level, is_wac) = get_flow_info_for_resume(job_id, &db).await?;\n\n    // HMAC secret = full capability. Skip approval_conditions checks: possession of the full\n    // resume URL is the authorization (it is only disclosed to intended approvers, e.g. when a\n    // step returns it). Identity-based rules, including self_approval_disabled, are enforced by\n    // the resume_suspended endpoint instead.\n\n    let exists = sqlx::query_scalar!(\n        r#\"\n            SELECT EXISTS (SELECT 1 FROM resume_job WHERE id = $1)\n            \"#,\n        Uuid::from_u128(job_id.as_u128() ^ resume_id as u128),\n    )\n    .fetch_one(&db)\n    .await?\n    .unwrap_or(false);\n\n    if exists {\n        return Err(anyhow::anyhow!(\"resume request already sent\").into());\n    }\n\n    let approver_value = if authed.as_ref().is_none()\n        || (approver\n            .approver\n            .clone()\n            .is_some_and(|x| x != \"\".to_string()))\n    {\n        approver.approver\n    } else {\n        authed.as_ref().map(|x| x.username.clone())\n    };\n    let mut tx: Transaction<'_, Postgres> = db.begin().await?;\n\n    // Inside the transaction that inserts the row and moves the suspend counter:\n    // validating earlier would let the workflow resolve this step and suspend on the\n    // next one in between, so a stale request would wake that later step instead.\n    if is_wac {","sourceCodeStart":5106,"sourceCodeEnd":5142,"githubUrl":"https://github.com/windmill-labs/windmill/blob/e474e8803ce2ff5c2df09a58dab51d45f5c922ca/backend/windmill-api/src/jobs.rs#L5106-L5142","documentation":"The flow resume (approval) endpoint refuses to send a second resume request for the same suspended step. A row recording that a resume/approval request was already dispatched exists, so a duplicate request is rejected to prevent double-resumption or duplicate approval emails.","triggerScenarios":"Calling the resume/approval request endpoint twice for the same suspended flow step before the first resume is consumed; retrying an approval request that already succeeded; two users clicking 'send approval email' concurrently.","commonSituations":"Duplicate clicks on an approval button; automated retries after a slow email send; replaying an API request without idempotency awareness.","solutions":["Wait for the original resume/approval request to be handled; do not resend","Skip the resend if you receive this error — the first request is already in flight","Cancel the suspended job (cancel_suspended_job) and restart the flow if a fresh request is needed","Check the approval step's state in the flow runs UI before sending another request"],"exampleFix":"// before\nawait api.resumeRequest(wId, flowId, resumeId, payload);\nawait api.resumeRequest(wId, flowId, resumeId, payload); // throws\n// after\nconst alreadySent = await checkResumeSent(flowId, resumeId);\nif (!alreadySent) await api.resumeRequest(wId, flowId, resumeId, payload);","handlingStrategy":"validation","validationCode":"// client-side: send at most one resume request per suspended step\nconst sent = new Set();\nfunction sendResume(flowId, resumeId, payload) {\n  const key = flowId + ':' + resumeId;\n  if (sent.has(key)) return Promise.resolve('skipped');\n  sent.add(key);\n  return client.resume(flowId, resumeId, payload);\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never retry resume-request calls blindly; the first send already registered","Disable the approval button after the first click","Check the step's state before sending a request"],"tags":["flow","approval","duplicate-request","state-conflict"],"backgroundTag":"duplicate-submission","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"}