Custom Property Editor guide for Salesforce LWC Configuration Editors
Version: 2.0
Author: Marc Swan (Salesforce Architect)
Last Updated: December 2025
Minimum API Version: 61.0+
Part I: Foundation
Part II: Flow Builder CPEs
Part III: Experience Cloud CPEs
Part IV: Advanced Topics
Part V: Reference
A Custom Property Editor (CPE) is a Lightning Web Component that replaces the default property panel input with a custom UI. CPEs enable:
Salesforce supports CPEs in two different contexts with completely different APIs:
| Platform | Use Case | Builder Interface |
|---|---|---|
| Flow Builder | Flow Screen Components, Invocable Actions | Flow Builder |
| Experience Cloud | Site Components | Experience Builder |
⚠️ CRITICAL: These are NOT interchangeable. Code written for one platform will NOT work on the other.
| Requirement | Flow CPE | Experience 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 | ✓ | ✓ |
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────────────────────────────────────┘
<!-- 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>
<!-- Main component meta.xml -->
<targetConfig targets="lightningCommunity__Default">
<property name="alignment"
type="String"
label="Text Alignment"
editor="c/alignmentPropertyEditor"/>
<!-- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ camelCase with /, on property -->
</targetConfig>
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
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
<?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>
<?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>
<?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>
<?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>
# 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
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 Name | CPE Reference |
|---|---|
myFlowComponent | c-my-flow-component |
superListBoxCPE | c-super-list-box-c-p-e |
accountSelector | c-account-selector |
┌─────────────────────────────────────────────────────────────────────────────┐
│ 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 │ │
│ └────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
Flow Builder communicates with CPEs through four main @api properties:
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
}
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[]' }
]
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();
}
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.
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
}
// 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;
}
}
<!-- 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>
Generic sObject types allow your Flow component to work with any Salesforce object selected at design time.
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>
// 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'
}
}
));
}
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 }
}
));
}
| Do | Don'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 |
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
);
}
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;
}
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 + '}';
}
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
┌─────────────────────────────────────────────────────────────────────────────┐
│ 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 │ │
│ └────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
// 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
}));
}
}
<!-- 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>
/* 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;
}
Custom Lightning Types allow you to define complex, reusable property structures for Experience Cloud components.
force-app/main/default/experiencePropertyTypeBundles/
└── cardStyleType/
├── schema.json # Defines the data structure
└── design.json # Defines the UI layout
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"]
}
| Type | Description | UI Control |
|---|---|---|
lightning__stringType | Basic text | Text input |
lightning__integerType | Whole numbers | Number input |
lightning__colorType | Color value | Color picker |
lightning__dateTimeType | Date/time | Date picker |
lightning__booleanType | True/false | Checkbox |
lightning__urlType | URLs | URL input |
lightning__textType | Long text | Textarea |
lightning__richTextType | Rich text | Rich text editor |
lightning__imageType | Image selection | Image picker |
lightning__iconType | Icon selection | Icon picker |
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"
}
}
}
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"]
}
}
<!-- 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}`;
}
| # | Gotcha | Solution |
|---|---|---|
| 1 | Folder name must match type name exactly | Case-sensitive: cardStyleType/ for cardStyleType |
| 2 | Schema changes break existing configs | Version types or provide migration |
| 3 | Properties not in design.json are hidden | Include all properties in layout |
| 4 | Editor override CPE receives sub-property only | Design CPE for single value |
| 5 | No array editing UI | Use JSON string workaround |
| 6 | Deploy types before components | Add to package.xml first |
| 7 | Not supported in App Builder | Experience Cloud only |
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 }
User Action → CPE Handler → CustomEvent → Experience Builder → Target Component
│
└── valuechange
{ value }
Both platforms require:
bubbles: true - Event bubbles up through DOMcomposed: 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
});
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
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
// 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;
}
}
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 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
}));
}
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;
}
// 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
}));
}
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>
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>
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>
// 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;
}
// 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);
};
}
// Load options only when needed
get fieldOptions() {
if (!this._fieldOptionsLoaded && this.selectedObject) {
this._fieldOptionsLoaded = true;
this.loadFieldOptions();
}
return this._fieldOptions || [];
}
| Problem | Cause | Solution |
|---|---|---|
| Events not reaching Flow Builder | Missing bubbles or composed | Add bubbles: true, composed: true |
| Values not initializing | Not processing inputVariables | Implement initializeValues() in setter |
| Flow variables not appearing | Not handling automaticOutputVariables | Implement the interface and call loadFlowVariables() |
| Generic type not saving | Wrong event name | Use configuration_editor_generic_type_mapping_changed |
| Validation not running | Method not exposed | Add @api to validate() method |
| Apex calls failing | Missing permissions | Check FLS and object permissions |
| Problem | Cause | Solution |
|---|---|---|
| Events not reaching Experience Builder | Missing bubbles or composed | Add bubbles: true, composed: true |
| Value not updating | Wrong event name | Use valuechange not change |
| CPE not appearing | Wrong editor path | Use editor="c/componentName" (camelCase with /) |
| CPE appearing in wrong place | Using configurationEditor | Use editor attribute on <property> |
| Complex type not working | Type not deployed | Deploy experiencePropertyTypeBundles first |
| Changes not persisting | LWR publishing model | Republish the site |
// ❌ 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
}));
isExposed="false" in meta.xmlbubbles: true and composed: true@api validate() method implementedconfiguration_editor_input_value_changedconfiguration_editor_generic_type_mapping_changed{!varName}) handledc-my-cpe)valuechange{ value: newValue }c/myCpe)┌─────────────────────────────────────────────────────────────────────────────┐
│ 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 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 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 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
See Section 6 for the complete Flow CPE implementation.
See Section 10 for the complete Experience Cloud CPE implementation.
// 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);
}
}
}
Document Version: 2.0
Last Updated: December 2025
Maintained by: Marc (Salesforce Architect)