```python:inference/core/workflows/execution_engine/v1/introspection/outputs_discovery.py
from typing import Dict, List, Union

from inference.core.workflows.errors import WorkflowDefinitionError
from inference.core.workflows.execution_engine.introspection.entities import \
    BlocksDescription


def map_step_name_to_block_type(workflow_steps: List[dict]) -> Dict[str, str]:
    result = {}
    for step in workflow_steps:
        if "name" not in step or "type" not in step:
            raise WorkflowDefinitionError(
                public_message="Workflow definition invalid - step without `name` or `type` defined found.",
                context="describing_workflow_outputs",
            )
        result[step["name"]] = step["type"]
    return result


def get_blocks_output_property_kinds(
    blocks_description: BlocksDescription,
) -> Dict[str, Dict[str, List[str]]]:
    block_output_map = {}
    for block in blocks_description.blocks:
        key = block.manifest_type_identifier
        output_property_kinds = get_output_property_kinds(block.outputs_manifest)
        block_output_map[key] = output_property_kinds
        for alias in block.manifest_type_identifier_aliases:
            block_output_map[alias] = output_property_kinds
    return block_output_map


def determine_workflow_outputs_kinds(
    outputs_definitions: List[dict],
    step_name_to_block_type: Dict[str, str],
    block_output_map: Dict[str, Dict[str, List[str]]],
) -> Dict[str, Union[List[str], Dict[str, List[str]]]]:
    workflow_response_definition = {}
    for output in outputs_definitions:
        output_name = output["name"]
        selector = output["selector"]
        step_name, selected_property = extract_step_name_and_selected_property(
            selector=selector,
        )
        if step_name not in step_name_to_block_type:
            raise WorkflowDefinitionError(
                public_message=f"Could not find step referred in outputs (`{step_name}`) within Workflow steps.",
                context="describing_workflow_outputs",
            )
        step_type = step_name_to_block_type[step_name]
        output_properties = block_output_map[step_type]
        if selected_property == "*":
            property_kind = output_properties
        else:
            if selected_property not in output_properties:
                raise WorkflowDefinitionError(
                    public_message=f"Step `{step_name}` does not declare {selected_property} in its outputs.",
                    context="describing_workflow_outputs",
                )
            property_kind = output_properties[selected_property]
        workflow_response_definition[output_name] = property_kind
    return workflow_response_definition

from typing import Tuple

from inference.core.workflows.execution_engine.introspection.blocks_loader import \
    describe_available_blocks
from inference.core.workflows.execution_engine.v1.core import \
    EXECUTION_ENGINE_V1_VERSION
from inference.core.workflows.execution_engine.v1.dynamic_blocks.block_assembler import \
    compile_dynamic_blocks

# Simple module-level cache to avoid re-describing available blocks repeatedly.
# Keyed by (execution_engine_version_str, repr(dynamic_blocks_definitions))
# Repr is used to avoid attempting to JSON-serialize potentially arbitrary objects;
# it preserves behavior and avoids raising different exception types.
_BLOCK_OUTPUT_MAP_CACHE: Dict[Tuple[str, str], Dict[str, Dict[str, List[str]]]] = {}



def describe_workflow_outputs(
    definition: dict,
) -> Dict[str, Union[List[str], Dict[str, List[str]]]]:
    dynamic_defs = definition.get("dynamic_blocks_definitions", [])
    cache_key = (str(EXECUTION_ENGINE_V1_VERSION), repr(dynamic_defs))

    block_output_map = _BLOCK_OUTPUT_MAP_CACHE.get(cache_key)
    if block_output_map is None:
        # Compile dynamic blocks only as needed and describe available blocks once per unique key.
        dynamic_blocks = compile_dynamic_blocks(
            dynamic_blocks_definitions=dynamic_defs,
            skip_class_eval=True,
        )
        blocks_description = describe_available_blocks(
            dynamic_blocks=dynamic_blocks,
            execution_engine_version=EXECUTION_ENGINE_V1_VERSION,
        )
        block_output_map = get_blocks_output_property_kinds(
            blocks_description=blocks_description
        )
        # Cache the computed mapping for subsequent calls with same key.
        _BLOCK_OUTPUT_MAP_CACHE[cache_key] = block_output_map

    try:
        step_name_to_block_type = map_step_name_to_block_type(
            workflow_steps=definition["steps"]
        )
        return determine_workflow_outputs_kinds(
            outputs_definitions=definition["outputs"],
            step_name_to_block_type=step_name_to_block_type,
            block_output_map=block_output_map,
        )
    except KeyError as error:
        raise WorkflowDefinitionError(
            public_message=f"Workflow definition invalid - missing property `{error}`.",
            inner_error=error,
            context="describing_workflow_outputs",
        )
```