flowable/flowable-engine · error · FlowableIllegalArgumentException
ExitCriterionId is null
Error message
ExitCriterionId is null
What it means
HistoricPlanItemInstanceQuery.planItemInstanceExitCriterionId() requires a non-null exit criterion id string. Flowable throws FlowableIllegalArgumentException immediately when null is passed, because a null filter would be indistinguishable from an unset filter and would silently change query semantics. This is an eager fail-fast guard at query-construction time, before any database access.
Solutions
- Pass a non-null exit criterion id string (the criterion element id from the CMMN model).
- Only call planItemInstanceExitCriterionId when the value is non-null; skip the filter (or use a different filter) otherwise.
- If filtering by 'has no exit criterion' is intended, do not use this method; instead fetch results and filter in application code or use a raw query.
- Check upstream data (HistoricPlanItemInstance.getExitCriterionId()) for null before reusing it as a query input.
Example fix
// before
query.planItemInstanceExitCriterionId(planItem.getExitCriterionId());
// after
String exitCriterionId = planItem.getExitCriterionId();
if (exitCriterionId != null) {
query.planItemInstanceExitCriterionId(exitCriterionId);
} Defensive patterns
Strategy: validation
Validate before calling
if (exitCriterionId == null || exitCriterionId.isEmpty()) {
throw new IllegalArgumentException("exitCriterionId must be provided");
}
query.planItemInstanceExitCriterionId(exitCriterionId); Type guard
boolean hasExitCriterionId(HistoricPlanItemInstance p) {
return p != null && p.getExitCriterionId() != null;
} Try / catch
try {
query.planItemInstanceExitCriterionId(exitCriterionId);
} catch (FlowableIllegalArgumentException e) {
log.warn("Invalid exit criterion id filter: {}", e.getMessage());
// rebuild query without the filter or return empty result
} Prevention
- Never forward HistoricPlanItemInstance.getExitCriterionId() straight into a query without a null check.
- Remember plan items completed (not exited) have a null exit criterion id.
- Centralize query building in one helper that validates filters before applying them.
- Validate DTO fields at the API boundary with bean validation (@NotNull) before reaching the Flowable query API.
When it happens
Trigger: Calling historicPlanItemInstanceQuery.planItemInstanceExitCriterionId(null) directly, or passing a variable/field that is null at runtime (e.g. planItemInstanceExitCriterionId(instance.getExitCriterionId()) where the plan item exited without a criterion, or a config/map lookup that returned null).
Common situations: Building history queries dynamically from a plan item instance whose exitCriterionId is null (plan items ended via completion rather than exit); copying filter fields between query objects where the source field was never set; deserializing filter DTOs from JSON where the key was absent.
Related errors
- activatedBefore is null
- assignee is null
- availableAfter is null
- availableBefore is null
- before time is null
AI-assisted analysis of flowable/flowable-engine@d6d39ce1c6 (2026-09-11).
Data as JSON: /api/errors/4a266a2bf0cccb16.
Report an issue: GitHub.
Appendix: source
Thrown at modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/history/HistoricPlanItemInstanceQueryImpl.java:320
}
@Override
public HistoricPlanItemInstanceQuery planItemInstanceEntryCriterionId(String entryCriterionId) {
if (entryCriterionId == null) {
throw new FlowableIllegalArgumentException("EntryCriterionId is null");
}
if (inOrStatement) {
this.currentOrQueryObject.entryCriterionId = entryCriterionId;
} else {
this.entryCriterionId = entryCriterionId;
}
return this;
}
@Override
public HistoricPlanItemInstanceQuery planItemInstanceExitCriterionId(String exitCriterionId) {
if (exitCriterionId == null) {
throw new FlowableIllegalArgumentException("ExitCriterionId is null");
}
if (inOrStatement) {
this.currentOrQueryObject.exitCriterionId = exitCriterionId;
} else {
this.exitCriterionId = exitCriterionId;
}
return this;
}
@Override
public HistoricPlanItemInstanceQuery planItemInstanceFormKey(String formKey) {
if (formKey == null) {
throw new FlowableIllegalArgumentException("formKey is null");
}
if (inOrStatement) {
this.currentOrQueryObject.formKey = formKey;
} else {
this.formKey = formKey;View on GitHub (pinned to d6d39ce1c6)