flowable/flowable-engine · error · FlowableIllegalArgumentException
Custom properties resolver delegate expression " + expressio
Error message
Custom properties resolver delegate expression " + expression + " did not resolve to an implementation of " + CustomPropertiesResolver.class
What it means
Flowable's DelegateExpressionCustomPropertiesResolver evaluates a delegate expression (e.g. '${someBean}') at runtime and expects the resolved object to implement CustomPropertiesResolver. When the expression resolves to an object of another type, getCustomPropertiesMap throws FlowableIllegalArgumentException. This fail-fast check prevents an unsafe cast later in listener dispatch.
Source
Thrown at modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/listener/DelegateExpressionCustomPropertiesResolver.java:43
public class DelegateExpressionCustomPropertiesResolver implements CustomPropertiesResolver {
protected Expression expression;
public DelegateExpressionCustomPropertiesResolver(Expression expression) {
this.expression = expression;
}
@Override
public Map<String, Object> getCustomPropertiesMap(DelegateExecution execution) {
// Note: we can't cache the result of the expression, because the
// execution can change: eg.
// delegateExpression='${mySpringBeanFactory.randomSpringBean()}'
Object delegate = expression.getValue(execution);
if (delegate instanceof CustomPropertiesResolver) {
return ((CustomPropertiesResolver) delegate).getCustomPropertiesMap(execution);
} else {
throw new FlowableIllegalArgumentException("Custom properties resolver delegate expression " + expression + " did not resolve to an implementation of " + CustomPropertiesResolver.class);
}
}
/**
* returns the expression text for this execution listener. Comes in handy if you want to check which listeners you already have.
*/
public String getExpressionText() {
return expression.getExpressionText();
}
}
View on GitHub (pinned to d6d39ce1c6)
Solutions
- Make the target bean implement org.flowable.engine.delegate.CustomPropertiesResolver and its getCustomPropertiesMap(DelegateExecution) method.
- Verify the expression actually points at the intended bean (e.g. '${myResolver}' resolving to a different bean of the same name in a different module).
- Check for Spring AOP proxies: if @Transactional/@Async wraps the bean, ensure the proxy still implements the interface (interface-based proxying).
- If the expression is a factory call like '${factory.create()}', confirm the factory returns a CustomPropertiesResolver instance, not a raw object.
Example fix
// before
public class MyResolver {
public Map<String, Object> getProps(DelegateExecution ex) { ... }
}
// after
import org.flowable.engine.delegate.CustomPropertiesResolver;
public class MyResolver implements CustomPropertiesResolver {
@Override
public Map<String, Object> getCustomPropertiesMap(DelegateExecution execution) { ... }
} Defensive patterns
Strategy: type-guard
Validate before calling
Object bean = applicationContext.getBean("myResolver");
if (!(bean instanceof org.flowable.engine.delegate.CustomPropertiesResolver)) {
throw new IllegalStateException("Bean 'myResolver' must implement CustomPropertiesResolver");
} Type guard
public static boolean isCustomPropertiesResolver(Object o) {
return o instanceof org.flowable.engine.delegate.CustomPropertiesResolver;
} Try / catch
try {
listener.getCustomPropertiesMap(execution);
} catch (org.flowable.common.engine.api.FlowableIllegalArgumentException e) {
log.error("Delegate expression did not resolve to a CustomPropertiesResolver: {}", e.getMessage());
throw new ConfigurationException("Fix the delegateExpression to point at a CustomPropertiesResolver bean", e);
} Prevention
- Always implement org.flowable.engine.delegate.CustomPropertiesResolver explicitly and add a unit test asserting the bean's type.
- Keep one canonical resolver interface import in the codebase and forbid legacy/wrong-package imports via checkstyle.
- When using Spring AOP proxies, verify proxy.getTargetClass() still implements the expected interface.
- Write an integration test that boots the process definition and evaluates every delegate expression before deploying.
When it happens
Trigger: A custom properties resolver is configured via delegate expression (flowable:customPropertiesResolverDelegateExpression or listener custom-properties resolver referencing an expression), the expression evaluates successfully (bean exists), but the returned bean does not implement org.flowable.engine.delegate.CustomPropertiesResolver.
Common situations: Spring bean wired to the wrong class or an interface misspelled in the bean's implements clause; refactoring renamed/moved CustomPropertiesResolver (e.g. org.flowable.engine.impl.bpmn.listener vs org.flowable.engine.delegate) so the bean implements the wrong package's interface; expression returning a proxy (Spring AOP) that fails instanceof; reusing an execution-listener bean as a custom-properties resolver.
Understand the failure class
Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.
Related errors
- Delegate expression " + expression + " did not resolve to an
- Delegate expression " + expression + " did not resolve to an
- Delegate expression " + expression + " did not resolve to an
- Delegate expression " + expression + " did not resolve to an
- Delegate expression ${expression} did not resolve to an impl
AI-assisted analysis of flowable/flowable-engine@d6d39ce1c6 (2026-09-11).
Data as JSON: /api/errors/dc64de46a3fdbeb7.
Report an issue: GitHub.