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

  1. Inspect the character and index reported in the message and remove/replace the invalid character
  2. Validate the expression offline against the Quartz cron syntax Flowable uses before deploying
  3. Replace unsupported macros like '@hourly' with the equivalent explicit expression ('0 0 * * * ?')
  4. 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

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)