affaan-m/ECC · error · ValueError

Unknown output node

Error message

Unknown output node

What it means

The workflow's output section must reference existing node names. validate_graph iterates references_in(contents['output']) and raises ValueError('Unknown output node') if any referenced first element is not a key of nodes.

Solutions

  1. Update the output reference to match an existing node name in nodes
  2. Rename the node itself to the name used in output if that was the intent
  3. Diff the workflow JSON against a working sibling (e.g. taste-apply.json) to spot the stale name
  4. Run load_graph again after the fix; any remaining mismatch will surface on the next validation pass

Example fix

// before
{"output": {"result": "{{apply_result.video}}"}}  // no node 'apply_result'
// after
{"output": {"result": "{{apply.video}}"}}
Defensive patterns

Strategy: validation

Validate before calling

nodes = set(graph['nodes'])
for ref in re.findall(r"\{\{(\w+)\.", json.dumps(graph.get('output', {}))):
    assert ref in nodes, f'unknown output node: {ref}'

Try / catch

try:
    graph = load_graph(kind)
except ValueError as e:
    if str(e) == 'Unknown output node':
        # fix the output section to point at an existing node
        ...

Prevention

When it happens

Trigger: contents['output'] contains e.g. ['apply_final','...'] but the nodes map has 'apply' or 'apply-motion' — a stale or misspelled node reference in the workflow's output declaration.

Common situations: Renaming the final node without updating the output section; editing the output by hand in the taste-<kind>.json workflow file; switching a workflow kind and reusing another workflow's output block.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/18ea1b4f0b02b6bc. Report an issue: GitHub.

Appendix: source

Thrown at skills/taste-application/scripts/workflow_graphs.py:107

        if name in stack:
            raise ValueError('Dependency cycle')
        if name not in ancestors:
            deps = nodes[name].get('depends', [])
            ancestors[name] = set(deps).union(*(visit(dep, stack | {name}) for dep in deps))
        return ancestors[name]

    for name, node in nodes.items():
        reachable = visit(name, set())
        for ref in references_in({'input': node.get('input'), 'fields': node.get('fields')}):
            if ref[0] not in reachable:
                raise ValueError(f'{name} references undeclared dependency: {ref[0]}')
            if ref[0] == 'input':
                if len(ref) < 2 or ref[1] not in inputs:
                    raise ValueError('Unknown workflow input')
                used.add(ref[1])
    for ref in references_in(contents.get('output', {})):
        if ref[0] not in nodes:
            raise ValueError('Unknown output node')
    if used != set(inputs):
        raise ValueError(f'Unwired workflow inputs: {sorted(set(inputs) - used)}')


def load_graph(kind):
    if kind not in ('apply', 'apply-motion', 'distill', 'prop3d'):
        raise ValueError('Unknown workflow kind')
    graph = json.loads((WORKFLOWS / f'taste-{kind}.json').read_text())
    validate_graph(graph)
    return graph


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--kind', choices=('apply', 'apply-bundle', 'distill'), required=True)
    parser.add_argument('--config', type=Path, required=True)
    parser.add_argument('--out', type=Path, required=True)
    args = parser.parse_args()

View on GitHub (pinned to 8321021c54)