{"record":{"id":"b21b3cf58360f81c","repo":"flowable/flowable-engine","slug":"provided-worker-id-has-external-worker-jobs-from-d","errorCode":null,"errorMessage":"provided worker id has external worker jobs from different tenant.","messagePattern":"provided worker id has external worker jobs from different tenant\\.","errorType":"validation","errorClass":"FlowableIllegalArgumentException","httpStatus":null,"severity":"error","filePath":"modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/cmd/UnacquireAllExternalWorkerJobsForWorkerCmd.java","lineNumber":51,"sourceCode":"        this.tenantId = tenantId;\n        this.jobServiceConfiguration = jobServiceConfiguration;\n    }\n\n    @Override\n    public Void execute(CommandContext commandContext) {\n        if (StringUtils.isEmpty(workerId)) {\n            throw new FlowableIllegalArgumentException(\"worker id must not be empty\");\n        }\n\n        ExternalWorkerJobEntityManager externalWorkerJobEntityManager = jobServiceConfiguration.getExternalWorkerJobEntityManager();\n\n        List<ExternalWorkerJobEntity> jobEntities = externalWorkerJobEntityManager.findJobsByWorkerId(workerId);\n        \n        if (!jobEntities.isEmpty()) {\n            if (StringUtils.isNotEmpty(tenantId)) {\n                for (ExternalWorkerJobEntity externalWorkerJob : jobEntities) {\n                    if (!tenantId.equals(externalWorkerJob.getTenantId())) {\n                        throw new FlowableIllegalArgumentException(\"provided worker id has external worker jobs from different tenant.\");\n                    }\n                }\n            }\n            \n            for (ExternalWorkerJobEntity externalWorkerJob : jobEntities) {\n                if (externalWorkerJob.isExclusive()) {\n                    new UnlockExclusiveJobCmd(externalWorkerJob, jobServiceConfiguration).execute(commandContext);\n                }\n            }\n            \n            externalWorkerJobEntityManager.bulkUpdateJobLockWithoutRevisionCheck(jobEntities, null, null);\n        }\n        \n        return null;\n    }\n}\n","sourceCodeStart":33,"sourceCodeEnd":68,"githubUrl":"https://github.com/flowable/flowable-engine/blob/d6d39ce1c69ff244f2d9dc6af756a9b95e865586/modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/cmd/UnacquireAllExternalWorkerJobsForWorkerCmd.java#L33-L68","documentation":"UnacquireAllExternalWorkerJobsForWorkerCmd.execute throws this FlowableIllegalArgumentException when an explicit tenantId is provided but at least one external worker job currently held by that worker belongs to a different tenant. Flowable refuses the bulk unacquire because it would cross tenant boundaries, enforcing tenant isolation for multi-tenant deployments.","triggerScenarios":"Calling unacquireAllExternalWorkerJobsForWorker(workerId, tenantId) where the worker (shared across tenants, e.g. same worker id registered globally) holds jobs from tenant A and tenant B while the caller passes tenantId=A; jobs created before a tenant migration still carrying an old/empty tenant id.","commonSituations":"Multi-tenant setups where the same worker id is reused across tenants; tenant id defaulting to empty string on job creation while the caller passes a real tenant id; tenant renaming/migration leaving stale tenant ids on jobs.","solutions":["Use unique worker ids per tenant so a worker never holds cross-tenant jobs","Pass null/empty tenantId if cross-tenant release is intended (no tenant filter applied)","Clean up mismatched tenant ids on the jobs or migrate them to the correct tenant","Catch FlowableIllegalArgumentException and fall back to per-job unacquire for matching tenants only"],"exampleFix":"// before\njobService.unacquireAllExternalWorkerJobsForWorker(workerId, \"tenantA\"); // fails: worker holds jobs of other tenants\n// after\njobService.unacquireAllExternalWorkerJobsForWorker(workerId, null); // release all jobs of this worker, any tenant","handlingStrategy":"validation","validationCode":"// only pass a tenantId if this worker exclusively serves that tenant\nboolean tenantMatches = jobs.stream()\n    .allMatch(j -> Objects.equals(expectedTenantId, j.getTenantId()));\nif (!tenantMatches) { /* handle cross-tenant case */ }","typeGuard":null,"tryCatchPattern":"try {\n    jobService.unacquireAllExternalWorkerJobsForWorker(workerId, tenantId);\n} catch (FlowableIllegalArgumentException e) {\n    jobService.unacquireAllExternalWorkerJobsForWorker(workerId, null);\n}","preventionTips":["Use unique worker ids per tenant in multi-tenant deployments","Audit jobs with stale/empty tenant ids after tenant migrations","Pass null tenantId intentionally when cross-tenant release is desired"],"tags":["multi-tenant","flowable","external-worker"],"backgroundTag":"invalid-argument-value","analyzedSha":"d6d39ce1c69ff244f2d9dc6af756a9b95e865586","analyzedAt":"2026-09-11T06:41:19.413Z","contentChangedAt":"2026-09-11T06:41:19.413Z","schemaVersion":2},"datasetVersion":"2026-09-18T11:17:12.947Z"}