Reactor Docs
Reactor Docs
  • CPE Comprehensive Developer Guide
  • Experience Cloud CPE Specification
  • Getting Started
  • Obsidian Syntax Test
  • Login

    CPE Comprehensive Developer Guide

    Custom Property Editor guide for Salesforce LWC Configuration Editors

    By Marc Swan

    #Custom Property Editor (CPE) Comprehensive Developer Guide

    #Salesforce LWC Configuration Editors for Flow Builder & Experience Cloud

    Version: 2.0 Author: Marc Swan (Salesforce Architect) Last Updated: December 2025
    Minimum API Version: 61.0+


    #Table of Contents

    Part I: Foundation

    1. Introduction & When to Use CPEs
    2. Platform Comparison: Flow vs Experience Cloud
    3. Project Scaffolding & File Structure

    Part II: Flow Builder CPEs

    1. Flow CPE Core Concepts
    2. Flow CPE JavaScript Interfaces
    3. Flow CPE Implementation Guide
    4. Generic sObject Types
    5. Flow Variable Selection

    Part III: Experience Cloud CPEs

    1. Experience Cloud CPE Core Concepts
    2. Experience Cloud CPE Implementation Guide
    3. Custom Lightning Types (ExperiencePropertyTypeBundle)

    Part IV: Advanced Topics

    1. Event Flow & Communication
    2. Lifecycle & Initialization
    3. Validation Patterns
    4. Complex Data Handling
    5. Design Patterns
    6. Performance Optimization

    Part V: Reference

    1. Troubleshooting Guide
    2. Best Practices Checklist
    3. Quick Reference Cards
    4. Complete Code Examples

    #Part I: Foundation

    #1. Introduction & When to Use CPEs

    #What is a Custom Property Editor?

    A Custom Property Editor (CPE) is a Lightning Web Component that replaces the default property panel input with a custom UI. CPEs enable:

    • Complex configuration beyond simple input fields
    • Dynamic options based on org metadata
    • Visual pickers (alignment, color, icons)
    • Conditional configuration based on other settings
    • Integration with Flow variables and resources
    • Custom validation logic
    • Real-time feedback and preview

    #Two Distinct CPE Platforms

    Salesforce supports CPEs in two different contexts with completely different APIs:

    PlatformUse CaseBuilder Interface
    Flow BuilderFlow Screen Components, Invocable ActionsFlow Builder
    Experience CloudSite ComponentsExperience Builder

    ⚠️ CRITICAL: These are NOT interchangeable. Code written for one platform will NOT work on the other.

    #When to Create a CPE

    #Create a CPE When You Need:

    RequirementFlow CPEExperience Cloud CPE
    Complex nested configurations✓✓
    Dynamic options from org metadata✓✓
    Conditional property visibility✓✓
    Visual pickers (color, alignment)✓✓
    Flow variable integration✓✗
    Generic sObject type selection✓✗
    Record type filtering✓✓
    Custom validation logic✓✓
    Grouped/tabbed properties✓✓

    #Use Standard Properties When:

    • Simple string/boolean/integer inputs suffice
    • Static picklists cover all options
    • Standard datasource binding works
    • No cross-field dependencies exist

    #2. Platform Comparison: Flow vs Experience Cloud

    #Side-by-Side API Comparison

    ┌─────────────────────────────────────────────────────────────────────────────────────┐
    │                              FLOW BUILDER CPE                                        │
    ├─────────────────────────────────────────────────────────────────────────────────────┤
    │ Registration:     configurationEditor="c-my-property-editor" (kebab-case)           │
    │ Where Declared:   <targetConfig> attribute                                           │
    │ Value Access:     @api inputVariables (array of {name, value, valueDataType})       │
    │ Update Event:     'configuration_editor_input_value_changed'                         │
    │ Event Detail:     { name, newValue, newValueDataType }                               │
    │ Validation:       @api validate() method (you implement, return errors)              │
    │ Context:          @api builderContext, @api genericTypeMappings                      │
    │ Output Variables: @api automaticOutputVariables                                      │
    │ Generic Types:    'configuration_editor_generic_type_mapping_changed' event          │
    │ CPE isExposed:    false                                                              │
    └─────────────────────────────────────────────────────────────────────────────────────┘
    
    ┌─────────────────────────────────────────────────────────────────────────────────────┐
    │                           EXPERIENCE CLOUD CPE                                       │
    ├─────────────────────────────────────────────────────────────────────────────────────┤
    │ Registration:     editor="c/myPropertyEditor" (camelCase with /)                    │
    │ Where Declared:   <property> attribute                                               │
    │ Value Access:     @api value (direct value)                                          │
    │ Update Event:     'valuechange'                                                      │
    │ Event Detail:     { value }                                                          │
    │ Validation:       @api errors (received FROM framework, display only)                │
    │ Context:          @api schema, @api label, @api description, @api required           │
    │ Output Variables: N/A                                                                │
    │ Generic Types:    N/A (use Custom Lightning Types instead)                           │
    │ CPE isExposed:    false                                                              │
    └─────────────────────────────────────────────────────────────────────────────────────┘
    

    #Declaration Syntax Comparison

    #Flow Builder

    <!-- Main component meta.xml -->
    <targetConfig targets="lightning__FlowScreen" 
                  configurationEditor="c-my-flow-cpe">
        <!--              ^^^^^^^^^^^^^^^^^^^^^^^ kebab-case, on targetConfig -->
        <property name="objectApiName" type="String" label="Object API Name"/>
        <property name="fieldApiName" type="String" label="Field API Name"/>
    </targetConfig>
    

    #Experience Cloud

    <!-- Main component meta.xml -->
    <targetConfig targets="lightningCommunity__Default">
        <property name="alignment" 
                  type="String" 
                  label="Text Alignment"
                  editor="c/alignmentPropertyEditor"/>
        <!--      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ camelCase with /, on property -->
    </targetConfig>
    

    #Quick Decision Matrix

    Which CPE type do you need?
    
    Is your component for...
    │
    ├─► Flow Builder (lightning__FlowScreen)?
    │   └─► Use FLOW CPE pattern
    │       • configurationEditor="c-component-name"
    │       • @api inputVariables
    │       • 'configuration_editor_input_value_changed'
    │
    ├─► Flow Invocable Actions (@InvocableMethod)?
    │   └─► Use FLOW CPE pattern
    │       • Same as above
    │
    ├─► Experience Cloud Sites?
    │   └─► Use EXPERIENCE CLOUD CPE pattern
    │       • editor="c/componentName"
    │       • @api value
    │       • 'valuechange'
    │
    └─► Lightning App Builder (Record Pages, Home, App)?
        └─► CPEs NOT SUPPORTED
            • Use standard properties only
            • Or use designTimeOnly pattern
    

    #3. Project Scaffolding & File Structure

    #Complete Project Structure

    force-app/main/default/
    ├── lwc/
    │   │
    │   │── ════════════════════════════════════════════════
    │   │   MAIN RUNTIME COMPONENTS
    │   │── ════════════════════════════════════════════════
    │   │
    │   ├── myFlowComponent/                    # Flow Screen Component
    │   │   ├── myFlowComponent.js
    │   │   ├── myFlowComponent.html
    │   │   ├── myFlowComponent.css
    │   │   └── myFlowComponent.js-meta.xml
    │   │
    │   ├── myExperienceComponent/              # Experience Cloud Component
    │   │   ├── myExperienceComponent.js
    │   │   ├── myExperienceComponent.html
    │   │   ├── myExperienceComponent.css
    │   │   └── myExperienceComponent.js-meta.xml
    │   │
    │   │── ════════════════════════════════════════════════
    │   │   CUSTOM PROPERTY EDITORS (CPEs)
    │   │── ════════════════════════════════════════════════
    │   │
    │   ├── myFlowComponentCPE/                 # Flow CPE (kebab: c-my-flow-component-c-p-e)
    │   │   ├── myFlowComponentCPE.js
    │   │   ├── myFlowComponentCPE.html
    │   │   ├── myFlowComponentCPE.css
    │   │   └── myFlowComponentCPE.js-meta.xml
    │   │
    │   ├── alignmentPropertyEditor/            # Experience CPE (ref: c/alignmentPropertyEditor)
    │   │   ├── alignmentPropertyEditor.js
    │   │   ├── alignmentPropertyEditor.html
    │   │   ├── alignmentPropertyEditor.css
    │   │   └── alignmentPropertyEditor.js-meta.xml
    │   │
    │   │── ════════════════════════════════════════════════
    │   │   SHARED/UTILITY COMPONENTS
    │   │── ════════════════════════════════════════════════
    │   │
    │   ├── cpeBase/                            # Optional base class for CPEs
    │   │   └── cpeBase.js
    │   │
    │   └── flowCombobox/                       # Flow variable selector (optional)
    │       ├── flowCombobox.js
    │       ├── flowCombobox.html
    │       └── flowCombobox.js-meta.xml
    │
    ├── classes/
    │   ├── MyComponentController.cls           # Apex support for CPE
    │   └── MyComponentControllerTest.cls
    │
    └── experiencePropertyTypeBundles/          # Custom Lightning Types (Experience Cloud only)
        └── cardStyleType/
            ├── schema.json
            └── design.json
    

    #Scaffolding Templates

    #Flow Component Meta.xml Template

    <?xml version="1.0" encoding="UTF-8"?>
    <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
        <apiVersion>61.0</apiVersion>
        <isExposed>true</isExposed>
        <masterLabel>My Flow Component</masterLabel>
        <description>Description for Flow Builder</description>
        
        <targets>
            <target>lightning__FlowScreen</target>
        </targets>
        
        <targetConfigs>
            <targetConfig targets="lightning__FlowScreen"
                          configurationEditor="c-my-flow-component-c-p-e">
                
                <!-- Input Properties -->
                <property name="objectApiName" 
                          type="String" 
                          label="Object API Name"
                          description="The API name of the object"
                          role="inputOnly"/>
                
                <property name="fieldApiName" 
                          type="String" 
                          label="Field API Name"
                          role="inputOnly"/>
                
                <property name="isRequired" 
                          type="Boolean" 
                          label="Required"
                          default="false"
                          role="inputOnly"/>
                
                <!-- Input/Output Properties -->
                <property name="selectedValue" 
                          type="String" 
                          label="Selected Value"
                          role="inputOutput"/>
                
                <!-- Collection Properties -->
                <property name="selectedValues" 
                          type="String[]" 
                          label="Selected Values"
                          role="inputOutput"/>
                
                <!-- Generic sObject Support -->
                <property name="recordId" 
                          type="{T}" 
                          label="Record"/>
                <propertyType name="T" 
                              extends="SObject" 
                              label="Object Type" 
                              description="Select the object type"/>
                
            </targetConfig>
        </targetConfigs>
    </LightningComponentBundle>
    

    #Flow CPE Meta.xml Template

    <?xml version="1.0" encoding="UTF-8"?>
    <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
        <apiVersion>61.0</apiVersion>
        <isExposed>false</isExposed>
        <!-- 
            ⚠️ CRITICAL: CPEs must have isExposed=false
            ⚠️ NO targets needed - CPE is invoked by framework
        -->
    </LightningComponentBundle>
    

    #Experience Cloud Component Meta.xml Template

    <?xml version="1.0" encoding="UTF-8"?>
    <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
        <apiVersion>61.0</apiVersion>
        <isExposed>true</isExposed>
        <masterLabel>My Experience Component</masterLabel>
        <description>Description for Experience Builder</description>
        
        <targets>
            <target>lightningCommunity__Page</target>
            <target>lightningCommunity__Default</target>
        </targets>
        
        <targetConfigs>
            <targetConfig targets="lightningCommunity__Default">
                
                <!-- Standard Property (no CPE) -->
                <property name="title" 
                          type="String" 
                          label="Title"
                          default="My Component"/>
                
                <!-- Property with Custom Editor -->
                <property name="alignment" 
                          type="String" 
                          label="Text Alignment"
                          default="left"
                          editor="c/alignmentPropertyEditor"/>
                
                <!-- Property with Custom Lightning Type -->
                <property name="cardStyle" 
                          type="cardStyleType" 
                          label="Card Styling"/>
                
                <!-- Property with Datasource -->
                <property name="recordId" 
                          type="String" 
                          label="Record ID"
                          datasource="@salesforce/Record"/>
                
            </targetConfig>
        </targetConfigs>
    </LightningComponentBundle>
    

    #Experience Cloud CPE Meta.xml Template

    <?xml version="1.0" encoding="UTF-8"?>
    <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
        <apiVersion>61.0</apiVersion>
        <isExposed>false</isExposed>
        <!-- 
            ⚠️ CRITICAL: CPEs must have isExposed=false
            ⚠️ NO targets needed - CPE is invoked by framework
        -->
    </LightningComponentBundle>
    

    #SFDX Project Creation Commands

    # Create new SFDX project
    sf project generate --name my-cpe-project --template standard
    
    # Create Flow component + CPE
    sf lightning generate component --name myFlowComponent --type lwc --output-dir force-app/main/default/lwc
    sf lightning generate component --name myFlowComponentCPE --type lwc --output-dir force-app/main/default/lwc
    
    # Create Experience Cloud component + CPE  
    sf lightning generate component --name myExperienceComponent --type lwc --output-dir force-app/main/default/lwc
    sf lightning generate component --name alignmentPropertyEditor --type lwc --output-dir force-app/main/default/lwc
    
    # Create Apex controller
    sf apex generate class --name MyComponentController --output-dir force-app/main/default/classes
    
    # Create Custom Lightning Type directory (manual)
    mkdir -p force-app/main/default/experiencePropertyTypeBundles/cardStyleType
    

    #Part II: Flow Builder CPEs

    #4. Flow CPE Core Concepts

    #Component Pairing

    Every Flow screen component can have an associated CPE. The CPE is specified in the main component's meta.xml:

    <targetConfig targets="lightning__FlowScreen" 
                  configurationEditor="c-my-component-c-p-e">
    

    Naming Convention: The CPE reference uses kebab-case with namespace prefix.

    Component NameCPE Reference
    myFlowComponentc-my-flow-component
    superListBoxCPEc-super-list-box-c-p-e
    accountSelectorc-account-selector

    #Data Flow Architecture

    ┌─────────────────────────────────────────────────────────────────────────────┐
    │                            FLOW BUILDER                                      │
    │  ┌────────────────────────────────────────────────────────────────────┐     │
    │  │                     Configuration Panel                            │     │
    │  │  ┌──────────────────────────────────────────────────────────────┐ │     │
    │  │  │                 Custom Property Editor                        │ │     │
    │  │  │                                                               │ │     │
    │  │  │   @api builderContext          ───────────────────────────►  │ │     │
    │  │  │   @api inputVariables          ───────────────────────────►  │ │     │
    │  │  │   @api genericTypeMappings     ───────────────────────────►  │ │     │
    │  │  │   @api automaticOutputVariables───────────────────────────►  │ │     │
    │  │  │                                                               │ │     │
    │  │  │   configuration_editor_input_value_changed  ◄─────────────── │ │     │
    │  │  │   configuration_editor_generic_type_mapping_changed ◄─────── │ │     │
    │  │  │                                                               │ │     │
    │  │  └──────────────────────────────────────────────────────────────┘ │     │
    │  └────────────────────────────────────────────────────────────────────┘     │
    │                                    │                                         │
    │                                    │ Properties                              │
    │                                    ▼                                         │
    │  ┌────────────────────────────────────────────────────────────────────┐     │
    │  │                    Main Screen Component                           │     │
    │  │                    @api objectApiName                              │     │
    │  │                    @api fieldApiName                               │     │
    │  │                    @api selectedValue                              │     │
    │  └────────────────────────────────────────────────────────────────────┘     │
    └─────────────────────────────────────────────────────────────────────────────┘
    

    #5. Flow CPE JavaScript Interfaces

    Flow Builder communicates with CPEs through four main @api properties:

    #5.1 builderContext

    Provides metadata about the flow and available resources.

    @api
    get builderContext() {
        return this._builderContext;
    }
    set builderContext(context) {
        this._builderContext = context || {};
        // Available properties:
        // context.variables        - Flow variables
        // context.elementInfos     - Information about flow elements
        // context.formulas         - Formula fields
        // context.objectInfos      - Object metadata
        // context.screens          - Screen elements
        // context.recordLookups    - Get Records elements
        // context.recordCreates    - Create Records elements
        // context.recordUpdates    - Update Records elements
        // context.recordDeletes    - Delete Records elements
    }
    

    #5.2 inputVariables

    Receives current configuration values as an array.

    @api
    get inputVariables() {
        return this._inputVariables;
    }
    set inputVariables(variables) {
        this._inputVariables = variables || [];
        // Each variable has: { name, value, valueDataType }
        this.initializeValues();
    }
    

    Structure of inputVariables:

    [
        { name: 'objectApiName', value: 'Account', valueDataType: 'String' },
        { name: 'fieldApiName', value: 'Industry', valueDataType: 'String' },
        { name: 'isRequired', value: true, valueDataType: 'Boolean' },
        { name: 'selectedValues', value: ['Val1', 'Val2'], valueDataType: 'String[]' }
    ]
    

    #5.3 genericTypeMappings

    Handles generic sObject type mappings for {T} properties.

    @api
    get genericTypeMappings() {
        return this._genericTypeMappings;
    }
    set genericTypeMappings(mappings) {
        this._genericTypeMappings = mappings || [];
        // Each mapping has: { typeName, typeValue }
        // e.g., { typeName: 'T', typeValue: 'Account' }
        this.initializeGenericType();
    }
    

    #5.4 automaticOutputVariables

    Provides access to output variables from other Flow components.

    @api
    get automaticOutputVariables() {
        return this._automaticOutputVariables;
    }
    set automaticOutputVariables(variables) {
        this._automaticOutputVariables = variables || [];
        // Each has: { name, dataType, isCollection, elementName }
        this.loadFlowVariables();
    }
    

    Important: This interface is called AFTER builderContext and inputVariables.

    #5.5 validate() Method

    Flow Builder calls this to validate the configuration.

    @api
    validate() {
        const errors = [];
        
        if (!this.selectedObject) {
            errors.push({
                key: 'OBJECT_REQUIRED',
                errorString: 'Please select an object'
            });
        }
        
        if (!this.selectedField) {
            errors.push({
                key: 'FIELD_REQUIRED', 
                errorString: 'Please select a field'
            });
        }
        
        return errors; // Empty array = valid
    }
    

    #6. Flow CPE Implementation Guide

    #Complete Flow CPE Template

    // myFlowComponentCPE.js
    import { LightningElement, api, track } from 'lwc';
    import getObjectOptions from '@salesforce/apex/MyComponentController.getObjectOptions';
    import getFieldOptions from '@salesforce/apex/MyComponentController.getFieldOptions';
    
    export default class MyFlowComponentCPE extends LightningElement {
        // ═══════════════════════════════════════════════════════════════════════
        // FLOW BUILDER INTERFACE PROPERTIES
        // ═══════════════════════════════════════════════════════════════════════
        
        _builderContext = {};
        _inputVariables = [];
        _genericTypeMappings = [];
        _automaticOutputVariables = [];
        
        // ═══════════════════════════════════════════════════════════════════════
        // UI STATE
        // ═══════════════════════════════════════════════════════════════════════
        
        @track isLoading = false;
        @track error = null;
        @track objectOptions = [];
        @track fieldOptions = [];
        @track flowVariableOptions = [];
        
        // ═══════════════════════════════════════════════════════════════════════
        // CONFIGURATION VALUES
        // ═══════════════════════════════════════════════════════════════════════
        
        selectedObject;
        selectedField;
        isRequired = false;
        initialSelectedValue;
        
        // ═══════════════════════════════════════════════════════════════════════
        // @api INTERFACE IMPLEMENTATIONS
        // ═══════════════════════════════════════════════════════════════════════
        
        @api
        get builderContext() {
            return this._builderContext;
        }
        set builderContext(context) {
            this._builderContext = context || {};
            this.loadObjectOptions();
        }
        
        @api
        get inputVariables() {
            return this._inputVariables;
        }
        set inputVariables(variables) {
            this._inputVariables = variables || [];
            this.initializeValues();
        }
        
        @api
        get genericTypeMappings() {
            return this._genericTypeMappings;
        }
        set genericTypeMappings(mappings) {
            this._genericTypeMappings = mappings || [];
            this.initializeGenericType();
        }
        
        @api
        get automaticOutputVariables() {
            return this._automaticOutputVariables;
        }
        set automaticOutputVariables(variables) {
            this._automaticOutputVariables = variables || [];
            this.loadFlowVariables();
        }
        
        // ═══════════════════════════════════════════════════════════════════════
        // VALIDATION
        // ═══════════════════════════════════════════════════════════════════════
        
        @api
        validate() {
            const errors = [];
            
            if (!this.selectedObject) {
                errors.push({
                    key: 'OBJECT_REQUIRED',
                    errorString: 'Please select an object'
                });
            }
            
            if (!this.selectedField) {
                errors.push({
                    key: 'FIELD_REQUIRED',
                    errorString: 'Please select a field'
                });
            }
            
            return errors;
        }
        
        // ═══════════════════════════════════════════════════════════════════════
        // INITIALIZATION
        // ═══════════════════════════════════════════════════════════════════════
        
        initializeValues() {
            let fieldToSet = null;
            
            if (this._inputVariables && Array.isArray(this._inputVariables)) {
                this._inputVariables.forEach(variable => {
                    switch (variable.name) {
                        case 'objectApiName':
                            this.selectedObject = variable.value;
                            break;
                        case 'fieldApiName':
                            fieldToSet = variable.value;
                            break;
                        case 'isRequired':
                            this.isRequired = variable.value === true || 
                                              variable.value === 'true';
                            break;
                        case 'initialSelectedValue':
                            // Handle Flow variable references
                            if (typeof variable.value === 'string' && 
                                variable.value.startsWith('{!')) {
                                this.initialSelectedValue = variable.value;
                            } else {
                                this.initialSelectedValue = variable.value || '';
                            }
                            break;
                    }
                });
            }
            
            // Load field options if object is set
            if (this.selectedObject && fieldToSet) {
                this.loadFieldOptions(fieldToSet);
            }
        }
        
        initializeGenericType() {
            if (this._genericTypeMappings && this._genericTypeMappings.length > 0) {
                const tMapping = this._genericTypeMappings.find(m => m.typeName === 'T');
                if (tMapping) {
                    this.selectedObject = tMapping.typeValue;
                }
            }
        }
        
        // ═══════════════════════════════════════════════════════════════════════
        // DATA LOADING
        // ═══════════════════════════════════════════════════════════════════════
        
        async loadObjectOptions() {
            this.isLoading = true;
            try {
                const options = await getObjectOptions();
                this.objectOptions = options || [];
            } catch (error) {
                console.error('Error loading objects:', error);
                this.loadFallbackObjectOptions();
            } finally {
                this.isLoading = false;
            }
        }
        
        loadFallbackObjectOptions() {
            this.objectOptions = [
                { label: 'Account', value: 'Account' },
                { label: 'Contact', value: 'Contact' },
                { label: 'Lead', value: 'Lead' },
                { label: 'Opportunity', value: 'Opportunity' },
                { label: 'Case', value: 'Case' }
            ];
        }
        
        async loadFieldOptions(fieldToSet) {
            if (!this.selectedObject) return;
            
            this.isLoading = true;
            try {
                const fields = await getFieldOptions({ 
                    objectApiName: this.selectedObject 
                });
                this.fieldOptions = fields || [];
                
                // Restore saved selection if valid
                if (fieldToSet && this.fieldOptions.some(f => f.value === fieldToSet)) {
                    this.selectedField = fieldToSet;
                }
            } catch (error) {
                console.error('Error loading fields:', error);
                this.fieldOptions = [];
            } finally {
                this.isLoading = false;
            }
        }
        
        loadFlowVariables() {
            const variables = [];
            const processedVariables = new Set();
            
            // Add none option
            variables.push({ label: '--None--', value: '' });
            
            // Process regular flow variables
            if (this._builderContext?.variables) {
                this._builderContext.variables.forEach(variable => {
                    if (variable.dataType === 'String' && !variable.isCollection) {
                        const variableRef = '{!' + variable.name + '}';
                        if (!processedVariables.has(variableRef)) {
                            variables.push({
                                label: variable.name,
                                value: variableRef
                            });
                            processedVariables.add(variableRef);
                        }
                    }
                });
            }
            
            // Process automatic output variables
            if (this._automaticOutputVariables?.length > 0) {
                this._automaticOutputVariables.forEach(autoVar => {
                    if (autoVar.dataType === 'String' && !autoVar.isCollection) {
                        const variableRef = '{!' + autoVar.name + '}';
                        if (!processedVariables.has(variableRef)) {
                            const label = autoVar.elementName 
                                ? `${autoVar.name} (from ${autoVar.elementName})`
                                : autoVar.name;
                            variables.push({
                                label: label,
                                value: variableRef
                            });
                            processedVariables.add(variableRef);
                        }
                    }
                });
            }
            
            this.flowVariableOptions = variables;
        }
        
        // ═══════════════════════════════════════════════════════════════════════
        // EVENT HANDLERS
        // ═══════════════════════════════════════════════════════════════════════
        
        handleObjectChange(event) {
            this.selectedObject = event.detail.value;
            this.selectedField = null;
            this.fieldOptions = [];
            
            // Dispatch configuration change
            this.dispatchConfigurationChange('objectApiName', this.selectedObject);
            this.dispatchConfigurationChange('fieldApiName', null);
            
            // Dispatch generic type mapping if using {T}
            this.dispatchGenericTypeChange('T', this.selectedObject);
            
            // Load fields for new object
            this.loadFieldOptions();
        }
        
        handleFieldChange(event) {
            this.selectedField = event.detail.value;
            this.dispatchConfigurationChange('fieldApiName', this.selectedField);
        }
        
        handleRequiredChange(event) {
            this.isRequired = event.target.checked;
            this.dispatchConfigurationChange('isRequired', this.isRequired);
        }
        
        handleInitialValueChange(event) {
            this.initialSelectedValue = event.detail.value;
            this.dispatchConfigurationChange('initialSelectedValue', this.initialSelectedValue);
        }
        
        // ═══════════════════════════════════════════════════════════════════════
        // EVENT DISPATCHING
        // ═══════════════════════════════════════════════════════════════════════
        
        dispatchConfigurationChange(name, value) {
            const valueChangeEvent = new CustomEvent(
                'configuration_editor_input_value_changed',
                {
                    bubbles: true,
                    cancelable: false,
                    composed: true,
                    detail: {
                        name: name,
                        newValue: value,
                        newValueDataType: this.getDataType(name)
                    }
                }
            );
            this.dispatchEvent(valueChangeEvent);
        }
        
        dispatchGenericTypeChange(typeName, typeValue) {
            const typeChangeEvent = new CustomEvent(
                'configuration_editor_generic_type_mapping_changed',
                {
                    bubbles: true,
                    cancelable: false,
                    composed: true,
                    detail: {
                        typeName: typeName,
                        typeValue: typeValue
                    }
                }
            );
            this.dispatchEvent(typeChangeEvent);
        }
        
        getDataType(name) {
            const dataTypes = {
                'objectApiName': 'String',
                'fieldApiName': 'String',
                'isRequired': 'Boolean',
                'initialSelectedValue': 'String',
                'initialSelectedValues': 'String[]',
                'selectedValues': 'String[]'
            };
            return dataTypes[name] || 'String';
        }
        
        // ═══════════════════════════════════════════════════════════════════════
        // COMPUTED PROPERTIES
        // ═══════════════════════════════════════════════════════════════════════
        
        get showFieldSelector() {
            return this.selectedObject && this.fieldOptions.length > 0;
        }
        
        get showAdvancedOptions() {
            return this.selectedObject && this.selectedField;
        }
    }
    

    #Flow CPE HTML Template

    <!-- myFlowComponentCPE.html -->
    <template>
        <div class="slds-p-around_medium">
            <!-- Loading Spinner -->
            <template if:true={isLoading}>
                <lightning-spinner alternative-text="Loading" size="small">
                </lightning-spinner>
            </template>
            
            <!-- Error Display -->
            <template if:true={error}>
                <div class="slds-text-color_error slds-m-bottom_small">
                    {error}
                </div>
            </template>
            
            <!-- Step 1: Object Selection -->
            <div class="slds-m-bottom_small">
                <lightning-combobox
                    name="objectApiName"
                    label="Select Object"
                    value={selectedObject}
                    placeholder="Select an Object..."
                    options={objectOptions}
                    onchange={handleObjectChange}
                    required>
                </lightning-combobox>
            </div>
            
            <!-- Step 2: Field Selection (Progressive Disclosure) -->
            <template if:true={showFieldSelector}>
                <div class="slds-m-bottom_small">
                    <lightning-combobox
                        name="fieldApiName"
                        label="Select Field"
                        value={selectedField}
                        placeholder="Select a Field..."
                        options={fieldOptions}
                        onchange={handleFieldChange}
                        required>
                    </lightning-combobox>
                </div>
            </template>
            
            <!-- Step 3: Advanced Options (Progressive Disclosure) -->
            <template if:true={showAdvancedOptions}>
                <div class="slds-m-bottom_small">
                    <lightning-input
                        type="checkbox"
                        name="isRequired"
                        label="Required"
                        checked={isRequired}
                        onchange={handleRequiredChange}>
                    </lightning-input>
                </div>
                
                <div class="slds-m-bottom_small">
                    <lightning-combobox
                        name="initialSelectedValue"
                        label="Initial Value (Optional)"
                        value={initialSelectedValue}
                        placeholder="Select a Flow Variable..."
                        options={flowVariableOptions}
                        onchange={handleInitialValueChange}>
                    </lightning-combobox>
                    <div class="slds-form-element__help">
                        Select a Flow variable or leave blank
                    </div>
                </div>
            </template>
        </div>
    </template>
    

    #7. Generic sObject Types

    Generic sObject types allow your Flow component to work with any Salesforce object selected at design time.

    #7.1 Declaring Generic Types

    In your main component's meta.xml:

    <targetConfig targets="lightning__FlowScreen"
                  configurationEditor="c-my-component-c-p-e">
        
        <!-- Property that accepts any sObject type -->
        <property name="recordId" 
                  type="{T}" 
                  label="Record ID"
                  description="ID of the record to process"/>
        
        <!-- Property that accepts a collection of any sObject type -->
        <property name="records" 
                  type="{T[]}" 
                  label="Records"
                  description="Collection of records to process"/>
        
        <!-- Define the generic type -->
        <propertyType name="T" 
                      extends="SObject" 
                      label="Object Type" 
                      description="Select the type of object"/>
        
    </targetConfig>
    

    #7.2 Handling Generic Types in CPE

    // In your CPE
    initializeGenericType() {
        if (this._genericTypeMappings?.length > 0) {
            const tMapping = this._genericTypeMappings.find(
                mapping => mapping.typeName === 'T'
            );
            if (tMapping) {
                this.selectedObjectType = tMapping.typeValue;
                this.loadObjectMetadata(tMapping.typeValue);
            }
        }
    }
    
    handleObjectTypeChange(event) {
        const objectType = event.detail.value;
        this.selectedObjectType = objectType;
        
        // Dispatch generic type mapping change
        this.dispatchEvent(new CustomEvent(
            'configuration_editor_generic_type_mapping_changed',
            {
                bubbles: true,
                composed: true,
                detail: {
                    typeName: 'T',      // Must match propertyType name
                    typeValue: objectType  // e.g., 'Account', 'Contact'
                }
            }
        ));
    }
    

    #7.3 Multiple Generic Types

    You can define multiple generic types for complex scenarios:

    <targetConfig targets="lightning__FlowScreen"
                  configurationEditor="c-record-cloner-c-p-e">
        
        <!-- Source record -->
        <property name="sourceRecord" type="{T}" label="Source Record"/>
        
        <!-- Target record (can be different type) -->
        <property name="targetRecord" type="{U}" label="Target Record"/>
        
        <!-- Define both generic types -->
        <propertyType name="T" extends="SObject" label="Source Object Type"/>
        <propertyType name="U" extends="SObject" label="Target Object Type"/>
        
    </targetConfig>
    
    // In CPE - handle both types
    handleSourceTypeChange(event) {
        this.dispatchEvent(new CustomEvent(
            'configuration_editor_generic_type_mapping_changed',
            {
                bubbles: true,
                composed: true,
                detail: { typeName: 'T', typeValue: event.detail.value }
            }
        ));
    }
    
    handleTargetTypeChange(event) {
        this.dispatchEvent(new CustomEvent(
            'configuration_editor_generic_type_mapping_changed',
            {
                bubbles: true,
                composed: true,
                detail: { typeName: 'U', typeValue: event.detail.value }
            }
        ));
    }
    

    #7.4 Generic Type Best Practices

    DoDon't
    ✓ Use single-letter names (T, U, V)✗ Use long type names
    ✓ Provide clear labels and descriptions✗ Leave descriptions empty
    ✓ Dispatch type changes immediately✗ Batch type changes
    ✓ Initialize from genericTypeMappings✗ Assume type is always set
    ✓ Handle null/undefined gracefully✗ Crash on missing type

    #8. Flow Variable Selection

    #8.1 Using External Components (Recommended)

    The recommended approach is to use specialized components like fsc_flow-combobox from Unofficial SF:

    <!-- Using fsc_flow-combobox for variable selection -->
    <c-fsc_flow-combobox
        name="initialSelectedValue"
        label="Initial Selected Value"
        value={initialSelectedValue}
        builder-context={builderContext}
        automatic-output-variables={automaticOutputVariables}
        onvaluechanged={handleInitialSelectedValueChange}>
    </c-fsc_flow-combobox>
    
    // Pass through the interfaces
    get builderContext() {
        return this._builderContext;
    }
    
    get automaticOutputVariables() {
        return this._automaticOutputVariables;
    }
    
    // Handle the value change
    handleInitialSelectedValueChange(event) {
        // Note: fsc_flow-combobox uses 'newValue' in event detail
        this.initialSelectedValue = event.detail.newValue;
        this.dispatchConfigurationChange(
            'initialSelectedValue', 
            this.initialSelectedValue
        );
    }
    

    #8.2 Manual Implementation

    If you need custom variable filtering or UI:

    loadFlowVariables() {
        const variables = [];
        const processedVariables = new Set();
        
        // None option
        variables.push({ label: '--None--', value: '' });
        
        // Regular flow variables from builderContext
        if (this._builderContext?.variables) {
            this._builderContext.variables.forEach(variable => {
                // Filter based on your needs
                if (this.isValidVariableForProperty(variable)) {
                    const variableRef = '{!' + variable.name + '}';
                    if (!processedVariables.has(variableRef)) {
                        variables.push({
                            label: variable.name,
                            value: variableRef,
                            dataType: variable.dataType,
                            isCollection: variable.isCollection
                        });
                        processedVariables.add(variableRef);
                    }
                }
            });
        }
        
        // Automatic output variables from other components
        if (this._automaticOutputVariables?.length > 0) {
            this._automaticOutputVariables.forEach(autoVar => {
                if (this.isValidVariableForProperty(autoVar)) {
                    const variableRef = '{!' + autoVar.name + '}';
                    if (!processedVariables.has(variableRef)) {
                        const label = autoVar.elementName 
                            ? `${autoVar.name} (from ${autoVar.elementName})`
                            : autoVar.name;
                        variables.push({
                            label: label,
                            value: variableRef,
                            dataType: autoVar.dataType,
                            isCollection: autoVar.isCollection
                        });
                        processedVariables.add(variableRef);
                    }
                }
            });
        }
        
        this.flowVariableOptions = variables;
    }
    
    isValidVariableForProperty(variable) {
        // For single String property
        if (this.propertyType === 'String') {
            return variable.dataType === 'String' && !variable.isCollection;
        }
        // For String collection property
        if (this.propertyType === 'String[]') {
            return variable.dataType === 'String' && variable.isCollection === true;
        }
        // For Boolean property
        if (this.propertyType === 'Boolean') {
            return variable.dataType === 'Boolean' && !variable.isCollection;
        }
        return false;
    }
    

    #8.3 Variable Reference Format

    Flow variable references use the {!variableName} format:

    // Detecting if a value is a Flow variable reference
    isFlowVariableReference(value) {
        return typeof value === 'string' && value.startsWith('{!');
    }
    
    // Extracting variable name from reference
    getVariableName(reference) {
        if (this.isFlowVariableReference(reference)) {
            return reference.substring(2, reference.length - 1);
        }
        return null;
    }
    
    // Creating a variable reference
    createVariableReference(variableName) {
        return '{!' + variableName + '}';
    }
    

    #Part III: Experience Cloud CPEs

    #9. Experience Cloud CPE Core Concepts

    #The Property Editor Contract

    Experience Cloud CPEs communicate through a simpler, direct-value interface:

    // REQUIRED @api properties for Experience Cloud CPEs
    @api label;        // String - Display label from XML
    @api description;  // String - Help text from XML
    @api required;     // Boolean - Is property required
    @api value;        // Any - THE CURRENT VALUE
    @api errors;       // Array - Validation errors FROM framework
    @api schema;       // Object - JSON Schema info for complex types
    

    #Data Flow Architecture

    ┌─────────────────────────────────────────────────────────────────────────────┐
    │                         EXPERIENCE BUILDER                                   │
    │  ┌────────────────────────────────────────────────────────────────────┐     │
    │  │                     Property Panel                                 │     │
    │  │  ┌──────────────────────────────────────────────────────────────┐ │     │
    │  │  │              Custom Property Editor                           │ │     │
    │  │  │                                                               │ │     │
    │  │  │   @api label              ─────────────────────────────────►  │ │     │
    │  │  │   @api description        ─────────────────────────────────►  │ │     │
    │  │  │   @api required           ─────────────────────────────────►  │ │     │
    │  │  │   @api value              ─────────────────────────────────►  │ │     │
    │  │  │   @api errors             ─────────────────────────────────►  │ │     │
    │  │  │   @api schema             ─────────────────────────────────►  │ │     │
    │  │  │                                                               │ │     │
    │  │  │   'valuechange' event     ◄───────────────────────────────── │ │     │
    │  │  │   { value: newValue }                                         │ │     │
    │  │  │                                                               │ │     │
    │  │  └──────────────────────────────────────────────────────────────┘ │     │
    │  └────────────────────────────────────────────────────────────────────┘     │
    │                                    │                                         │
    │                                    │ Property Value                          │
    │                                    ▼                                         │
    │  ┌────────────────────────────────────────────────────────────────────┐     │
    │  │                    Target Component                                │     │
    │  │                    @api alignment                                  │     │
    │  │                    @api cardStyle                                  │     │
    │  └────────────────────────────────────────────────────────────────────┘     │
    └─────────────────────────────────────────────────────────────────────────────┘
    

    #10. Experience Cloud CPE Implementation Guide

    #Complete Experience Cloud CPE Template

    // alignmentPropertyEditor.js
    import { LightningElement, api } from 'lwc';
    
    export default class AlignmentPropertyEditor extends LightningElement {
        // ═══════════════════════════════════════════════════════════════════════
        // EXPERIENCE CLOUD CONTRACT - Required @api Properties
        // ═══════════════════════════════════════════════════════════════════════
        
        @api label;        // Display label from XML
        @api description;  // Help text
        @api required;     // Is this property required
        @api value;        // Current value (READ FROM HERE)
        @api errors;       // Validation errors from framework
        @api schema;       // JSON Schema for complex types
        
        // ═══════════════════════════════════════════════════════════════════════
        // ALIGNMENT OPTIONS
        // ═══════════════════════════════════════════════════════════════════════
        
        alignmentOptions = [
            { value: 'left', icon: 'utility:left_align_text', label: 'Left' },
            { value: 'center', icon: 'utility:center_align_text', label: 'Center' },
            { value: 'right', icon: 'utility:right_align_text', label: 'Right' }
        ];
        
        // ═══════════════════════════════════════════════════════════════════════
        // COMPUTED PROPERTIES
        // ═══════════════════════════════════════════════════════════════════════
        
        get currentValue() {
            return this.value || 'left';
        }
        
        get hasErrors() {
            return this.errors && this.errors.length > 0;
        }
        
        get errorMessages() {
            return this.errors?.map(e => e.message).join(', ') || '';
        }
        
        get optionsWithSelection() {
            return this.alignmentOptions.map(option => ({
                ...option,
                selected: option.value === this.currentValue,
                buttonClass: option.value === this.currentValue 
                    ? 'slds-button slds-button_icon slds-button_icon-border-filled' 
                    : 'slds-button slds-button_icon slds-button_icon-border'
            }));
        }
        
        // ═══════════════════════════════════════════════════════════════════════
        // EVENT HANDLERS
        // ═══════════════════════════════════════════════════════════════════════
        
        handleAlignmentClick(event) {
            const selectedValue = event.currentTarget.dataset.value;
            
            // Dispatch valuechange event (Experience Cloud pattern)
            this.dispatchEvent(new CustomEvent('valuechange', {
                detail: { value: selectedValue },
                bubbles: true,    // ⚠️ REQUIRED
                composed: true    // ⚠️ REQUIRED
            }));
        }
    }
    

    #Experience Cloud CPE HTML Template

    <!-- alignmentPropertyEditor.html -->
    <template>
        <div class="slds-form-element">
            <!-- Label -->
            <label class="slds-form-element__label">
                <template if:true={required}>
                    <abbr class="slds-required" title="required">*</abbr>
                </template>
                {label}
            </label>
            
            <!-- Help Text -->
            <template if:true={description}>
                <div class="slds-form-element__help slds-m-bottom_x-small">
                    {description}
                </div>
            </template>
            
            <!-- Visual Alignment Picker -->
            <div class="slds-form-element__control">
                <div class="slds-button-group" role="group">
                    <template for:each={optionsWithSelection} for:item="option">
                        <button key={option.value}
                                class={option.buttonClass}
                                data-value={option.value}
                                onclick={handleAlignmentClick}
                                title={option.label}>
                            <lightning-icon 
                                icon-name={option.icon} 
                                alternative-text={option.label}
                                size="x-small">
                            </lightning-icon>
                        </button>
                    </template>
                </div>
            </div>
            
            <!-- Error Messages -->
            <template if:true={hasErrors}>
                <div class="slds-form-element__help slds-text-color_error">
                    {errorMessages}
                </div>
            </template>
        </div>
    </template>
    

    #Experience Cloud CPE CSS

    /* alignmentPropertyEditor.css */
    :host {
        display: block;
    }
    
    .slds-button_icon-border-filled {
        background-color: var(--slds-c-button-brand-color-background, #0176d3);
        border-color: var(--slds-c-button-brand-color-border, #0176d3);
    }
    
    .slds-button_icon-border-filled lightning-icon {
        --slds-c-icon-color-foreground-default: white;
    }
    

    #11. Custom Lightning Types (ExperiencePropertyTypeBundle)

    Custom Lightning Types allow you to define complex, reusable property structures for Experience Cloud components.

    #11.1 Directory Structure

    force-app/main/default/experiencePropertyTypeBundles/
    └── cardStyleType/
        ├── schema.json      # Defines the data structure
        └── design.json      # Defines the UI layout
    

    #11.2 schema.json

    Defines the properties using JSON Schema with Salesforce extensions:

    {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "title": "Card Style",
        "description": "Styling options for the card component",
        "properties": {
            "backgroundColor": {
                "type": "string",
                "title": "Background Color",
                "description": "Card background color",
                "default": "#ffffff",
                "lightning:type": "lightning__colorType"
            },
            "borderRadius": {
                "type": "string",
                "title": "Border Radius",
                "description": "Corner rounding",
                "default": "medium",
                "enum": ["none", "small", "medium", "large"]
            },
            "shadowDepth": {
                "type": "integer",
                "title": "Shadow Depth",
                "description": "Shadow intensity (0-5)",
                "default": 2,
                "minimum": 0,
                "maximum": 5
            },
            "padding": {
                "type": "string",
                "title": "Padding",
                "default": "medium",
                "enum": ["none", "small", "medium", "large"]
            },
            "headerText": {
                "type": "string",
                "title": "Header Text",
                "maxLength": 100
            },
            "showBorder": {
                "type": "boolean",
                "title": "Show Border",
                "default": true
            }
        },
        "required": ["backgroundColor", "borderRadius"]
    }
    

    #11.3 Built-in Lightning Types

    TypeDescriptionUI Control
    lightning__stringTypeBasic textText input
    lightning__integerTypeWhole numbersNumber input
    lightning__colorTypeColor valueColor picker
    lightning__dateTimeTypeDate/timeDate picker
    lightning__booleanTypeTrue/falseCheckbox
    lightning__urlTypeURLsURL input
    lightning__textTypeLong textTextarea
    lightning__richTextTypeRich textRich text editor
    lightning__imageTypeImage selectionImage picker
    lightning__iconTypeIcon selectionIcon picker

    #11.4 design.json

    Defines how properties are organized in the UI:

    {
        "layout": {
            "type": "tabs",
            "tabs": [
                {
                    "label": "Colors & Style",
                    "properties": ["backgroundColor", "borderRadius", "shadowDepth"]
                },
                {
                    "label": "Layout",
                    "properties": ["padding", "showBorder"]
                },
                {
                    "label": "Content",
                    "properties": ["headerText"]
                }
            ]
        },
        "propertyEditors": {
            "borderRadius": {
                "component": "c/borderRadiusEditor"
            }
        }
    }
    

    #11.5 Layout Types

    Tabs Layout:

    {
        "layout": {
            "type": "tabs",
            "tabs": [
                { "label": "Tab 1", "properties": ["prop1", "prop2"] },
                { "label": "Tab 2", "properties": ["prop3", "prop4"] }
            ]
        }
    }
    

    Accordion Layout:

    {
        "layout": {
            "type": "accordion",
            "sections": [
                { "label": "Section 1", "properties": ["prop1", "prop2"] },
                { "label": "Section 2", "properties": ["prop3", "prop4"] }
            ]
        }
    }
    

    Vertical Layout:

    {
        "layout": {
            "type": "vertical",
            "properties": ["prop1", "prop2", "prop3", "prop4"]
        }
    }
    

    #11.6 Using Custom Types in Components

    <!-- In component meta.xml -->
    <targetConfig targets="lightningCommunity__Default">
        <property name="cardStyle" 
                  type="cardStyleType" 
                  label="Card Styling"/>
    </targetConfig>
    
    // In component JS
    @api cardStyle;
    
    get backgroundColor() {
        return this.cardStyle?.backgroundColor || '#ffffff';
    }
    
    get borderRadiusClass() {
        const radius = this.cardStyle?.borderRadius || 'medium';
        return `border-radius-${radius}`;
    }
    

    #11.7 Custom Type Gotchas

    #GotchaSolution
    1Folder name must match type name exactlyCase-sensitive: cardStyleType/ for cardStyleType
    2Schema changes break existing configsVersion types or provide migration
    3Properties not in design.json are hiddenInclude all properties in layout
    4Editor override CPE receives sub-property onlyDesign CPE for single value
    5No array editing UIUse JSON string workaround
    6Deploy types before componentsAdd to package.xml first
    7Not supported in App BuilderExperience Cloud only

    #Part IV: Advanced Topics

    #12. Event Flow & Communication

    #Flow Builder Event Flow

    User Action → CPE Handler → CustomEvent → Flow Builder → Main Component
                                  │
                                  ├── configuration_editor_input_value_changed
                                  │   { name, newValue, newValueDataType }
                                  │
                                  └── configuration_editor_generic_type_mapping_changed
                                      { typeName, typeValue }
    

    #Experience Cloud Event Flow

    User Action → CPE Handler → CustomEvent → Experience Builder → Target Component
                                  │
                                  └── valuechange
                                      { value }
    

    #Critical Event Properties

    Both platforms require:

    • bubbles: true - Event bubbles up through DOM
    • composed: true - Event crosses shadow DOM boundaries
    // Without these, events won't reach the builder!
    new CustomEvent('valuechange', {
        detail: { value: newValue },
        bubbles: true,    // ⚠️ REQUIRED
        composed: true    // ⚠️ REQUIRED
    });
    

    #13. Lifecycle & Initialization

    #Flow CPE Initialization Timeline

    T0: Component Creation
        └── Default values initialized
    
    T1: builderContext Set
        └── Flow metadata available
        └── Load object options
        
    T2: inputVariables Set
        └── Saved values restored
        └── initializeValues() called
        
    T3: genericTypeMappings Set (if applicable)
        └── Generic type initialized
        
    T4: automaticOutputVariables Set
        └── Output variables from other components available
        └── loadFlowVariables() refreshed
        
    T5: connectedCallback
        └── DOM ready
        └── Final initialization
        
    T6: User Interaction Ready
    

    #Experience Cloud CPE Initialization Timeline

    T0: Component Creation
        └── Default values initialized
    
    T1: @api Properties Set (in order)
        └── @api value - Current value
        └── @api label - Display label
        └── @api description - Help text
        └── @api required - Required flag
        └── @api errors - Any validation errors
        └── @api schema - JSON Schema (for complex types)
        
    T2: connectedCallback
        └── DOM ready
        
    T3: User Interaction Ready
    

    #Handling Async Initialization

    // Flow CPE - handle async properly
    async loadFieldOptions(fieldToSet) {
        const savedSelection = fieldToSet || this.selectedField;
        
        this.isLoading = true;
        try {
            const options = await getFieldOptions({ 
                objectApiName: this.selectedObject 
            });
            this.fieldOptions = options || [];
            
            // Restore selection if still valid
            const stillValid = this.fieldOptions.some(
                opt => opt.value === savedSelection
            );
            if (stillValid) {
                this.selectedField = savedSelection;
            } else {
                this.selectedField = null;
                this.dispatchConfigurationChange('fieldApiName', null);
            }
        } catch (error) {
            console.error('Error loading fields:', error);
            this.fieldOptions = [];
        } finally {
            this.isLoading = false;
        }
    }
    

    #14. Validation Patterns

    #Flow CPE Validation

    Flow Builder calls @api validate() when the user tries to save:

    @api
    validate() {
        const errors = [];
        
        // Basic required validation
        if (!this.selectedObject) {
            errors.push({
                key: 'OBJECT_REQUIRED',
                errorString: 'Please select an object'
            });
        }
        
        // Conditional validation
        if (this.enableAdvancedMode && !this.advancedConfig) {
            errors.push({
                key: 'ADVANCED_CONFIG_REQUIRED',
                errorString: 'Advanced configuration is required when advanced mode is enabled'
            });
        }
        
        // Cross-field validation
        if (this.minValue > this.maxValue) {
            errors.push({
                key: 'INVALID_RANGE',
                errorString: 'Minimum value cannot exceed maximum value'
            });
        }
        
        // Pattern validation
        if (this.emailPattern && !this.isValidEmail(this.emailPattern)) {
            errors.push({
                key: 'INVALID_EMAIL',
                errorString: 'Please enter a valid email pattern'
            });
        }
        
        return errors; // Empty array = valid
    }
    
    isValidEmail(email) {
        return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
    }
    

    #Experience Cloud Validation

    Experience Cloud CPEs receive errors from the framework via @api errors:

    // Display errors received from framework
    get hasErrors() {
        return this.errors && this.errors.length > 0;
    }
    
    get errorMessages() {
        return this.errors?.map(e => e.message).join(', ') || '';
    }
    
    // Real-time validation feedback (optional)
    handleValueChange(event) {
        const value = event.target.value;
        
        // Perform client-side validation
        if (this.required && !value) {
            this.showValidationError('This field is required');
            return;
        }
        
        this.clearValidationError();
        
        // Dispatch change (framework will also validate)
        this.dispatchEvent(new CustomEvent('valuechange', {
            detail: { value },
            bubbles: true,
            composed: true
        }));
    }
    

    #15. Complex Data Handling

    #Storing Complex Objects

    Both platforms store property values as primitives. For complex data, use JSON:

    // CPE - Saving complex object
    handleConfigChange() {
        const complexConfig = {
            columns: this.selectedColumns,
            sorting: {
                field: this.sortField,
                direction: this.sortDirection
            },
            filters: this.activeFilters
        };
        
        const jsonValue = JSON.stringify(complexConfig);
        
        // Flow CPE
        this.dispatchConfigurationChange('configuration', jsonValue);
        
        // OR Experience Cloud CPE
        this.dispatchEvent(new CustomEvent('valuechange', {
            detail: { value: jsonValue },
            bubbles: true,
            composed: true
        }));
    }
    
    // Main Component - Parsing complex object
    @api
    set configuration(value) {
        try {
            this._configuration = JSON.parse(value || '{}');
        } catch (e) {
            console.error('Invalid configuration JSON:', e);
            this._configuration = {};
        }
    }
    get configuration() {
        return this._configuration;
    }
    

    #Handling Collections

    // Flow CPE - String[] properties
    handleMultiSelectChange(event) {
        const selectedValues = event.detail.value; // Array
        
        this.dispatchEvent(new CustomEvent(
            'configuration_editor_input_value_changed',
            {
                bubbles: true,
                composed: true,
                detail: {
                    name: 'selectedValues',
                    newValue: selectedValues,
                    newValueDataType: 'String[]'  // ⚠️ Important!
                }
            }
        ));
    }
    
    // Experience Cloud - JSON workaround for arrays
    handleItemsChange(event) {
        const items = event.detail.items; // Array
        const jsonValue = JSON.stringify(items);
        
        this.dispatchEvent(new CustomEvent('valuechange', {
            detail: { value: jsonValue },
            bubbles: true,
            composed: true
        }));
    }
    

    #16. Design Patterns

    #Progressive Disclosure

    Show options progressively based on selections:

    <template>
        <!-- Step 1: Always visible -->
        <lightning-combobox
            label="Object"
            value={selectedObject}
            options={objectOptions}
            onchange={handleObjectChange}>
        </lightning-combobox>
        
        <!-- Step 2: Show after object selected -->
        <template if:true={selectedObject}>
            <lightning-combobox
                label="Field"
                value={selectedField}
                options={fieldOptions}
                onchange={handleFieldChange}>
            </lightning-combobox>
        </template>
        
        <!-- Step 3: Show after field selected -->
        <template if:true={selectedField}>
            <!-- Advanced options -->
        </template>
    </template>
    

    #Accordion/Section Pattern

    Group related properties:

    <template>
        <lightning-accordion allow-multiple-sections-open
                             active-section-name={activeSections}>
            <lightning-accordion-section name="basic" label="Basic Settings">
                <!-- Basic properties -->
            </lightning-accordion-section>
            
            <lightning-accordion-section name="advanced" label="Advanced Settings">
                <!-- Advanced properties -->
            </lightning-accordion-section>
            
            <lightning-accordion-section name="styling" label="Styling">
                <!-- Style properties -->
            </lightning-accordion-section>
        </lightning-accordion>
    </template>
    

    #Visual Picker Pattern

    For visual selections like alignment, colors, icons:

    <template>
        <div class="visual-picker">
            <template for:each={options} for:item="option">
                <div key={option.value}
                     class={option.containerClass}
                     data-value={option.value}
                     onclick={handleSelection}>
                    <lightning-icon 
                        icon-name={option.icon}
                        size="medium">
                    </lightning-icon>
                    <span class="option-label">{option.label}</span>
                </div>
            </template>
        </div>
    </template>
    

    #17. Performance Optimization

    #Caching Metadata

    // Cache frequently accessed metadata
    _objectMetadataCache = new Map();
    
    async getObjectMetadata(objectName) {
        if (this._objectMetadataCache.has(objectName)) {
            return this._objectMetadataCache.get(objectName);
        }
        
        const metadata = await fetchObjectMetadata({ objectName });
        this._objectMetadataCache.set(objectName, metadata);
        return metadata;
    }
    

    #Debouncing User Input

    // Debounce rapid changes
    handleSearchInput(event) {
        const searchTerm = event.target.value;
        this.debouncedSearch(searchTerm);
    }
    
    debouncedSearch = this.debounce((searchTerm) => {
        this.performSearch(searchTerm);
    }, 300);
    
    debounce(func, wait) {
        let timeout;
        return (...args) => {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), wait);
        };
    }
    

    #Lazy Loading Options

    // Load options only when needed
    get fieldOptions() {
        if (!this._fieldOptionsLoaded && this.selectedObject) {
            this._fieldOptionsLoaded = true;
            this.loadFieldOptions();
        }
        return this._fieldOptions || [];
    }
    

    #Part V: Reference

    #18. Troubleshooting Guide

    #Flow CPE Issues

    ProblemCauseSolution
    Events not reaching Flow BuilderMissing bubbles or composedAdd bubbles: true, composed: true
    Values not initializingNot processing inputVariablesImplement initializeValues() in setter
    Flow variables not appearingNot handling automaticOutputVariablesImplement the interface and call loadFlowVariables()
    Generic type not savingWrong event nameUse configuration_editor_generic_type_mapping_changed
    Validation not runningMethod not exposedAdd @api to validate() method
    Apex calls failingMissing permissionsCheck FLS and object permissions

    #Experience Cloud CPE Issues

    ProblemCauseSolution
    Events not reaching Experience BuilderMissing bubbles or composedAdd bubbles: true, composed: true
    Value not updatingWrong event nameUse valuechange not change
    CPE not appearingWrong editor pathUse editor="c/componentName" (camelCase with /)
    CPE appearing in wrong placeUsing configurationEditorUse editor attribute on <property>
    Complex type not workingType not deployedDeploy experiencePropertyTypeBundles first
    Changes not persistingLWR publishing modelRepublish the site

    #Common Mistakes

    // ❌ WRONG: Missing bubbles/composed
    this.dispatchEvent(new CustomEvent('valuechange', {
        detail: { value: newValue }
    }));
    
    // ✅ CORRECT: Include bubbles and composed
    this.dispatchEvent(new CustomEvent('valuechange', {
        detail: { value: newValue },
        bubbles: true,
        composed: true
    }));
    
    // ❌ WRONG: Using Flow event in Experience Cloud
    this.dispatchEvent(new CustomEvent('configuration_editor_input_value_changed', {
        detail: { name: 'prop', newValue: 'val', newValueDataType: 'String' }
    }));
    
    // ✅ CORRECT: Use valuechange for Experience Cloud
    this.dispatchEvent(new CustomEvent('valuechange', {
        detail: { value: 'val' },
        bubbles: true,
        composed: true
    }));
    

    #19. Best Practices Checklist

    #General Best Practices

    • CPE component has isExposed="false" in meta.xml
    • No targets defined in CPE meta.xml
    • All events have bubbles: true and composed: true
    • Loading states displayed during async operations
    • Errors handled gracefully with fallbacks
    • Values preserved when parent selections change
    • Debouncing implemented for rapid user input
    • Console logging for debugging (remove in production)

    #Flow CPE Specific

    • All four interfaces implemented (builderContext, inputVariables, genericTypeMappings, automaticOutputVariables)
    • @api validate() method implemented
    • Correct event: configuration_editor_input_value_changed
    • Generic types use configuration_editor_generic_type_mapping_changed
    • Data types correctly specified in event detail
    • Flow variable references ({!varName}) handled
    • configurationEditor uses kebab-case (c-my-cpe)

    #Experience Cloud CPE Specific

    • All six @api properties defined (label, description, required, value, errors, schema)
    • Correct event: valuechange
    • Event detail structure: { value: newValue }
    • editor attribute uses camelCase with slash (c/myCpe)
    • Custom Lightning Types deployed before components
    • Site republished after changes

    #20. Quick Reference Cards

    #Flow CPE Quick Reference

    ┌─────────────────────────────────────────────────────────────────────────────┐
    │                         FLOW CPE QUICK REFERENCE                             │
    ├─────────────────────────────────────────────────────────────────────────────┤
    │                                                                              │
    │  REGISTRATION:                                                               │
    │  ─────────────                                                               │
    │  <targetConfig configurationEditor="c-my-cpe-name">                          │
    │                                    ^^^^^^^^^^^^^^                            │
    │                                    kebab-case                                │
    │                                                                              │
    │  INTERFACES:                                                                 │
    │  ───────────                                                                 │
    │  @api builderContext              // Flow metadata                           │
    │  @api inputVariables              // Array of {name, value, valueDataType}   │
    │  @api genericTypeMappings         // Array of {typeName, typeValue}          │
    │  @api automaticOutputVariables    // Output vars from other components       │
    │  @api validate()                  // Return array of {key, errorString}      │
    │                                                                              │
    │  VALUE CHANGE EVENT:                                                         │
    │  ───────────────────                                                         │
    │  new CustomEvent('configuration_editor_input_value_changed', {               │
    │      bubbles: true,                                                          │
    │      composed: true,                                                         │
    │      detail: {                                                               │
    │          name: 'propertyName',                                               │
    │          newValue: value,                                                    │
    │          newValueDataType: 'String' // or 'Boolean', 'String[]', etc.        │
    │      }                                                                       │
    │  });                                                                         │
    │                                                                              │
    │  GENERIC TYPE EVENT:                                                         │
    │  ───────────────────                                                         │
    │  new CustomEvent('configuration_editor_generic_type_mapping_changed', {      │
    │      bubbles: true,                                                          │
    │      composed: true,                                                         │
    │      detail: {                                                               │
    │          typeName: 'T',                                                      │
    │          typeValue: 'Account'                                                │
    │      }                                                                       │
    │  });                                                                         │
    │                                                                              │
    │  DATA TYPES: String, Boolean, Integer, DateTime, String[], Sobject, {T}      │
    │                                                                              │
    └─────────────────────────────────────────────────────────────────────────────┘
    

    #Experience Cloud CPE Quick Reference

    ┌─────────────────────────────────────────────────────────────────────────────┐
    │                    EXPERIENCE CLOUD CPE QUICK REFERENCE                      │
    ├─────────────────────────────────────────────────────────────────────────────┤
    │                                                                              │
    │  REGISTRATION:                                                               │
    │  ─────────────                                                               │
    │  <property name="alignment" type="String" editor="c/alignmentEditor"/>       │
    │                                           ^^^^^^^^^^^^^^^^^^^^^^^^           │
    │                                           camelCase with /                   │
    │                                                                              │
    │  REQUIRED @api PROPERTIES:                                                   │
    │  ─────────────────────────                                                   │
    │  @api label;        // Display label                                         │
    │  @api description;  // Help text                                             │
    │  @api required;     // Boolean - required field                              │
    │  @api value;        // Current value (READ THIS)                             │
    │  @api errors;       // Validation errors from framework                      │
    │  @api schema;       // JSON Schema for complex types                         │
    │                                                                              │
    │  VALUE CHANGE EVENT:                                                         │
    │  ───────────────────                                                         │
    │  new CustomEvent('valuechange', {                                            │
    │      bubbles: true,                                                          │
    │      composed: true,                                                         │
    │      detail: {                                                               │
    │          value: newValue                                                     │
    │      }                                                                       │
    │  });                                                                         │
    │                                                                              │
    │  BUILT-IN LIGHTNING TYPES:                                                   │
    │  ─────────────────────────                                                   │
    │  lightning__stringType, lightning__integerType, lightning__colorType,        │
    │  lightning__dateTimeType, lightning__booleanType, lightning__urlType,        │
    │  lightning__textType, lightning__richTextType, lightning__imageType,         │
    │  lightning__iconType                                                         │
    │                                                                              │
    │  CUSTOM TYPE STRUCTURE:                                                      │
    │  ──────────────────────                                                      │
    │  experiencePropertyTypeBundles/                                              │
    │  └── myType/                                                                 │
    │      ├── schema.json                                                         │
    │      └── design.json                                                         │
    │                                                                              │
    └─────────────────────────────────────────────────────────────────────────────┘
    

    #21. Complete Code Examples

    #21.1 Complete Flow CPE Example

    See Section 6 for the complete Flow CPE implementation.

    #21.2 Complete Experience Cloud CPE Example

    See Section 10 for the complete Experience Cloud CPE implementation.

    #21.3 Supporting Apex Controller

    // MyComponentController.cls
    public with sharing class MyComponentController {
        
        @AuraEnabled(cacheable=true)
        public static List<ComboboxOption> getObjectOptions() {
            List<ComboboxOption> options = new List<ComboboxOption>();
            
            // Get all standard and custom objects
            Map<String, Schema.SObjectType> globalDescribe = Schema.getGlobalDescribe();
            
            for (String objectName : globalDescribe.keySet()) {
                Schema.SObjectType sObjType = globalDescribe.get(objectName);
                Schema.DescribeSObjectResult describe = sObjType.getDescribe();
                
                // Filter to accessible, queryable objects
                if (describe.isAccessible() && describe.isQueryable() && !describe.isDeprecatedAndHidden()) {
                    options.add(new ComboboxOption(
                        describe.getLabel(),
                        describe.getName()
                    ));
                }
            }
            
            options.sort();
            return options;
        }
        
        @AuraEnabled(cacheable=true)
        public static List<ComboboxOption> getFieldOptions(String objectApiName) {
            List<ComboboxOption> options = new List<ComboboxOption>();
            
            if (String.isBlank(objectApiName)) {
                return options;
            }
            
            Schema.SObjectType sObjType = Schema.getGlobalDescribe().get(objectApiName);
            if (sObjType == null) {
                return options;
            }
            
            Map<String, Schema.SObjectField> fields = sObjType.getDescribe().fields.getMap();
            
            for (String fieldName : fields.keySet()) {
                Schema.DescribeFieldResult fieldDescribe = fields.get(fieldName).getDescribe();
                
                if (fieldDescribe.isAccessible()) {
                    options.add(new ComboboxOption(
                        fieldDescribe.getLabel(),
                        fieldDescribe.getName()
                    ));
                }
            }
            
            options.sort();
            return options;
        }
        
        @AuraEnabled(cacheable=true)
        public static List<ComboboxOption> getPicklistFields(String objectApiName) {
            List<ComboboxOption> options = new List<ComboboxOption>();
            
            if (String.isBlank(objectApiName)) {
                return options;
            }
            
            Schema.SObjectType sObjType = Schema.getGlobalDescribe().get(objectApiName);
            if (sObjType == null) {
                return options;
            }
            
            Map<String, Schema.SObjectField> fields = sObjType.getDescribe().fields.getMap();
            
            for (String fieldName : fields.keySet()) {
                Schema.DescribeFieldResult fieldDescribe = fields.get(fieldName).getDescribe();
                
                // Only include picklist fields
                if (fieldDescribe.isAccessible() && 
                    (fieldDescribe.getType() == Schema.DisplayType.PICKLIST ||
                     fieldDescribe.getType() == Schema.DisplayType.MULTIPICKLIST)) {
                    
                    Boolean isMultiSelect = fieldDescribe.getType() == Schema.DisplayType.MULTIPICKLIST;
                    
                    options.add(new ComboboxOption(
                        fieldDescribe.getLabel() + (isMultiSelect ? ' (Multi-Select)' : ''),
                        fieldDescribe.getName()
                    ));
                }
            }
            
            options.sort();
            return options;
        }
        
        public class ComboboxOption implements Comparable {
            @AuraEnabled public String label;
            @AuraEnabled public String value;
            
            public ComboboxOption(String label, String value) {
                this.label = label;
                this.value = value;
            }
            
            public Integer compareTo(Object compareTo) {
                ComboboxOption other = (ComboboxOption) compareTo;
                return this.label.compareTo(other.label);
            }
        }
    }
    

    #Resources & References

    #Official Documentation

    • LWC Developer Guide - Experience Builder
    • LWC Configuration Reference
    • Flow Builder Custom Property Editors

    #Community Resources

    • Salesforce Developer Blog - CPEs
    • Unofficial SF - Flow Combobox
    • GitHub - Experience Cloud CPE Samples

    Document Version: 2.0
    Last Updated: December 2025
    Maintained by: Marc (Salesforce Architect)

    On this page

    • Custom Property Editor (CPE) Comprehensive Developer Guide
    • Salesforce LWC Configuration Editors for Flow Builder & Experience Cloud
    • Table of Contents
    • Part I: Foundation
    • 1. Introduction & When to Use CPEs
    • What is a Custom Property Editor?
    • Two Distinct CPE Platforms
    • When to Create a CPE
    • 2. Platform Comparison: Flow vs Experience Cloud
    • Side-by-Side API Comparison
    • Declaration Syntax Comparison
    • Quick Decision Matrix
    • 3. Project Scaffolding & File Structure
    • Complete Project Structure
    • Scaffolding Templates
    • SFDX Project Creation Commands
    • Part II: Flow Builder CPEs
    • 4. Flow CPE Core Concepts
    • Component Pairing
    • Data Flow Architecture
    • 5. Flow CPE JavaScript Interfaces
    • 5.1 builderContext
    • 5.2 inputVariables
    • 5.3 genericTypeMappings
    • 5.4 automaticOutputVariables
    • 5.5 validate() Method
    • 6. Flow CPE Implementation Guide
    • Complete Flow CPE Template
    • Flow CPE HTML Template
    • 7. Generic sObject Types
    • 7.1 Declaring Generic Types
    • 7.2 Handling Generic Types in CPE
    • 7.3 Multiple Generic Types
    • 7.4 Generic Type Best Practices
    • 8. Flow Variable Selection
    • 8.1 Using External Components (Recommended)
    • 8.2 Manual Implementation
    • 8.3 Variable Reference Format
    • Part III: Experience Cloud CPEs
    • 9. Experience Cloud CPE Core Concepts
    • The Property Editor Contract
    • Data Flow Architecture
    • 10. Experience Cloud CPE Implementation Guide
    • Complete Experience Cloud CPE Template
    • Experience Cloud CPE HTML Template
    • Experience Cloud CPE CSS
    • 11. Custom Lightning Types (ExperiencePropertyTypeBundle)
    • 11.1 Directory Structure
    • 11.2 schema.json
    • 11.3 Built-in Lightning Types
    • 11.4 design.json
    • 11.5 Layout Types
    • 11.6 Using Custom Types in Components
    • 11.7 Custom Type Gotchas
    • Part IV: Advanced Topics
    • 12. Event Flow & Communication
    • Flow Builder Event Flow
    • Experience Cloud Event Flow
    • Critical Event Properties
    • 13. Lifecycle & Initialization
    • Flow CPE Initialization Timeline
    • Experience Cloud CPE Initialization Timeline
    • Handling Async Initialization
    • 14. Validation Patterns
    • Flow CPE Validation
    • Experience Cloud Validation
    • 15. Complex Data Handling
    • Storing Complex Objects
    • Handling Collections
    • 16. Design Patterns
    • Progressive Disclosure
    • Accordion/Section Pattern
    • Visual Picker Pattern
    • 17. Performance Optimization
    • Caching Metadata
    • Debouncing User Input
    • Lazy Loading Options
    • Part V: Reference
    • 18. Troubleshooting Guide
    • Flow CPE Issues
    • Experience Cloud CPE Issues
    • Common Mistakes
    • 19. Best Practices Checklist
    • General Best Practices
    • Flow CPE Specific
    • Experience Cloud CPE Specific
    • 20. Quick Reference Cards
    • Flow CPE Quick Reference
    • Experience Cloud CPE Quick Reference
    • 21. Complete Code Examples
    • 21.1 Complete Flow CPE Example
    • 21.2 Complete Experience Cloud CPE Example
    • 21.3 Supporting Apex Controller
    • Resources & References
    • Official Documentation
    • Community Resources