flowable/flowable-engine · error · FlowableIllegalArgumentException
TenantId can only be used with either processDefinitionKey…
Error message
TenantId can only be used with either processDefinitionKey or message.
What it means
FlowableIllegalArgumentException thrown by createProcessInstance when the request sets a tenantId (request.isTenantSet() is true) together with processDefinitionId. Tenant resolution happens when looking up a definition by key or message; with an explicit definition id the tenant is already fully determined, so passing a tenantId is contradictory and rejected.
Solutions
- Remove tenantId from the request when starting by processDefinitionId
- Switch to processDefinitionKey plus tenantId if tenant scoping is required
- Have the server-side tenant context resolve the definition key per tenant instead of sending an id
- Audit request-building code to only add tenantId for key/message starts
Example fix
// before
{"processDefinitionId":"orderProcess:2:125","tenantId":"acme"}
// after
{"processDefinitionKey":"orderProcess","tenantId":"acme"} Defensive patterns
Strategy: validation
Validate before calling
if (req.tenantId != null && req.processDefinitionId != null) {
throw new Error('tenantId cannot be combined with processDefinitionId; use processDefinitionKey or message');
} Type guard
function tenantUsageValid(req) {
return !(req?.tenantId != null && req?.processDefinitionId != null);
} Try / catch
try { await startProcessInstance(req); } catch (e) { if (e.status === 400 && /TenantId can only be used/.test(e.message)) { delete req.processDefinitionId; } throw e; } Prevention
- Only attach tenantId when starting by key or message
- Remove tenant fields when switching to definition-id based starts
- Centralize start-request construction so tenant rules live in one place
When it happens
Trigger: POST /runtime/process-instances with both processDefinitionId and tenantId (or the tenantId field populated such that isTenantSet() returns true) in the body.
Common situations: Multi-tenant clients always including tenantId out of habit, generic request builders that attach tenant context to every start call, migrating code from key-based to id-based starts while keeping the tenant field.
Understand the failure class
Background: Conflicting config options: "cannot be used together" — configuration validation errors across open-source libraries — this error's family across 162 libraries.
Related errors
- A group or a user is required to create an identity link.
- A group or a user is required to create an identity link.
- Comment text is required.
- Either processDefinitionId, processDefinitionKey or message…
- Identity link family should be 'users' or 'groups'.
AI-assisted analysis of flowable/flowable-engine@d6d39ce1c6 (2026-09-11).
Data as JSON: /api/errors/32c0e30f976dca0f.
Report an issue: GitHub.
Appendix: source
Thrown at modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/ProcessInstanceCollectionResource.java:343
})
@PostMapping(value = "/runtime/process-instances", produces = "application/json")
@ResponseStatus(HttpStatus.CREATED)
public ProcessInstanceResponse createProcessInstance(@RequestBody ProcessInstanceCreateRequest request) {
if (request.getProcessDefinitionId() == null && request.getProcessDefinitionKey() == null && request.getMessage() == null) {
throw new FlowableIllegalArgumentException("Either processDefinitionId, processDefinitionKey or message is required.");
}
int paramsSet = ((request.getProcessDefinitionId() != null) ? 1 : 0) + ((request.getProcessDefinitionKey() != null) ? 1 : 0) + ((request.getMessage() != null) ? 1 : 0);
if (paramsSet > 1) {
throw new FlowableIllegalArgumentException("Only one of processDefinitionId, processDefinitionKey or message should be set.");
}
if (request.isTenantSet()) {
// Tenant-id can only be used with either key or message
if (request.getProcessDefinitionId() != null) {
throw new FlowableIllegalArgumentException("TenantId can only be used with either processDefinitionKey or message.");
}
}
Map<String, Object> startVariables = null;
Map<String, Object> transientVariables = null;
Map<String, Object> startFormVariables = null;
if (request.getStartFormVariables() != null && !request.getStartFormVariables().isEmpty()) {
startFormVariables = new HashMap<>();
for (RestVariable variable : request.getStartFormVariables()) {
if (variable.getName() == null) {
throw new FlowableIllegalArgumentException("Variable name is required.");
}
startFormVariables.put(variable.getName(), restResponseFactory.getVariableValue(variable));
}
}
if (request.getVariables() != null && !request.getVariables().isEmpty()) {
startVariables = new HashMap<>();View on GitHub (pinned to d6d39ce1c6)