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

  1. Pass a non-null exit criterion id string (the criterion element id from the CMMN model).
  2. Only call planItemInstanceExitCriterionId when the value is non-null; skip the filter (or use a different filter) otherwise.
  3. 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.
  4. 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

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


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)