apache/seatunnel · error · ConfigCheckException

At least one sink plugin must be configured.

Error message

At least one sink plugin must be configured.

What it means

validateSinks requires the job config to declare at least one sink plugin; an empty sink list throws ConfigCheckException('At least one sink plugin must be configured.'). Every SeaTunnel job must write data somewhere.

Solutions

  1. Add a sink { ... } block (e.g. Console {}) to the job config
  2. Ensure 'sink' is a list-shaped block containing a valid plugin name
  3. Confirm the sink plugin name is spelled correctly so it parses as a plugin config

Example fix

// before
env {...}
source { Fake {} }
// after
env {...}
source { Fake {} }
sink { Console {} }
Defensive patterns

Strategy: validation

Validate before calling

boolean hasSink = config.hasPath("sink") && !config.getConfigList("sink").isEmpty(); if (!hasSink) throw new IllegalArgumentException("config must define a sink block");

Try / catch

try { ConfigValidationUtils.validate(config); } catch (ConfigCheckException e) { log.error("Job config invalid: {}", e.getMessage()); }

Prevention

When it happens

Trigger: Job config has no 'sink' block, an empty sink list, or sink entries not recognized as configs by getConfigList (e.g. sink written as object instead of list).

Common situations: Testing source-only configs, deleting the sink section while trimming examples, or a typo turning the sink block into something TypesafeConfigUtils ignores.

Understand the failure class

Background: "is required", "must be set", "missing required field": configuration validation errors across open-source libraries — this error's family across 36 libraries.

Related errors


AI-assisted analysis of apache/seatunnel@cf67b549a7 (2026-09-10). Data as JSON: /api/errors/4ac8fc39558a7bc1. Report an issue: GitHub.

Appendix: source

Thrown at seatunnel-core/seatunnel-core-starter/src/main/java/org/apache/seatunnel/core/starter/utils/ConfigValidationUtils.java:336

                    .validate(factory.optionRule());
            SeaTunnelTransform<?> transform = factory.createTransform(context).createTransform();
            transform.setJobContext(jobContext);

            String pluginOutputIdentifier =
                    ReadonlyConfig.fromConfig(transformConfig).get(PLUGIN_OUTPUT);
            outputTables.put(
                    pluginOutputIdentifier,
                    new TableInfo(transform.getProducedCatalogTables(), pluginOutputIdentifier));
        }
        return new ArrayList<>(outputTables.values());
    }

    private static void validateSinks(
            List<TableInfo> upstreamTables,
            List<? extends Config> sinkConfigs,
            JobContext jobContext) {
        if (sinkConfigs.isEmpty()) {
            throw new ConfigCheckException("At least one sink plugin must be configured.");
        }

        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        SeaTunnelFactoryDiscovery factoryDiscovery =
                new SeaTunnelFactoryDiscovery(TableSinkFactory.class, ADD_URL_TO_CLASSLOADER);
        SeaTunnelSinkPluginDiscovery sinkPluginDiscovery =
                new SeaTunnelSinkPluginDiscovery(ADD_URL_TO_CLASSLOADER);
        Function<PluginIdentifier, SeaTunnelSink> fallbackCreateSink =
                sinkPluginDiscovery::createPluginInstance;

        TableInfo defaultInput = upstreamTables.get(upstreamTables.size() - 1);
        for (Config sinkConfig : sinkConfigs) {
            PluginIdentifier pluginIdentifier = getPluginIdentifier(sinkConfig, PluginType.SINK);
            TableSinkFactory sinkFactory =
                    (TableSinkFactory)
                            factoryDiscovery
                                    .createOptionalPluginInstance(pluginIdentifier)
                                    .orElse(null);

View on GitHub (pinned to cf67b549a7)