flowable/flowable-engine · error · FlowableForbiddenException

${workerId} does not hold a lock on the requested job

Error message

${workerId} does not hold a lock on the requested job

What it means

FlowableForbiddenException thrown in completeJob when the supplied workerId differs from the job's current lock owner (job.getLockOwner()). The engine only lets the worker that acquired (and still holds) the lock complete the job. Mapped to HTTP 403.

Source

Thrown at modules/flowable-external-job-rest/src/main/java/org/flowable/external/job/rest/service/api/acquire/ExternalWorkerAcquireJobResource.java:110

    @ApiOperation(value = "Complete an External Worker Jobs", code = 204, tags = { "Acquire and Execute" })
    @ApiResponses({
            @ApiResponse(code = 204, message = "Indicates the job was successfully completed."),
            @ApiResponse(code = 400, message = "Indicates the request was invalid."),
            @ApiResponse(code = 403, message = "Indicates the user does not have the rights complete the job."),
            @ApiResponse(code = 404, message = "Indicates the job does not exist."),
    })
    @PostMapping(value = "/acquire/jobs/{jobId}/complete", produces = "application/json")
    public ResponseEntity<?> completeJob(@PathVariable String jobId, @RequestBody ExternalWorkerJobCompleteRequest request) {
        String workerId = request.getWorkerId();
        if (StringUtils.isEmpty(workerId)) {
            throw new FlowableIllegalArgumentException("workerId is required");
        }

        ExternalWorkerJob job = getExternalWorkerJobById(jobId);

        if (!workerId.equals(job.getLockOwner())) {
            throw new FlowableForbiddenException(workerId + " does not hold a lock on the requested job");
        }

        if (job.getProcessInstanceId() != null) {
            if (managementService != null) {
                if (restApiInterceptor != null) {
                    restApiInterceptor.completeExternalWorkerJob(job, request);
                }

                managementService.createExternalWorkerCompletionBuilder(job.getId(), workerId)
                        .variables(extractVariables(request.getVariables()))
                        .complete();
            } else {
                throw new FlowableException("Cannot complete BPMN job. There is no BPMN engine available");
            }
        } else if (ScopeTypes.CMMN.equals(job.getScopeType())) {
            if (cmmnManagementService != null) {
                if (restApiInterceptor != null) {
                    restApiInterceptor.completeExternalWorkerJob(job, request);

View on GitHub (pinned to d6d39ce1c6)

Solutions

  1. Use exactly the workerId that was sent during acquisition when completing.
  2. Increase lockDuration or renew locks if long-running work causes lock expiry.
  3. On 403, re-acquire the topic and reprocess rather than retrying completion.
  4. Ensure only one worker instance processes a given acquired job.

Example fix

// before
acquire(workerId: "worker-A"); complete(jobId, workerId: "worker-B"); // 403
// after
AcquiredJob job = acquire("worker-A");
complete(job.getId(), "worker-A"); // same id
Defensive patterns

Strategy: try-catch

Validate before calling

// client can pre-check only if it stored the lock owner from acquisition
boolean isLockOwner = jobId.equals(currentLock.getJobId()) && workerId.equals(currentLock.getWorkerId());

Try / catch

try {
    restClient.complete(jobId, request);
} catch (HttpClientErrorException.Forbidden e) {
    log.warn("Lost lock on job {} (expired or re-acquired); re-acquiring", jobId);
    reacquireAndRetry(work);
}

Prevention

When it happens

Trigger: POST /external-worker/acquire/jobs/{jobId}/complete where request workerId != ACT_RU_EXTERNAL_JOB.LOCK_OWNER_ — e.g. a different worker instance attempts completion, or the lock expired and was re-acquired by another worker.

Common situations: Multiple workers sharing a workerId prefix but sending different actual ids; lock duration expired, job re-acquired elsewhere, then original worker tries to complete; load-balanced workers handling each other's acquired jobs; restart of worker generating a new random id while retrying completion of an old job.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of flowable/flowable-engine@d6d39ce1c6 (2026-09-11). Data as JSON: /api/errors/d90882296bb21249. Report an issue: GitHub.