flowable/flowable-engine · error · FlowableException

Cannot set value of '', it's readonly!

Error message

Cannot set value of '', it's readonly!

What it means

ReadOnlyMapELResolver is an EL resolver that exposes a read-only map of variables to Flowable expressions. When an expression attempts to assign a value to a property backed by the wrapped map, the resolver deliberately rejects the write because the exposed map is intended to be immutable within expression evaluation. This guards engine-managed variables from being mutated mid-expression.

Solutions

  1. Replace the assignment expression with a write through the engine API (e.g. taskService.setVariable / execution.setVariable, or delegateExecution.setVariable in a delegate) instead of mutating variables inside EL.
  2. If writes are required, register a writable resolver/VariableContainer (e.g. VariableContainerWrapper with setVariable support) instead of ReadOnlyMapELResolver.
  3. Set the variable before the expression evaluation, not inside the expression itself.
  4. If the write is intentional inside a script, use a script task with proper variable mapping (in/out mappings) rather than EL assignment.

Example fix

// before
expression="${orderStatus = 'APPROVED'}"

// after
<flowable:field name="expression" expression="${orderStatus}" />
// plus in a delegate: execution.setVariable("orderStatus", "APPROVED");
Defensive patterns

Strategy: validation

Validate before calling

if (expression != null && expression.matches(".*[^=!<>+\-*/]=[^=].*")) {
    throw new IllegalArgumentException("Expression must not assign to read-only variables: " + expression);
}

Type guard

boolean isReadOnlyVariable(VariableContainer c, String name) {
    return c instanceof ReadOnlyMapELResolver; // writes via setVariable are rejected for read-only containers
}

Try / catch

try {
    managementService.executeCommand(new ExpressionSetCommand(expression));
} catch (FlowableException e) {
    if (e.getMessage() != null && e.getMessage().contains("readonly")) {
        throw new IllegalArgumentException("Assignments are not allowed on read-only variables");
    }
    throw e;
}

Prevention

When it happens

Trigger: Evaluating a Flowable expression that contains an assignment (e.g. ${myVar = 'x'} in a script/expression) where myVar is a key in the read-only map passed to the resolver; calling setValue on the resolver directly with a base of null and a property contained in wrappedMap.

Common situations: Using an assignment-style expression in a BPMN service task or listener expression where a read-only variable map (e.g. task/execution variables exposed read-only) is mutated; copying an expression written for a writable variable scope into a read-only context; custom code injecting ReadOnlyMapELResolver and then expecting variable writes to work.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


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

Appendix: source

Thrown at modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/el/ReadOnlyMapELResolver.java:56

        if (base == null) {
            if (wrappedMap.containsKey(property)) {
                context.setPropertyResolved(true);
                return wrappedMap.get(property);
            }
        }
        return null;
    }

    @Override
    public boolean isReadOnly(ELContext context, Object base, Object property) {
        return true;
    }

    @Override
    public void setValue(ELContext context, Object base, Object property, Object value) {
        if (base == null) {
            if (wrappedMap.containsKey(property)) {
                throw new FlowableException("Cannot set value of '" + property + "', it's readonly!");
            }
        }
    }

    @Override
    public Class<?> getCommonPropertyType(ELContext context, Object arg) {
        return Object.class;
    }

    @Override
    public Class<?> getType(ELContext context, Object arg1, Object arg2) {
        return Object.class;
    }
}

View on GitHub (pinned to d6d39ce1c6)