{"record":{"id":"5e43414cdde914dc","repo":"apache/beam","slug":"explicit-output-identify-object-value-of-the-chain-transform","errorCode":null,"errorMessage":"Explicit output {identify_object(value)} of the chain transform is not an output of the last transform.","messagePattern":"Explicit output (.+?) of the chain transform is not an output of the last transform\\.","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"sdks/python/apache_beam/yaml/yaml_transform.py","lineNumber":964,"sourceCode":"        transform['input'] = composite_spec['input']\n      elif is_empty(composite_spec['input']):\n        del composite_spec['input']\n      else:\n        transform['input'] = {\n            key: key\n            for key in composite_spec['input'].keys()\n        }\n    else:\n      transform['input'] = new_transforms[-1]['__uuid__']\n    new_transforms.append(transform)\n  new_transforms.extend(spec.get('extra_transforms', []))\n  composite_spec['transforms'] = new_transforms\n\n  last_transform = new_transforms[-1]['__uuid__']\n  if has_explicit_outputs:\n    for (key, value) in composite_spec['output'].items():\n      if is_not_output_of_last_transform(new_transforms, value):\n        raise ValueError(\n            f\"Explicit output {identify_object(value)} of the chain transform\"\n            f\" is not an output of the last transform.\")\n\n    composite_spec['output'] = {\n        key: f'{last_transform}.{value}'\n        for (key, value) in composite_spec['output'].items()\n    }\n  else:\n    composite_spec['output'] = {'__implicit_outputs__': last_transform}\n  if 'name' not in composite_spec:\n    composite_spec['name'] = 'Chain'\n  composite_spec['type'] = 'composite'\n  return composite_spec\n\n\ndef preprocess_chain(spec):\n  if spec['type'] == 'chain':\n    return chain_as_composite(spec)","sourceCodeStart":946,"sourceCodeEnd":982,"githubUrl":"https://github.com/apache/beam/blob/12126d8942aaf848030c478b4c6a28c6af861c66/sdks/python/apache_beam/yaml/yaml_transform.py#L946-L982","documentation":"In Beam YAML pipelines, a 'chain' transform is expanded into a composite whose external output must be produced by the last transform in the chain. If the spec has an explicit 'output' mapping that references a value not produced by the last transform, chain_as_composite rejects it so the pipeline cannot silently drop or misroute outputs.","triggerScenarios":"Calling chain_as_composite (directly or via ExpandChainTransform/preprocessing) on a spec where composite_spec['output'] maps a key to a value for which is_not_output_of_last_transform(new_transforms, value) is True — e.g. output refers to an intermediate transform's output or a nonexistent output name instead of the final transform's output.","commonSituations":"Hand-written YAML where the top-level output key points at an earlier step in a chain (e.g. ReadToWrite chain exposing 'Read' output); renaming the last transform but forgetting to update output references; copy-pasting output blocks between chains.","solutions":["Update the explicit output mapping so each value names an output of the last transform in the chain (or just the transform's default output name).","If you need an intermediate result, split the chain or move that transform to be last.","Remove the explicit 'output' section to let the chain default to the last transform's output."],"exampleFix":"# before\n- type: chain\n  transforms:\n    - type: ReadFromText\n      name: Read\n    - type: WriteToJson\n      name: Write\n  output: {result: Read}\n# after\n- type: chain\n  transforms:\n    - type: ReadFromText\n      name: Read\n    - type: WriteToJson\n      name: Write\n  output: {result: Write}","handlingStrategy":"validation","validationCode":"def check_chain_outputs(spec):\n    transforms = spec.get('transforms', [])\n    if not transforms:\n        return\n    last = transforms[-1]\n    for key, value in (spec.get('output') or {}).items():\n        owner = value.split('.')[0] if '.' in value else last.get('name', last['type'])\n        if owner != last.get('name', last['type']):\n            raise ValueError(f\"output {key}->{value} not produced by last transform\")","typeGuard":"def output_belongs_to_last(spec):\n    last_name = spec['transforms'][-1].get('name', spec['transforms'][-1]['type'])\n    return all(v.split('.')[0] == last_name for v in spec.get('output', {}).values())","tryCatchPattern":"try:\n    spec = chain_as_composite(spec)\nexcept ValueError as e:\n    if 'not an output of the last transform' in str(e):\n        spec['output'] = {k: v.split('.')[-1] for k, v in spec['output'].items()}\n    else:\n        raise","preventionTips":["Keep explicit chain outputs pointing only at the final transform","Drop the output key entirely to use the chain default","Re-verify output names after renaming chain steps"],"tags":["python","apache-beam","yaml","pipeline-validation"],"backgroundTag":"invalid-config-value","analyzedSha":"12126d8942aaf848030c478b4c6a28c6af861c66","analyzedAt":"2026-09-13T01:50:10.254Z","contentChangedAt":"2026-09-13T01:50:10.254Z","schemaVersion":2},"datasetVersion":"2026-09-20T03:17:13.778Z"}