# LWC Flow Integration Guide This guide covers building Lightning Web Components for use in Salesforce Flow Screens. --- ## Overview ```text ┌─────────────────────────────────────────────────────────────────────┐ │ FLOW ↔ LWC COMMUNICATION │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ FLOW │ │ LWC │ │ │ │ Variables │ │ Component │ │ │ └────────┬────────┘ └────────┬────────┘ │ │ │ │ │ │ │ @api (inputOnly) │ │ │ ├─────────────────────────────────────▶│ │ │ │ │ │ │ │ FlowAttributeChangeEvent (outputOnly)│ │ │ │◀─────────────────────────────────────┤ │ │ │ │ │ │ │ FlowNavigationFinishEvent │ │ │ │◀─────────────────────────────────────┤ │ │ │ (NEXT, BACK, FINISH, PAUSE) │ │ │ │ └─────────────────────────────────────────────────────────────────────┘ ``` --- ## Quick Reference | Direction | Mechanism | Use Case | |-----------|-----------|----------| | Flow → LWC | `@api` with `role="inputOnly"` | Pass context data to component | | LWC → Flow | `FlowAttributeChangeEvent` | Return user selections/data | | LWC → Navigation | `FlowNavigationFinishEvent` | Trigger Next/Back/Finish | --- ## Meta.xml Configuration ### Target Configuration ```xml lightning__FlowScreen ``` ### Property Roles ```xml ``` ### Supported Property Types | Type | Description | Example | |------|-------------|---------| | `String` | Text values | Record IDs, names | | `Boolean` | True/false | Flags, completion status | | `Integer` | Whole numbers | Counts, indexes | | `Date` | Date values | Due dates | | `DateTime` | Date and time | Timestamps | | `@salesforce/schema/*` | SObject references | Record types | --- ## FlowAttributeChangeEvent This is the **critical** mechanism for sending data back to Flow. ### Import ```javascript import { FlowAttributeChangeEvent } from 'lightning/flowSupport'; ``` ### Usage ```javascript // Dispatch event to update Flow variable // First param: @api property name (must match meta.xml exactly) // Second param: new value this.dispatchEvent(new FlowAttributeChangeEvent( 'selectedRecordId', // Property name this.recordId // Value )); ``` ### Example: Selection Handler ```javascript @api selectedRecordId; @api selectedRecordName; handleSelect(event) { const id = event.target.dataset.id; const name = event.target.dataset.name; // Update local properties this.selectedRecordId = id; this.selectedRecordName = name; // Notify Flow of BOTH changes this.dispatchEvent(new FlowAttributeChangeEvent('selectedRecordId', id)); this.dispatchEvent(new FlowAttributeChangeEvent('selectedRecordName', name)); } ``` ### Common Mistake ```javascript // ❌ WRONG: Only updating local property this.selectedRecordId = id; // ✅ CORRECT: Update AND dispatch event this.selectedRecordId = id; this.dispatchEvent(new FlowAttributeChangeEvent('selectedRecordId', id)); ``` --- ## FlowNavigationFinishEvent Programmatically trigger Flow navigation from your component. ### Import ```javascript import { FlowNavigationFinishEvent } from 'lightning/flowSupport'; ``` ### Navigation Actions | Action | Description | When Available | |--------|-------------|----------------| | `'NEXT'` | Go to next screen | Mid-flow screens | | `'BACK'` | Go to previous screen | After first screen | | `'FINISH'` | Complete the flow | Final screens | | `'PAUSE'` | Pause flow (if enabled) | Pausable flows | ### Usage ```javascript // Navigate to next screen this.dispatchEvent(new FlowNavigationFinishEvent('NEXT')); // Navigate back this.dispatchEvent(new FlowNavigationFinishEvent('BACK')); // Finish the flow this.dispatchEvent(new FlowNavigationFinishEvent('FINISH')); ``` ### Check Available Actions Flow provides available actions via a special `@api` property: ```javascript // Automatically populated by Flow runtime @api availableActions = []; get canGoNext() { return this.availableActions.includes('NEXT'); } get canGoBack() { return this.availableActions.includes('BACK'); } handleNext() { if (this.canGoNext) { this.dispatchEvent(new FlowNavigationFinishEvent('NEXT')); } } ``` ### Conditional Navigation Buttons ```html ``` --- ## Validation Before Navigation Always validate before allowing navigation: ```javascript handleNext() { // Validate if (!this.selectedRecordId) { this.errorMessage = 'Please select a record.'; this.dispatchEvent(new FlowAttributeChangeEvent('errorMessage', this.errorMessage)); return; } // Clear error this.errorMessage = null; this.dispatchEvent(new FlowAttributeChangeEvent('errorMessage', null)); // Mark complete and navigate this.isComplete = true; this.dispatchEvent(new FlowAttributeChangeEvent('isComplete', true)); this.dispatchEvent(new FlowNavigationFinishEvent('NEXT')); } ``` --- ## Apex Integration in Flow Context ### Wire Service ```javascript import { wire } from 'lwc'; import getRecords from '@salesforce/apex/MyController.getRecords'; @api recordId; // From Flow @wire(getRecords, { parentId: '$recordId' }) wiredRecords({ error, data }) { if (data) { this.records = data; } else if (error) { this.error = this.reduceErrors(error); } } ``` ### Imperative Calls ```javascript import processRecord from '@salesforce/apex/MyController.processRecord'; async handleProcess() { this.isLoading = true; try { const result = await processRecord({ recordId: this.selectedRecordId }); if (result.success) { this.dispatchEvent(new FlowNavigationFinishEvent('NEXT')); } else { this.errorMessage = result.message; this.dispatchEvent(new FlowAttributeChangeEvent('errorMessage', result.message)); } } catch (error) { this.errorMessage = this.reduceErrors(error); } finally { this.isLoading = false; } } ``` --- ## Flow Context Variables Flow provides special context via reserved variable names: ```xml ``` --- ## Testing LWC in Flows ### Jest Testing ```javascript import { createElement } from 'lwc'; import FlowScreenComponent from 'c/flowScreenComponent'; import { FlowAttributeChangeEvent, FlowNavigationFinishEvent } from 'lightning/flowSupport'; // Mock the flow support module jest.mock('lightning/flowSupport', () => ({ FlowAttributeChangeEvent: jest.fn(), FlowNavigationFinishEvent: jest.fn() }), { virtual: true }); describe('c-flow-screen-component', () => { afterEach(() => { while (document.body.firstChild) { document.body.removeChild(document.body.firstChild); } jest.clearAllMocks(); }); it('dispatches FlowAttributeChangeEvent on selection', async () => { const element = createElement('c-flow-screen-component', { is: FlowScreenComponent }); element.availableActions = ['NEXT', 'BACK']; document.body.appendChild(element); // Simulate selection const tile = element.shadowRoot.querySelector('.record-tile'); tile.click(); // Verify event dispatched expect(FlowAttributeChangeEvent).toHaveBeenCalled(); }); it('dispatches FlowNavigationFinishEvent on next', async () => { const element = createElement('c-flow-screen-component', { is: FlowScreenComponent }); element.availableActions = ['NEXT']; element.selectedRecordId = '001xx000000001'; document.body.appendChild(element); // Click next button const nextButton = element.shadowRoot.querySelector('lightning-button[label="Next"]'); nextButton.click(); // Verify navigation event expect(FlowNavigationFinishEvent).toHaveBeenCalledWith('NEXT'); }); }); ``` ### Manual Testing 1. Create a Screen Flow in Setup 2. Add your LWC component to a screen 3. Map input/output variables 4. Test in Flow debug mode 5. Verify variable values in debug panel --- ## Common Patterns ### Selection with Confirmation ```javascript handleSelect(event) { this.selectedId = event.target.dataset.id; // Don't navigate yet - wait for explicit confirmation } handleConfirm() { if (!this.selectedId) { this.showError('Please select an item'); return; } this.dispatchEvent(new FlowAttributeChangeEvent('selectedId', this.selectedId)); this.dispatchEvent(new FlowNavigationFinishEvent('NEXT')); } ``` ### Multi-Select to Collection ```javascript @api selectedIds = []; handleToggle(event) { const id = event.target.dataset.id; if (this.selectedIds.includes(id)) { this.selectedIds = this.selectedIds.filter(i => i !== id); } else { this.selectedIds = [...this.selectedIds, id]; } // Send collection back to Flow this.dispatchEvent(new FlowAttributeChangeEvent('selectedIds', this.selectedIds)); } ``` ### Conditional Screen (Skip Logic) ```javascript connectedCallback() { // Auto-skip if condition met if (this.shouldSkip) { this.dispatchEvent(new FlowNavigationFinishEvent('NEXT')); } } ``` --- ## Troubleshooting | Issue | Cause | Solution | |-------|-------|----------| | Output not updating in Flow | Missing FlowAttributeChangeEvent | Always dispatch event after updating @api property | | Navigation buttons not showing | Wrong availableActions | Check Flow provides availableActions correctly | | Component not appearing | Missing `isExposed: true` | Set in meta.xml | | Properties not mapping | Role mismatch | Use `inputOnly` for inputs, `outputOnly` for outputs | | Values reset on navigation | Local state not persisted | Use @api properties for all persisted data | --- ## Template Use the template at `assets/flow-screen-component/` as a starting point. --- ## Passing sObjects and Wrapper Classes to Flow ### Overview Flow can receive complex Apex types through `apex://` type bindings. This enables: - Passing sObjects directly (not just IDs) - Passing wrapper/DTO classes with multiple fields - Two-way data binding for record editing ### apex:// Type Syntax In your meta.xml, reference Apex classes using the `apex://` prefix: ```xml ``` ### Wrapper Class Requirements Apex wrapper classes must be **public** and have **public properties**: ```apex public class OrderController { // Wrapper class for Flow public class OrderSummaryWrapper { @AuraEnabled public String orderId; @AuraEnabled public String orderName; @AuraEnabled public Decimal totalAmount; @AuraEnabled public List lineItems; @AuraEnabled public Account customer; } public class LineItemWrapper { @AuraEnabled public String productName; @AuraEnabled public Integer quantity; @AuraEnabled public Decimal unitPrice; } // Invocable method to create the wrapper @InvocableMethod(label='Get Order Summary') public static List getOrderSummary(List orderIds) { // Query and build wrapper... } } ``` ### Using sObjects in LWC ```javascript import { api, LightningElement } from 'lwc'; import { FlowAttributeChangeEvent } from 'lightning/flowSupport'; export default class AccountEditor extends LightningElement { // Receive sObject from Flow @api accountRecord; // Track local modifications _modifiedAccount; connectedCallback() { // Create a working copy this._modifiedAccount = { ...this.accountRecord }; } handleNameChange(event) { this._modifiedAccount.Name = event.target.value; } handleSave() { // Send modified record back to Flow this.dispatchEvent( new FlowAttributeChangeEvent('updatedAccount', this._modifiedAccount) ); } } ``` ### Using Wrapper Classes in LWC ```javascript import { api, LightningElement } from 'lwc'; export default class OrderSummaryViewer extends LightningElement { @api orderSummary; // apex://OrderController.OrderSummaryWrapper get formattedTotal() { return this.orderSummary?.totalAmount?.toLocaleString('en-US', { style: 'currency', currency: 'USD' }); } get lineItems() { return this.orderSummary?.lineItems || []; } get customerName() { // Access nested sObject return this.orderSummary?.customer?.Name || 'Unknown'; } } ``` ### Flow Configuration for apex:// Types 1. **Create an Invocable Action** that returns your wrapper: ```apex @InvocableMethod public static List getData(List inputs) { ... } ``` 2. **In Flow Builder**, call the Invocable Action before the screen 3. **Store result** in an Apex-Defined Variable 4. **Pass to LWC** via the screen component input mapping ### Common Patterns #### Pattern 1: Record Edit with Validation ```javascript // LWC that receives, edits, and returns an sObject @api inputRecord; // apex://Contact (inputOnly) @api outputRecord; // apex://Contact (outputOnly) @api isValid = false; // Boolean (outputOnly) handleFieldChange(event) { const field = event.target.dataset.field; this.workingRecord[field] = event.target.value; // Validate and update outputs this.isValid = this.validateRecord(); this.dispatchEvent(new FlowAttributeChangeEvent('outputRecord', this.workingRecord)); this.dispatchEvent(new FlowAttributeChangeEvent('isValid', this.isValid)); } ``` #### Pattern 2: Multi-Record Selection ```javascript // Select from a list, output selected items @api availableRecords; // apex://Account[] (inputOnly) @api selectedRecords = []; // apex://Account[] (outputOnly) handleSelect(event) { const id = event.target.dataset.id; const record = this.availableRecords.find(r => r.Id === id); if (record && !this.selectedRecords.find(r => r.Id === id)) { this.selectedRecords = [...this.selectedRecords, record]; this.dispatchEvent( new FlowAttributeChangeEvent('selectedRecords', this.selectedRecords) ); } } ``` #### Pattern 3: Master-Detail Editing ```javascript // Edit parent with nested child records @api orderWrapper; // apex://OrderController.OrderWithLines (inputOnly) @api updatedOrder; // apex://OrderController.OrderWithLines (outputOnly) handleLineItemChange(event) { const index = event.target.dataset.index; const field = event.target.dataset.field; const value = event.target.value; // Update nested structure const updated = JSON.parse(JSON.stringify(this.orderWrapper)); updated.lineItems[index][field] = value; // Recalculate totals updated.totalAmount = updated.lineItems.reduce( (sum, item) => sum + (item.quantity * item.unitPrice), 0 ); this.updatedOrder = updated; this.dispatchEvent(new FlowAttributeChangeEvent('updatedOrder', updated)); } ``` ### Limitations | Limitation | Workaround | |------------|------------| | No `@JsonAccess` support | Ensure wrapper classes don't require JSON annotation | | 1000 record limit per collection | Paginate or filter in Apex before passing | | No generic types | Create specific wrapper classes | | Complex nesting depth | Flatten deep hierarchies | ### Debugging Tips 1. **Console log received data** to verify structure: ```javascript connectedCallback() { console.log('Received from Flow:', JSON.stringify(this.inputWrapper)); } ``` 2. **Check Apex class visibility** - inner classes need `public` modifier 3. **Verify @AuraEnabled** on all properties you need to access --- ## Cross-Skill Integration | Integration | See Also | |-------------|----------| | Flow → Apex → LWC | [triangle-pattern.md](triangle-pattern.md) | | Apex @AuraEnabled | [sf-apex/references/best-practices.md](../../sf-apex/references/best-practices.md) | | Flow Templates | [sf-flow/assets/](../../sf-flow/assets/) | | Async Notifications | [async-notification-patterns.md](async-notification-patterns.md) | | State Management | [state-management.md](state-management.md) |