apache/beam · error · IllegalArgumentException

Output has tags but expected output tags

Error message

Output has tags ${actualOutputTags} but expected output tags ${outputTags}

What it means

After expand(), YamlTransform compares the ids of the output TupleTags actually produced by the external transform against the output tags declared in the spec. A set mismatch (missing, extra, or renamed outputs) throws IllegalArgumentException listing both sets joined by commas.

Solutions

  1. Update the declared outputTags in the YAML/Java spec to exactly match the tags the transform emits.
  2. Change the external transform to emit exactly the declared set of output tags.
  3. Diff the two tag sets printed in the message to find extra/missing/renamed tags.
  4. Pin the external transform version so its output contract matches the spec.

Example fix

// before
outputs: ["main"]
// after (transform also emits "side")
outputs: ["main", "side"]
Defensive patterns

Strategy: validation

Validate before calling

Set<String> declared = new HashSet<>(yamlSpecOutputs);
Set<String> actual = result.expand().keySet().stream().map(TupleTag::getId).collect(Collectors.toSet());
if (!declared.equals(actual)) throw new IllegalStateException("Output tag mismatch: " + actual + " vs " + declared);

Type guard

boolean hasExpectedOutputTags(Object out, Set<String> expected) {
  return !(out instanceof PCollection) && out.expand().keySet().stream().map(TupleTag::getId).collect(Collectors.toSet()).equals(expected);
}

Try / catch

try {
  return expand(input);
} catch (IllegalArgumentException e) {
  if (e.getMessage().contains("Output has tags")) { /* sync spec with transform */ }
  throw e;
}

Prevention

When it happens

Trigger: Running a YamlTransform whose underlying external transform's expanded output map keys differ from the declared outputTags — e.g. external transform emits tags {main, side} while spec declares only {main}.

Common situations: Upgrading a Python external transform that added a new output while the YAML spec stayed unchanged; typos in declared output tag names; auto-generated TupleTag ids not matching spec.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of apache/beam@12126d8942 (2026-09-13). Data as JSON: /api/errors/3ec7557d8ad5a38f. Report an issue: GitHub.

Appendix: source

Thrown at sdks/java/extensions/yaml/src/main/java/org/apache/beam/sdk/extensions/yaml/YamlTransform.java:197

      }
      return (OutputT) output;
    } else {
      if (output instanceof PCollection) {
        // ExternalPythonTransform always returns single outputs as PCollections.
        if (outputTags.size() != 1) {
          throw new IllegalArgumentException(
              "Expected " + outputTags.size() + " outputs, but got exactly one.");
        }
        return (OutputT)
            PCollectionRowTuple.of(outputTags.iterator().next(), (PCollection<Row>) output);
      } else {
        Map<TupleTag<?>, PValue> expandedOutputs = output.expand();
        Set<String> actualOutputTags =
            expandedOutputs.keySet().stream()
                .map(TupleTag::getId)
                .collect(Collectors.toCollection(HashSet::new));
        if (!outputTags.equals(actualOutputTags)) {
          throw new IllegalArgumentException(
              "Output has tags "
                  + Joiner.on(", ").join(actualOutputTags)
                  + " but expected output tags "
                  + Joiner.on(", ").join(outputTags));
        }
        PCollectionRowTuple result = PCollectionRowTuple.empty(input.getPipeline());
        for (Map.Entry<TupleTag<?>, PValue> subOutput : expandedOutputs.entrySet()) {
          result = result.and(subOutput.getKey().getId(), (PCollection<Row>) subOutput.getValue());
        }
        return (OutputT) result;
      }
    }
  }
}

View on GitHub (pinned to 12126d8942)