flowable/flowable-engine · error · ActivitiException

form property ' ' is not writable

Error message

form property '${id}' is not writable

What it means

Thrown by FormPropertyHandler.submitFormProperty when a form property is submitted for an execution but the property is marked as not writable (isWritable false, i.e. activiti:writable="false" or a read-only property) and the submitted map contains that id. FormPropertySession calls this for each submitted property when a form is submitted for a task/execution.

Solutions

  1. Remove read-only property ids from the submitted map before calling submitTaskFormData.
  2. Filter the payload using FormData.getFormProperties(): submit only entries whose FormProperty.isWritable() is true.
  3. Keep computed/read-only values out of client payloads entirely; let the engine fill them via their expression.
  4. If the property genuinely needs to be submitted, set activiti:writable="true" in the BPMN form property declaration.

Example fix

// before
formService.submitTaskFormData(taskId, allRenderedProps); // includes read-only ids
// after
Map<String, String> writable = new HashMap<>();
for (FormProperty fp : formService.getTaskFormData(taskId).getFormProperties()) {
    if (fp.isWritable() && allRenderedProps.containsKey(fp.getId())) {
        writable.put(fp.getId(), allRenderedProps.get(fp.getId()));
    }
}
formService.submitTaskFormData(taskId, writable);
Defensive patterns

Strategy: validation

Validate before calling

FormData data = formService.getTaskFormData(taskId);
Map<String,String> writable = new HashMap<>();
for (FormProperty fp : data.getFormProperties()) {
    if (fp.isWritable() && submitted.containsKey(fp.getId())) {
        writable.put(fp.getId(), submitted.get(fp.getId()));
    }
}
formService.submitTaskFormData(taskId, writable);

Try / catch

try {
    formService.submitTaskFormData(taskId, props);
} catch (org.activiti.engine.ActivitiException e) {
    if (e.getMessage().endsWith("is not writable")) {
        LOGGER.warn("Stripping read-only props and resubmitting");
        props.keySet().removeIf(id -> !isWritable(id));
        formService.submitTaskFormData(taskId, props);
    }
}

Prevention

When it happens

Trigger: Calling formService.submitTaskFormData(taskId, properties) or saveTaskForm with a map containing the id of a read-only form property (e.g. one filled by the engine via expression, or declared writable=false in BPMN).

Common situations: Front-ends echoing back ALL rendered form properties (including read-only ones) in the submit payload; form definitions where a property was flipped to writable=false but the client wasn't updated; auto-generated forms posting back computed fields.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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

Appendix: source

Thrown at modules/flowable5-engine/src/main/java/org/activiti/engine/impl/form/FormPropertyHandler.java:80

                modelValue = defaultExpression.getValue(NoExecutionVariableScope.getSharedInstance());
            }
        }

        if (modelValue instanceof String) {
            formProperty.setValue((String) modelValue);
        } else if (type != null) {
            String formValue = type.convertModelValueToFormValue(modelValue);
            formProperty.setValue(formValue);
        } else if (modelValue != null) {
            formProperty.setValue(modelValue.toString());
        }

        return formProperty;
    }

    public void submitFormProperty(ExecutionEntity execution, Map<String, String> properties) {
        if (!isWritable && properties.containsKey(id)) {
            throw new ActivitiException("form property '" + id + "' is not writable");
        }

        if (isRequired && !properties.containsKey(id) && defaultExpression == null) {
            throw new ActivitiException("form property '" + id + "' is required");
        }
        boolean propertyExists = false;
        Object modelValue = null;
        if (properties.containsKey(id)) {
            propertyExists = true;
            final String propertyValue = properties.remove(id);
            if (type != null) {
                modelValue = type.convertFormValueToModelValue(propertyValue);
            } else {
                modelValue = propertyValue;
            }
        } else if (defaultExpression != null) {
            final Object expressionValue = defaultExpression.getValue(execution);
            if (type != null && expressionValue != null) {

View on GitHub (pinned to d6d39ce1c6)