# 084 Building 208f8a41

> LWC Flow Integration Guide

- Skill: `tools-only/084-building-208f8a41` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add tools-only/084-building-208f8a41`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tools-only/084-building-208f8a41/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: tools-only (https://skillmd.com/u/tools-only)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/tools-only/084-building-208f8a41

---

# LWC Flow Integration Guide

This guide covers building Lightning Web Components for use in Salesforce Flow Screens.

---

## Overview

```
┌─────────────────────────────────────────────────────────────────────┐
│                     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
<targets>
    <target>lightning__FlowScreen</target>
</targets>
```

### Property Roles

```xml
<targetConfig targets="lightning__FlowScreen">
    <!-- INPUT: Flow → Component -->
    <property
        name="recordId"
        type="String"
        label="Record ID"
        description="ID from Flow"
        role="inputOnly"/>

    <!-- OUTPUT: Component → Flow -->
    <property
        name="selectedValue"
        type="String"
        label="Selected Value"
        description="User's selection"
        role="outputOnly"/>
</targetConfig>
```

### 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
<template lwc:if={canGoBack}>
    <lightning-button label="Back" onclick={handleBack}></lightning-button>
</template>

<template lwc:if={canGoNext}>
    <lightning-button label="Next" variant="brand" onclick={handleNext}></lightning-button>
</template>
```

---

## 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
<!-- In Flow Builder, map these to your component -->
<property name="recordId" value="{!$Record.Id}"/>
<property name="objectApiName" value="{!$Record.Object}"/>
```

---

## 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 `templates/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
<targetConfig targets="lightning__FlowScreen">
    <!-- Pass entire Account record -->
    <property
        name="accountRecord"
        type="apex://Account"
        label="Account Record"
        role="inputOnly"/>

    <!-- Pass custom wrapper class -->
    <property
        name="orderSummary"
        type="apex://OrderController.OrderSummaryWrapper"
        label="Order Summary"
        role="inputOnly"/>

    <!-- Output a modified record -->
    <property
        name="updatedAccount"
        type="apex://Account"
        label="Updated Account"
        role="outputOnly"/>
</targetConfig>
```

### 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<LineItemWrapper> 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<OrderSummaryWrapper> getOrderSummary(List<Id> 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<MyWrapper> getData(List<String> 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/docs/best-practices.md](../../sf-apex/docs/best-practices.md) |
| Flow Templates | [sf-flow/templates/](../../sf-flow/templates/) |
| Async Notifications | [async-notification-patterns.md](async-notification-patterns.md) |
| State Management | [state-management.md](state-management.md) |

