flowable/flowable-engine · error · ParseException
Unexpected character:
Error message
Unexpected character:
What it means
CronExpression parses cron strings field by field in storeExpressionVals. When it encounters a character at the current position that cannot start or continue a valid field expression (a digit, '*', '?', '/', or option letter), it throws this ParseException with the offending character and its index. It means the cron expression contains a syntactically invalid character.
Solutions
- Inspect the character and index reported in the message and remove/replace the invalid character
- Validate the expression offline against the Quartz cron syntax Flowable uses before deploying
- Replace unsupported macros like '@hourly' with the equivalent explicit expression ('0 0 * * * ?')
- Ensure the expression has the right number of fields (6-7: sec min hour dom mon dow [year]) so characters fall in expected positions
Example fix
// before
new CronExpression("0 0 12 * * #");
// after
new CronExpression("0 0 12 * * ?"); Defensive patterns
Strategy: validation
Validate before calling
boolean isValidCron(String expr) {
if (expr == null || expr.isBlank()) return false;
String allowed = "0123456789*?,-/LMW#ABCDEFGHIJKLMNOPQRSTUVWXYZabcxyz ";
for (char c : expr.toCharArray()) {
if (allowed.indexOf(c) < 0 && !Character.isDigit(c)) return false;
}
int fields = expr.trim().split("\\s+").length;
return fields == 6 || fields == 7;
} Try / catch
try {
CronExpression cron = new CronExpression(cronExpr);
} catch (ParseException e) {
log.error("Invalid cron expression '{}' at index {}: {}", cronExpr, e.getErrorOffset(), e.getMessage());
throw new ConfigurationException("Bad cron expression", e);
} Prevention
- Validate cron expressions at configuration load time, not first use
- Test expressions with a helper that computes the next fire time
- Avoid copying cron strings from dialects with different syntax; use 6-7 field Quartz format
- Replace macros like @hourly with explicit Quartz expressions
When it happens
Trigger: Parsing a cron string (new CronExpression(expr) or via CronExpressionFactory) that contains characters like '#', '%', '@', ',' where a value was expected, or a stray letter, e.g. "0 0 12 * * #" or whitespace/formatting mistakes, non-ASCII digits, copy-paste artifacts.
Common situations: Configuration typos in application.yml / process XML timer definitions; expressions copied from Windows-style or Quartz GUI tools with extra characters; quotes or trailing whitespace characters; localization issues (Unicode digits).
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
AI-assisted analysis of flowable/flowable-engine@d6d39ce1c6 (2026-09-11).
Data as JSON: /api/errors/c4bdcf55d0ec248c.
Report an issue: GitHub.
Appendix: source
Thrown at modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/calendar/CronExpression.java:703
}
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 {
c = s.charAt(i);
if (c >= '0' && c <= '9') {
ValueSet vs = getValue(val, s, i);
val = vs.value;
i = vs.pos;
}
i = checkNext(i, s, val, type);
return i;
}
} else {
throw new ParseException("Unexpected character: " + c, i);
}
return i;
}
private void checkIncrementRange(int incr, int type, int idxPos) throws ParseException {
if (incr > 59 && (type == SECOND || type == MINUTE)) {
throw new ParseException("Increment > 60 : " + incr, idxPos);
} else if (incr > 23 && (type == HOUR)) {
throw new ParseException("Increment > 24 : " + incr, idxPos);
} else if (incr > 31 && (type == DAY_OF_MONTH)) {
throw new ParseException("Increment > 31 : " + incr, idxPos);
} else if (incr > 7 && (type == DAY_OF_WEEK)) {
throw new ParseException("Increment > 7 : " + incr, idxPos);
} else if (incr > 12 && (type == MONTH)) {
throw new ParseException("Increment > 12 : " + incr, idxPos);
}
}View on GitHub (pinned to d6d39ce1c6)