flowable/flowable-engine · error · FlowableException

form property '" + id + "' is not writable

Error message

form property '" + id + "' is not writable

What it means

FormPropertyHandler.submitFormProperty enforces the writability flag of each BPMN form property (flowable:writable="false"). If a submitted property map contains a value for a read-only property, FlowableException "form property '<id>' is not writable" is thrown, preventing clients from injecting values into properties marked read-only.

Solutions

  1. Remove the read-only key from the submitted map before calling the form service.
  2. If the field must be editable, set flowable:writable="true" (or omit it) on the form property in the BPMN.
  3. Filter submissions by querying getTaskFormData(...).getFormProperties() and only submitting writable ones.
  4. Update the client UI to disable submission of non-writable fields.

Example fix

// before
formService.submitTaskFormData(taskId, allRenderedFields); // includes readOnly field 'status'
// after
Map<String,String> writable = new HashMap<>();
for (FormProperty p : formService.getTaskFormData(taskId).getFormProperties()) {
    if (p.isWritable() && allRenderedFields.containsKey(p.getId())) {
        writable.put(p.getId(), allRenderedFields.get(p.getId()));
    }
}
formService.submitTaskFormData(taskId, writable);
Defensive patterns

Strategy: validation

Validate before calling

Map<String,String> submit = properties.entrySet().stream()
    .filter(e -> formProperties.stream()
        .anyMatch(p -> p.getId().equals(e.getKey()) && p.isWritable()))
    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

Try / catch

try {
    formService.submitTaskFormData(taskId, properties);
} catch (FlowableException e) {
    if (e.getMessage().endsWith("is not writable")) {
        String readOnlyId = extractPropertyId(e.getMessage());
        properties.remove(readOnlyId);
        formService.submitTaskFormData(taskId, properties);
    } else { throw e; }
}

Prevention

When it happens

Trigger: FormService.submitTaskFormData / startProcessInstanceByForm with a map containing a key whose FormProperty was declared writable="false" in the BPMN start/task form.

Common situations: Generic form UIs that submit every rendered field including read-only ones, clients copying start-form values into task-form submissions, accidental reuse of a shared properties map across forms.

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/4864c34488b027d0. Report an issue: GitHub.

Appendix: source

Thrown at modules/flowable-engine/src/main/java/org/flowable/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 FlowableException("form property '" + id + "' is not writable");
        }

        if (isRequired && !properties.containsKey(id) && defaultExpression == null) {
            throw new FlowableException("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)