tinyhumansai/openhuman · error

workflow run {id} is already completed

Error message

workflow run {id} is already completed

What it means

Thrown by workflow_runs::engine::resume_workflow_run when the run row exists but its status is already Completed. Completed is terminal; resume only makes sense for interrupted/failed/incomplete runs, and the check runs before the definition lookup and any state mutation.

Source

Thrown at src/openhuman/agent/orchestration/workflow_runs/engine.rs:296

    log::debug!(target: LOG_TARGET, "[workflow_run_engine] stop.marked_interrupted run={id}");
    Ok(Some(updated))
}

/// Resume an interrupted (or otherwise incomplete) workflow run.
///
/// Reloads the run, clears any stale cancellation flag, flips the row back to
/// `Running`, and spawns a fresh engine loop. Phases already `completed` in
/// `phase_states` are skipped; the loop continues from the first incomplete
/// phase whose dependencies are satisfied. Returns the run row (now `Running`),
/// or an error if the run is unknown / already terminal-complete / its
/// definition no longer exists.
pub async fn resume_workflow_run(config: &Config, id: &str) -> Result<WorkflowRun> {
    log::debug!(target: LOG_TARGET, "[workflow_run_engine] resume.entry run={id}");
    let run = get_workflow_run(&config.workspace_dir, id)?
        .ok_or_else(|| anyhow!("unknown workflow run: {id}"))?;

    if matches!(run.status, WorkflowRunStatus::Completed) {
        return Err(anyhow!("workflow run {id} is already completed"));
    }

    let definition = definition_by_id(&run.definition_id)
        .ok_or_else(|| anyhow!("definition {} no longer exists", run.definition_id))?;

    // Clear any prior cancellation intent and re-register a fresh flag.
    clear_cancel_flag(id);
    register_cancel_flag(id);

    let resumed = upsert_workflow_run(
        &config.workspace_dir,
        WorkflowRunUpsert {
            id: run.id.clone(),
            definition_id: run.definition_id.clone(),
            parent_thread_id: run.parent_thread_id.clone(),
            input: run.input.clone(),
            phase_states: run.phase_states.clone(),
            child_run_ids: run.child_run_ids.clone(),

View on GitHub (pinned to a221052e0d)

Solutions

  1. Fetch the run and check status before resuming; skip Completed
  2. Make resume idempotent in the caller: treat 'already completed' as success
  3. Refresh the run list after any resume so the UI stops offering it

Example fix

// before
resume_workflow_run(&config, &run_id).await?;

// after — treat terminal-complete as a no-op
let run = get_workflow_run(&config.workspace_dir, &run_id)?
    .ok_or_else(|| anyhow!("unknown workflow run: {run_id}"))?;
let run = match run.status {
    WorkflowRunStatus::Completed => run, // already done — nothing to resume
    _ => resume_workflow_run(&config, &run_id).await?,
};
Defensive patterns

Strategy: validation

Validate before calling

let run = get_workflow_run(&config.workspace_dir, run_id)?
    .ok_or_else(|| anyhow!("unknown workflow run {run_id}"))?;
if matches!(run.status, WorkflowRunStatus::Completed) {
    return Ok(run); // terminal — nothing to resume
}

Type guard

fn is_resumable(run: &WorkflowRun) -> bool {
    !matches!(run.status, WorkflowRunStatus::Completed)
}

Try / catch

match resume_workflow_run(&config, run_id).await {
    Ok(r) => Ok(r),
    Err(e) if e.to_string().contains("already completed") => {
        get_workflow_run(&config.workspace_dir, run_id)?.context("run vanished") // return final state
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: Calling resume on a run that finished successfully — double-submit from a UI, a retry of a resume request whose first attempt completed the run, or an orchestrator that resumes everything in a list without filtering by status.

Common situations: Retry logic that treats any prior error (even one raised after completion) as 'resume needed'; stale run list in the UI still showing a Resume action; scripted bulk-resume over mixed-status runs.

Related errors


AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16). Data as JSON: /api/errors/baefcd38545ec2cf. Report an issue: GitHub.