flowable/flowable-engine · error · ParseException

Offset from last day must be <= 30

Error message

Offset from last day must be <= 30

What it means

Thrown when the Day-of-Month field uses the 'L-offset' syntax (e.g. 'L-3' meaning 3 days before the last day of the month) and the offset exceeds 30. Since months never have more than 31 days, offsets > 30 are meaningless and rejected.

Solutions

  1. Clamp the offset to <= 30 before composing the expression: 'L-30' is the maximum.
  2. If you need the 1st of the month, use a concrete day ('1') instead of a huge L-offset.
  3. Validate user-supplied offset input (1..30) in the UI/API before storing the cron string.
  4. Catch ParseException and map it to a field-specific validation message for end users.

Example fix

// before
int off = 45;
new CronExpression("0 0 12 L-" + off + " * ?");
// after
int off = Math.min(off, 30);
new CronExpression("0 0 12 L-" + off + " * ?");
Defensive patterns

Strategy: validation

Validate before calling

java.util.regex.Matcher m = java.util.regex.Pattern.compile("L-(\\d+)").matcher(expr);
if (m.find() && Integer.parseInt(m.group(1)) > 30) {
    throw new IllegalArgumentException("L offset must be <= 30: " + expr);
}

Try / catch

try {
    new CronExpression(expr);
} catch (java.text.ParseException e) {
    if (e.getMessage().contains("Offset from last day")) {
        throw new IllegalArgumentException("L- offset must be <= 30 in: " + expr, e);
    }
}

Prevention

When it happens

Trigger: new CronExpression("0 0 12 L-31 * ?") or any 'L-n' with n > 31... specifically > 30 per this check — e.g. 'L-45', 'L-100', often from an unbounded user-provided offset variable interpolated into the expression.

Common situations: Dynamic schedules where the offset comes from user config without bounds checking; confusion about semantics (thinking L-31 is legal because months have 31 days); generators producing large negative-day values.

Understand the failure class

Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.

Related errors


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

Appendix: source

Thrown at modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/calendar/CronExpression.java:674

            }

            addToSet(ALL_SPEC_INT, -1, incr, type);
            return i;
        } else if (c == 'L') {
            i++;
            if (type == DAY_OF_MONTH) {
                lastdayOfMonth = true;
            }
            if (type == DAY_OF_WEEK) {
                addToSet(7, 7, 0, type);
            }
            if (type == DAY_OF_MONTH && s.length() > i) {
                c = s.charAt(i);
                if (c == '-') {
                    ValueSet vs = getValue(0, s, i + 1);
                    lastdayOffset = vs.value;
                    if (lastdayOffset > 30) {
                        throw new ParseException("Offset from last day must be <= 30", i + 1);
                    }
                    i = vs.pos;
                }
                if (s.length() > i) {
                    c = s.charAt(i);
                    if (c == 'W') {
                        nearestWeekday = true;
                        i++;
                    }
                }
            }
            return i;
        } else if (c >= '0' && c <= '9') {
            int val = Integer.parseInt(String.valueOf(c));
            i++;
            if (i >= s.length()) {
                addToSet(val, -1, -1, type);
            } else {

View on GitHub (pinned to d6d39ce1c6)