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

  1. Treat this as expected for late/duplicate approvals — no action needed; the flow already moved on
  2. Fetch the flow run status first and only resume when state is 'suspended'
  3. Design approval steps with one approver or use a form/approval UI that disables after first click
  4. 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

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


AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03). Data as JSON: /api/errors/909692167db1137f. Report an issue: GitHub.