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

  1. Make the target bean implement org.flowable.engine.delegate.CustomPropertiesResolver and its getCustomPropertiesMap(DelegateExecution) method.
  2. 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).
  3. Check for Spring AOP proxies: if @Transactional/@Async wraps the bean, ensure the proxy still implements the interface (interface-based proxying).
  4. 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

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


AI-assisted analysis of flowable/flowable-engine@d6d39ce1c6 (2026-09-11). Data as JSON: /api/errors/dc64de46a3fdbeb7. Report an issue: GitHub.