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
- Clamp the offset to <= 30 before composing the expression: 'L-30' is the maximum.
- If you need the 1st of the month, use a concrete day ('1') instead of a huge L-offset.
- Validate user-supplied offset input (1..30) in the UI/API before storing the cron string.
- 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
- Clamp user-supplied L offsets to 1..30 before composing the expression
- Use concrete day-of-month for small offsets (e.g. day 1 instead of L-30)
- Validate dynamic schedule inputs at the API boundary
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
- A numeric value between 1 and 5 must follow the '#' option
- '?' can only be specified for Day-of-Month -OR- Day-of-Week.
- '?' can only be specified for Day-of-Month or Day-of-Week.
- Illegal character after ?:
- Illegal characters for this position: '
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)