flowable/flowable-engine · error · FlowableIllegalArgumentException
Delegate expression " + expression + " did not resolve to an
Error message
Delegate expression " + expression + " did not resolve to an implementation of " + ExecutionListener.class + " nor " + JavaDelegate.class
What it means
DelegateExpressionExecutionListener.resolveDelegateExpression resolves the configured delegate expression at event time. The result must implement either ExecutionListener or JavaDelegate (JavaDelegate instances are wrapped in JavaDelegateInvocation); otherwise notify throws FlowableIllegalArgumentException. Flowable deliberately supports both types here, so any other object type fails.
Source
Thrown at modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/listener/DelegateExpressionExecutionListener.java:49
public class DelegateExpressionExecutionListener implements ExecutionListener {
protected Expression expression;
private final List<FieldDeclaration> fieldDeclarations;
public DelegateExpressionExecutionListener(Expression expression, List<FieldDeclaration> fieldDeclarations) {
this.expression = expression;
this.fieldDeclarations = fieldDeclarations;
}
@Override
public void notify(DelegateExecution execution) {
Object delegate = DelegateExpressionUtil.resolveDelegateExpression(expression, execution, fieldDeclarations);
if (delegate instanceof ExecutionListener) {
CommandContextUtil.getProcessEngineConfiguration().getDelegateInterceptor().handleInvocation(new ExecutionListenerInvocation((ExecutionListener) delegate, execution));
} else if (delegate instanceof JavaDelegate) {
CommandContextUtil.getProcessEngineConfiguration().getDelegateInterceptor().handleInvocation(new JavaDelegateInvocation((JavaDelegate) delegate, execution));
} else {
throw new FlowableIllegalArgumentException("Delegate expression " + expression + " did not resolve to an implementation of " + ExecutionListener.class + " nor " + JavaDelegate.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
- Implement org.flowable.engine.delegate.ExecutionListener (notify(DelegateExecution)) in the delegate bean.
- If the bean is meant to do service work, implement org.flowable.engine.delegate.JavaDelegate (execute(DelegateExecution)) instead — this listener also accepts JavaDelegates.
- Fix the delegate expression to point at the bean that actually implements one of the accepted interfaces.
- After an Activiti-to-Flowable migration, update imports from org.activiti.* delegate interfaces to org.flowable.engine.delegate.*.
Example fix
// before
<flowable:executionListener event="start" delegateExpression="${orderHelper}" />
// orderHelper implements neither interface
// after: make OrderHelper implement the interface
public class OrderHelper implements ExecutionListener {
@Override
public void notify(DelegateExecution execution) { ... }
} Defensive patterns
Strategy: validation
Validate before calling
Object bean = applicationContext.getBean("orderHelper");
if (!(bean instanceof ExecutionListener) && !(bean instanceof JavaDelegate)) {
throw new IllegalStateException("orderHelper must implement ExecutionListener or JavaDelegate");
} Type guard
public static boolean isValidExecutionDelegate(Object o) {
return o instanceof org.flowable.engine.delegate.ExecutionListener
|| o instanceof org.flowable.engine.delegate.JavaDelegate;
} Try / catch
try {
executionListener.notify(execution);
} catch (org.flowable.common.engine.api.FlowableIllegalArgumentException e) {
log.error("Bad delegateExpression config: {}", e.getMessage());
throw new IllegalArgumentException("delegateExpression must resolve to an ExecutionListener or JavaDelegate", e);
} Prevention
- Run a startup validation pass that resolves all delegateExpression values in deployed definitions and asserts their types.
- Never reuse a TaskListener bean in an execution-listener slot; name beans by role (e.g. 'orderExecutionListener').
- After an Activiti migration, grep for org.activiti imports in delegate classes.
- Add tests that execute each listener path with a mocked DelegateExecution.
When it happens
Trigger: An execution listener is declared with flowable:delegateExpression='${someBean}' on a process/flow element; the expression resolves to a bean that implements neither org.flowable.engine.delegate.ExecutionListener nor org.flowable.engine.delegate.JavaDelegate.
Common situations: Bean implements the old Activiti ExecutionListener interface after a migration; bean implements TaskListener instead of ExecutionListener (wrong listener type on element); expression typo resolving to an unrelated bean; expression calling a method that returns a domain object rather than a listener.
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
- Custom properties resolver delegate expression " + expressio
- 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/26da40c92fd2641a.
Report an issue: GitHub.