flowable/flowable-engine · error · ScanException
invalid character '" + c1 + "'
Error message
invalid character '" + c1 + "'
What it means
Scanner.nextEval() throws ScanException("invalid character 'c'") when, while scanning the inside of a ${...} evaluation block, the current character cannot start any valid expression token (not a digit, letter, quote, operator, or whitespace). The expected field is 'expression token'.
Solutions
- Find and remove/replace the reported invalid character (ScanException includes position and the character).
- Re-type the expression manually instead of pasting to eliminate smart quotes and invisible Unicode.
- Ensure only supported operator/symbol characters are used inside ${...} blocks.
- Check file encoding (UTF-8 vs Latin-1) if the character came from a config file.
Example fix
// before
String expr = "${order#total > 5}"; // '#' invalid here
// after
String expr = "${order.total > 5}"; Defensive patterns
Strategy: validation
Validate before calling
// reject characters that cannot start an EL token inside ${...}
Pattern VALID = Pattern.compile("[A-Za-z0-9_$'\"().,=<>!+\-*/%?:&|^~{}\\[\\] ]*");
boolean scanClean(String expr) { return VALID.matcher(expr).matches(); } Try / catch
try {
builder.build(expr);
} catch (TreeBuilderException e) {
throw new IllegalArgumentException("Invalid character in expression near pos " + e.getPosition() + ": " + expr);
} Prevention
- Strip smart quotes and invisible Unicode when pasting expressions.
- Use only documented EL operators inside ${...} blocks.
- Validate config file encoding (UTF-8).
- Parse-check expressions in CI.
When it happens
Trigger: Any character outside the EL grammar appearing inside ${...}, such as '@', '#', '!', misused, a stray '%' or '~' that is not part of an operator, or a control/unicode character pasted into the expression.
Common situations: Copy-pasting expressions from documents/emails introducing smart quotes or invisible characters; typos in operators (e.g. '##'); placeholder syntax from other templating engines (#{} or %%{}) pasted into a ${} block; corrupted config files with encoding issues.
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.
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- invalid escape sequence \\
- unterminated string
- Builder is missing constructor (can't pass features)
- Cannot change fixed value with
- Cannot coerce property to array index:
AI-assisted analysis of flowable/flowable-engine@d6d39ce1c6 (2026-09-11).
Data as JSON: /api/errors/3a788e9679adb3d3.
Report an issue: GitHub.
Appendix: source
Thrown at modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/de/odysseus/el/tree/impl/Scanner.java:451
if (Character.isJavaIdentifierStart(c1)) {
int i = position+1;
int l = input.length();
while (i < l && Character.isJavaIdentifierPart(input.charAt(i))) {
i++;
}
String name = input.substring(position, i);
Token keyword;
switch (token.getSymbol()) {
case COLON:
keyword = javaKeyword(name);
break;
default:
keyword = keyword(name);
}
return keyword == null ? token(Symbol.IDENTIFIER, name, i - position) : keyword;
}
throw new ScanException(position, "invalid character '" + c1 + "'", "expression token");
}
protected Token nextToken() throws ScanException {
if (isEval()) {
if (input.charAt(position) == '}') {
return fixed(Symbol.END_EVAL);
}
return nextEval();
} else {
if (position+1 < input.length() && input.charAt(position+1) == '{') {
switch (input.charAt(position)) {
case '#':
return fixed(Symbol.START_EVAL_DEFERRED);
case '$':
return fixed(Symbol.START_EVAL_DYNAMIC);
}
}
return nextText();View on GitHub (pinned to d6d39ce1c6)