flowable/flowable-engine · error · FlowableException

endOr() can only be called after calling or()

Error message

endOr() can only be called after calling or()

What it means

API-misuse guard in CaseInstanceQueryImpl.endOr: the OR block was terminated without a matching or() call (or the internal inOrStatement flag was never set), so the query state is unbalanced and the builder refuses to continue.

Solutions

  1. Track whether or() was called (boolean flag) and only call endOr() conditionally.
  2. Remove the stray endOr() call, or pair it with an or() that adds the intended OR conditions.
  3. Audit chained builder code so every endOr() has exactly one matching or() and blocks are not nested.
  4. Centralize OR-block construction in one helper method that owns both or() and endOr().

Example fix

// before
query.caseDefinitionKey("A");
query.endOr(); // throws: or() never called
// after
boolean inOr = false;
if (needsOrFilter) {
    query.or().caseDefinitionKey("A").caseDefinitionKey("B");
    inOr = true;
}
if (inOr) {
    query.endOr();
}
Defensive patterns

Strategy: try-catch

Validate before calling

boolean orActive = query instanceof CaseInstanceQueryImpl && ((CaseInstanceQueryImpl) query).inOrStatement;
if (orActive) {
    query.endOr();
}

Try / catch

try {
    query.endOr();
} catch (FlowableException e) {
    if (e.getMessage().contains("endOr() can only be called after calling or()")) {
        log.debug("endOr() without or(): ignored");
    } else {
        throw e;
    }
}

Prevention

When it happens

Trigger: Calling query.endOr() unconditionally (e.g. in a finally block or generic builder teardown) when or() was never invoked; conditional code paths where or() was skipped but endOr() still runs; calling endOr() twice.

Common situations: Template/generic query builders that always emit endOr(); refactors where the or() call was removed but endOr() remained; accidental duplicate endOr() in chained builder code.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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

Appendix: source

Thrown at modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/runtime/CaseInstanceQueryImpl.java:889

    public CaseInstanceQuery or() {
        if (inOrStatement) {
            throw new FlowableException("the query is already in an or statement");
        }

        inOrStatement = true;
        if (commandContext != null) {
            currentOrQueryObject = new CaseInstanceQueryImpl(commandContext, cmmnEngineConfiguration);
        } else {
            currentOrQueryObject = new CaseInstanceQueryImpl(commandExecutor, cmmnEngineConfiguration);
        }
        orQueryObjects.add(currentOrQueryObject);
        return this;
    }

    @Override
    public CaseInstanceQuery endOr() {
        if (!inOrStatement) {
            throw new FlowableException("endOr() can only be called after calling or()");
        }

        inOrStatement = false;
        currentOrQueryObject = null;
        return this;
    }
    
    @Override
    public CaseInstanceQuery variableValueEquals(String variableName, Object variableValue) {
        if (inOrStatement) {
            currentOrQueryObject.variableValueEquals(variableName, variableValue, false);
            return this;
        } else {
            return variableValueEquals(variableName, variableValue, false);
        }
    }

    @Override

View on GitHub (pinned to d6d39ce1c6)