flowable/flowable-engine · error · FlowableException

Channel model in tenant has retry configuration but no…

Error message

Channel model <key> in tenant <tenantId> has retry configuration but no topics have been provided for it

What it means

KafkaChannelDefinitionProcessor.createEndpointConfigurations() builds retry-topic configurations for a Kafka channel. Retry requires at least one destination topic; if the channel has retry configuration but its resolved topic list is empty, it throws this FlowableException. It prevents silently creating a retry setup with nothing to publish retries to.

Solutions

  1. Add the required topic(s) to the channel model's topics configuration.
  2. If retry is not intended, remove the retry configuration from the channel model.
  3. Check the retry topic configuration (retryTopicConfiguration) specifies destination topic properties with non-empty topic names.
  4. Validate channel model definitions at deploy time with a pre-check that topics is non-empty when retry is enabled.

Example fix

// before: retry configured, topics missing
<channel key="orders" type="kafka">
  <retry delays="1000,5000"/>
</channel>

// after
<channel key="orders" type="kafka" topics="orders">
  <retry delays="1000,5000"/>
</channel>
Defensive patterns

Strategy: validation

Validate before calling

if (channelModel.isRetryEnabled() && (channelModel.getTopics() == null || channelModel.getTopics().isEmpty())) {
    throw new IllegalArgumentException("Channel " + channelModel.getKey() + " has retry config but no topics");
}

Try / catch

try {
    configurations(channelModel, tenantId);
} catch (FlowableException e) {
    log.error("Invalid Kafka channel definition: {}", e.getMessage());
}

Prevention

When it happens

Trigger: Registering/deploying a Kafka channel whose channelModel has retry configuration enabled while getTopics() (main endpoint topics or retry-specified topics) returns an empty collection.

Common situations: Channel model XML/JSON defines retry delays but omits the 'topics' attribute; topics list cleared by a miswritten template; channel converted to retry mode without updating topic configuration.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


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

Appendix: source

Thrown at modules/flowable-event-registry-spring/src/main/java/org/flowable/eventregistry/spring/kafka/KafkaChannelDefinitionProcessor.java:251

        if (retryTopicConfiguration != null) {

            Collection<String> topics;
            if (mainEndpoint.getTopics().isEmpty()) {
                TopicPartitionOffset[] topicPartitionsToAssign = mainEndpoint.getTopicPartitionsToAssign();
                if (topicPartitionsToAssign != null && topicPartitionsToAssign.length > 0) {
                    topics = Arrays.stream(topicPartitionsToAssign)
                            .map(TopicPartitionOffset::getTopic)
                            .collect(Collectors.toCollection(LinkedHashSet::new));
                } else {
                    topics = Collections.emptyList();
                }
            } else {
                topics = mainEndpoint.getTopics();
            }

            if (topics.isEmpty()) {
                throw new FlowableException("Channel model " + channelModel.getKey() + " in tenant " + tenantId
                        + " has retry configuration but no topics have been provided for it");
            }

            Collection<KafkaChannelDefinitionProcessor.Configuration> configurations = new ArrayList<>(
                    retryTopicConfiguration.getDestinationTopicProperties().size());

            DefaultDestinationTopicResolver topicResolver = new DefaultDestinationTopicResolver(Clock.systemUTC());
            topicResolver.setApplicationContext(applicationContext);
            DefaultDestinationTopicProcessor processor = new DefaultDestinationTopicProcessor(topicResolver);
            ListenerContainerFactoryConfigurer factoryConfigurer = createListenerContainerFactoryConfigurer(retryConfiguration, backOff, topicResolver);

            String id = mainEndpoint.getId();
            if (id == null) {
                id = "no.id.provided";
            }
            DestinationTopicProcessor.Context context = new DestinationTopicProcessor.Context(id, retryTopicConfiguration.getDestinationTopicProperties());
            processor.processDestinationTopicProperties(destinationTopicProperties -> {
                Suffixer suffixer = new Suffixer(destinationTopicProperties.suffix());

View on GitHub (pinned to d6d39ce1c6)