flowable/flowable-engine · warning · FlowableException

Cannot delete ${job} when the job is being executed. Try aga

Error message

Cannot delete ${job} when the job is being executed. Try again later.

What it means

After finding the timer job, DeleteTimerJobCmd checks job.getLockOwner(); a non-null lock owner means the async executor acquisition thread already locked the job for execution, so deleting it would race with a running job. The command throws a plain FlowableException telling the caller to retry later.

Source

Thrown at modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/cmd/DeleteTimerJobCmd.java:83

    protected TimerJobEntity getJobToDelete(CommandContext commandContext) {
        if (timerJobId == null) {
            throw new FlowableIllegalArgumentException("jobId is null");
        }
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Deleting job {}", timerJobId);
        }

        TimerJobEntity job = jobServiceConfiguration.getTimerJobEntityManager().findById(timerJobId);
        if (job == null) {
            throw new FlowableObjectNotFoundException("No timer job found with id '" + timerJobId + "'", Job.class);
        }

        // We need to check if the job was locked, ie acquired by the job acquisition thread
        // This happens if the job was already acquired, but not yet executed.
        // In that case, we can't allow to delete the job.
        if (job.getLockOwner() != null) {
            throw new FlowableException("Cannot delete " + job + " when the job is being executed. Try again later.");
        }
        return job;
    }

}

View on GitHub (pinned to d6d39ce1c6)

Solutions

  1. Retry the delete after a short delay until the lock owner is cleared (job executed or lock released).
  2. Cancel the owning execution/process instance instead of deleting the timer directly, so the engine cleans up safely.
  3. Catch FlowableException and implement exponential backoff for this transient condition.
  4. Temporarily suspend the job executor during bulk timer cleanup operations.

Example fix

// before
managementService.deleteTimerJob(timerJobId); // FlowableException if locked
// after
TimerJobInfo job = managementService.createTimerJobQuery().timerJobId(timerJobId).singleResult();
if (job != null) {
    managementService.deleteTimerJob(timerJobId); // retry with backoff on FlowableException
}
Defensive patterns

Strategy: retry

Validate before calling

TimerJobInfo j = managementService.createTimerJobQuery().timerJobId(id).singleResult();
boolean locked = j != null; // lock owner not exposed; rely on retry for FlowableException

Try / catch

try { managementService.deleteTimerJob(id); } catch (FlowableException e) { scheduler.schedule(this::deleteWithBackoff, 2, SECONDS); }

Prevention

When it happens

Trigger: managementService.deleteTimerJob(id) called while the async executor has acquired but not yet executed the timer job (lock owner column populated).

Common situations: Admin/UI code deleting timers at the same moment the job executor fires them; high-load systems where the acquisition thread grabs timers between a user query and the delete; canceling a process instance manually via job deletion instead of letting engine cascade handle it.

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 flowable/flowable-engine@d6d39ce1c6 (2026-09-11). Data as JSON: /api/errors/57c8c77370f78af4. Report an issue: GitHub.