# 1095 Triangle Pattern Acc40d97

> Flow-LWC-Apex Triangle: LWC Perspective

- Skill: `tools-only/1095-triangle-pattern-acc40d97` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add tools-only/1095-triangle-pattern-acc40d97`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tools-only/1095-triangle-pattern-acc40d97/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: tools-only (https://skillmd.com/u/tools-only)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/tools-only/1095-triangle-pattern-acc40d97

---

# Flow-LWC-Apex Triangle: LWC Perspective

The **Triangle Architecture** is a foundational Salesforce pattern where Flow, LWC, and Apex work together. This guide focuses on the **LWC role** in this architecture.

---

## Architecture Overview

```
                         ┌─────────────────────────────────────┐
                         │              FLOW                   │
                         │         (Orchestrator)              │
                         └───────────────┬─────────────────────┘
                                         │
              ┌──────────────────────────┼──────────────────────────┐
              │                          │                          │
              │ screens                  │ actionCalls              │
              │ <componentInstance>      │ actionType="apex"        │
              │                          │                          │
              ▼                          ▼                          ▲
┌─────────────────────────┐    ┌─────────────────────────┐         │
│          LWC            │    │         APEX            │         │
│     (UI Component)      │───▶│   (Business Logic)      │─────────┘
│                         │    │                         │
│ • Rich UI/UX        ◀── YOU ARE HERE                   │
│ • User Interaction      │    │ • @InvocableMethod      │
│ • FlowAttribute         │    │ • @AuraEnabled          │
│   ChangeEvent           │    │ • Complex Logic         │
│ • FlowNavigation        │    │ • DML Operations        │
│   FinishEvent           │    │                         │
└─────────────────────────┘    └─────────────────────────┘
              │                          ▲
              │      @AuraEnabled        │
              │      wire / imperative   │
              └──────────────────────────┘
```

---

## LWC's Role in the Triangle

| Communication Path | Mechanism | Direction |
|-------------------|-----------|-----------|
| Flow → LWC | `inputParameters` → `@api` | One-way input |
| LWC → Flow | `FlowAttributeChangeEvent` | Event-based output |
| LWC → Flow Nav | `FlowNavigationFinishEvent` | Navigation command |
| LWC → Apex | `@wire` or `imperative` | Async call |
| Apex → LWC | Return value / wire refresh | Response |

---

## Pattern 1: Flow Screen Component

**Use Case**: Custom UI component for user selection within a guided Flow.

```
┌─────────┐     @api (in)      ┌─────────┐
│  FLOW   │ ────────────────▶  │   LWC   │
│ Screen  │                    │  Screen │
│         │ ◀────────────────  │Component│
│         │  FlowAttribute     │         │
└─────────┘   ChangeEvent      └─────────┘
```

### LWC JavaScript

```javascript
import { LightningElement, api } from 'lwc';
import { FlowAttributeChangeEvent, FlowNavigationFinishEvent } from 'lightning/flowSupport';

export default class RecordSelector extends LightningElement {
    // Input from Flow (read-only in component)
    @api recordId;
    @api availableActions = [];

    // Output to Flow (must dispatch event to update)
    @api selectedRecordId;

    handleSelect(event) {
        // Update local property
        this.selectedRecordId = event.detail.id;

        // CRITICAL: Dispatch event to update Flow variable
        this.dispatchEvent(new FlowAttributeChangeEvent(
            'selectedRecordId',  // Must match @api property name
            this.selectedRecordId
        ));
    }

    handleFinish() {
        // Navigate Flow to next screen
        if (this.availableActions.find(action => action === 'NEXT')) {
            this.dispatchEvent(new FlowNavigationFinishEvent('NEXT'));
        }
    }
}
```

### LWC meta.xml (Flow Target)

```xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>65.0</apiVersion>
    <isExposed>true</isExposed>
    <masterLabel>Record Selector</masterLabel>
    <description>Custom record selection component for Flow screens</description>
    <targets>
        <target>lightning__FlowScreen</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__FlowScreen">
            <property name="recordId" type="String" label="Record ID" description="Parent record ID"/>
            <property name="selectedRecordId" type="String" label="Selected Record ID" role="outputOnly"/>
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>
```

### Property Roles

| Role | Description | Flow Behavior |
|------|-------------|---------------|
| (none) | Input/Output | Editable in Flow builder |
| `role="inputOnly"` | Input only | Cannot be used as output |
| `role="outputOnly"` | Output only | Read-only in Flow builder |

---

## Pattern 2: LWC Calling Apex

**Use Case**: LWC needs data or operations beyond Flow context.

### Wire Pattern (Reactive, Cached)

```javascript
import { LightningElement, api, wire } from 'lwc';
import getRecords from '@salesforce/apex/RecordController.getRecords';

export default class RecordList extends LightningElement {
    @api recordId;
    records;
    error;

    @wire(getRecords, { parentId: '$recordId' })
    wiredRecords({ error, data }) {
        if (data) {
            this.records = data;
            this.error = undefined;
        } else if (error) {
            this.error = error;
            this.records = undefined;
        }
    }
}
```

### Imperative Pattern (On-Demand, Fresh)

```javascript
import { LightningElement, api } from 'lwc';
import { FlowNavigationFinishEvent } from 'lightning/flowSupport';
import processRecord from '@salesforce/apex/RecordController.processRecord';

export default class RecordProcessor extends LightningElement {
    @api recordId;
    @api availableActions = [];
    isProcessing = false;

    async handleSubmit() {
        this.isProcessing = true;
        try {
            const result = await processRecord({ recordId: this.recordId });
            if (result.isSuccess) {
                // Navigate Flow forward
                this.dispatchEvent(new FlowNavigationFinishEvent('NEXT'));
            }
        } catch (error) {
            // Handle error (show toast, etc.)
            console.error('Processing failed:', error.body.message);
        } finally {
            this.isProcessing = false;
        }
    }
}
```

### When to Use Each Pattern

| Pattern | Use When | Caching |
|---------|----------|---------|
| `@wire` | Data should refresh when inputs change | Yes (cacheable=true) |
| Imperative | User-triggered action, DML needed | No |

---

## Pattern 3: Full Triangle Integration

**Use Case**: LWC in Flow screen that calls Apex for complex operations.

```javascript
import { LightningElement, api, wire } from 'lwc';
import { FlowAttributeChangeEvent, FlowNavigationFinishEvent } from 'lightning/flowSupport';
import getProducts from '@salesforce/apex/ProductController.getProducts';
import calculatePricing from '@salesforce/apex/PricingService.calculate';

export default class ProductSelector extends LightningElement {
    @api accountId;           // Input from Flow
    @api selectedProducts;    // Output to Flow
    @api totalPrice;          // Output to Flow
    @api availableActions = [];

    products;
    selectedIds = new Set();

    // Wire: Fetch products reactively
    @wire(getProducts, { accountId: '$accountId' })
    wiredProducts({ data, error }) {
        if (data) this.products = data;
    }

    handleProductSelect(event) {
        const productId = event.target.dataset.id;
        if (this.selectedIds.has(productId)) {
            this.selectedIds.delete(productId);
        } else {
            this.selectedIds.add(productId);
        }
        this.updateFlowOutputs();
    }

    async updateFlowOutputs() {
        // Update selected products output
        this.selectedProducts = Array.from(this.selectedIds);
        this.dispatchEvent(new FlowAttributeChangeEvent(
            'selectedProducts',
            this.selectedProducts
        ));

        // Imperative: Calculate pricing
        const result = await calculatePricing({
            productIds: this.selectedProducts
        });
        this.totalPrice = result.total;
        this.dispatchEvent(new FlowAttributeChangeEvent(
            'totalPrice',
            this.totalPrice
        ));
    }

    handleNext() {
        this.dispatchEvent(new FlowNavigationFinishEvent('NEXT'));
    }
}
```

---

## Jest Testing (Flow Integration)

```javascript
import { createElement } from 'lwc';
import RecordSelector from 'c/recordSelector';
import { FlowAttributeChangeEvent, FlowNavigationFinishEvent } from 'lightning/flowSupport';

// Mock lightning/flowSupport
jest.mock('lightning/flowSupport', () => ({
    FlowAttributeChangeEvent: jest.fn(),
    FlowNavigationFinishEvent: jest.fn()
}), { virtual: true });

describe('c-record-selector', () => {
    afterEach(() => {
        while (document.body.firstChild) {
            document.body.removeChild(document.body.firstChild);
        }
        jest.clearAllMocks();
    });

    it('dispatches FlowAttributeChangeEvent on selection', async () => {
        const element = createElement('c-record-selector', {
            is: RecordSelector
        });
        element.recordId = '001xx000003GYHAA2';
        document.body.appendChild(element);

        // Simulate user selection
        const handler = jest.fn();
        element.addEventListener('flowattributechange', handler);

        const tile = element.shadowRoot.querySelector('[data-id]');
        tile.click();

        // Verify FlowAttributeChangeEvent was constructed
        expect(FlowAttributeChangeEvent).toHaveBeenCalledWith(
            'selectedRecordId',
            expect.any(String)
        );
    });

    it('dispatches FlowNavigationFinishEvent on finish', async () => {
        const element = createElement('c-record-selector', {
            is: RecordSelector
        });
        element.availableActions = ['NEXT', 'FINISH'];
        document.body.appendChild(element);

        const finishButton = element.shadowRoot.querySelector('.finish-button');
        finishButton.click();

        expect(FlowNavigationFinishEvent).toHaveBeenCalledWith('NEXT');
    });
});
```

---

## Deployment Order

When deploying integrated triangle solutions:

```
1. APEX CLASSES
   └── @AuraEnabled controllers (LWC depends on these)

2. LWC COMPONENTS      ← Deploy SECOND
   └── meta.xml with lightning__FlowScreen target
   └── JavaScript with Flow imports

3. FLOWS
   └── Reference deployed LWC components
```

---

## Common Anti-Patterns

| Anti-Pattern | Problem | Solution |
|--------------|---------|----------|
| Missing FlowAttributeChangeEvent | Output never updates in Flow | Always dispatch event when output changes |
| Direct @api property mutation for outputs | Flow doesn't see changes | Use FlowAttributeChangeEvent |
| Mixing @wire and imperative for same data | Cache/freshness conflicts | Choose one pattern per data need |
| Calling Apex for Flow-available data | Unnecessary callouts | Pass via inputParameters |
| Hardcoded navigation actions | Breaks in different contexts | Check availableActions first |

---

## Flow Events Reference

| Event | Purpose | Parameters |
|-------|---------|------------|
| `FlowAttributeChangeEvent` | Update output property | (propertyName, value) |
| `FlowNavigationFinishEvent` | Navigate Flow | 'NEXT', 'BACK', 'PAUSE', 'FINISH' |

---

## Related Documentation

| Topic | Location |
|-------|----------|
| Flow screen component template | `sf-lwc/templates/flow-screen-component/` |
| LWC best practices | `sf-lwc/docs/lwc-best-practices.md` |
| Apex triangle perspective | `sf-apex/docs/triangle-pattern.md` |
| Flow triangle perspective | `sf-flow/docs/triangle-pattern.md` |

