Skip to content
Back to skills

1095 Triangle Pattern 2184b13a

ASecurity

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. ---

  • 9 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 11, 2026
developmentjavascriptjavareacttestingapidocumentation

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 11, 2026

npx -y skills add tools-only/X-Skills --skill 1095-triangle-pattern_2184b13a --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 1095 Triangle Pattern 2184b13a?

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

Security grade badge for 1095 Triangle Pattern 2184b13a
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-1095-triangle-pattern-2184b13a/badge)](https://www.skillsdirectory.com/skills/tools-only-1095-triangle-pattern-2184b13a)

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

SKILL.md
<!-- Parent: sf-lwc/SKILL.md -->
# 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/assets/flow-screen-component/` |
| LWC best practices | `sf-lwc/references/lwc-best-practices.md` |
| Apex triangle perspective | `sf-apex/references/triangle-pattern.md` |
| Flow triangle perspective | `sf-flow/references/triangle-pattern.md` |

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.