Skip to content
Back to skills

2522 Flow Integration Guide 0dae3399

ASecurity

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

  • 9 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added May 31, 2026
developmentjavascriptgojavatestingdebuggingapi

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned May 31, 2026

npx -y skills add tools-only/X-Skills --skill 2522-flow-integration-guide_0dae3399 --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 2522 Flow Integration Guide 0dae3399?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 2522 Flow Integration Guide 0dae3399
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-2522-flow-integration-guide-0dae3399/badge)](https://www.skillsdirectory.com/skills/tools-only-2522-flow-integration-guide-0dae3399)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
<!-- Parent: sf-lwc/SKILL.md -->
# 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 `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
<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/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) |

Files in this skill

  • README.md859 B
  • skill.md19 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.