Forms, Flows and Production · Lesson 22 of 25
LWC in Flows, Quick Actions and Experience Cloud
Some of the most valuable LWCs never sit on a page by themselves. Admins build processes in Flow and need a custom screen that standard components can't provide; users want a one-click quick action on a record; customers use an Experience Cloud site. The same component model works in all three – you mostly change the configuration file and a few APIs.
In this lesson
- A Screen Flow component with inputs, outputs and validation
- Screen and headless quick actions
- Experience Cloud considerations
A Screen Flow component
Flow passes values in through @api properties and reads outputs back. When your component changes an output, it tells the flow with FlowAttributeChangeEvent; Flow calls your @api validate() before moving to the next screen:
<?xml version="1.0" encoding="UTF-8"?>
<!-- seatPicker.js-meta.xml -->
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>65.0</apiVersion>
<isExposed>true</isExposed>
<masterLabel>Seat Picker</masterLabel>
<targets>
<target>lightning__FlowScreen</target>
</targets>
<targetConfigs>
<targetConfig targets="lightning__FlowScreen">
<property name="maxSeats" type="Integer" label="Maximum seats" role="inputOnly" />
<property name="seats" type="Integer" label="Seats selected" />
</targetConfig>
</targetConfigs>
</LightningComponentBundle>
<!-- seatPicker.html -->
<template>
<p>Seats: {seats} of {maxSeats}</p>
<lightning-button label="Add seat" onclick={addSeat}></lightning-button>
<lightning-button label="Remove seat" onclick={removeSeat}></lightning-button>
</template>
// seatPicker.js
import { LightningElement, api } from 'lwc';
import { FlowAttributeChangeEvent } from 'lightning/flowSupport';
export default class SeatPicker extends LightningElement {
@api maxSeats = 5; // input from the flow
@api seats = 0; // input and output
setSeats(value) {
this.dispatchEvent(new FlowAttributeChangeEvent('seats', value)); // the flow updates its variable…
}
addSeat() {
if (this.seats < this.maxSeats) this.setSeats(this.seats + 1);
}
removeSeat() {
if (this.seats > 0) this.setSeats(this.seats - 1);
}
@api
validate() { // called by Flow when the user clicks Next
if (this.seats > 0) return { isValid: true };
return { isValid: false, errorMessage: 'Pick at least one seat to continue.' };
}
}
Seats: 0 of 3
[Add seat] [Remove seat]
› the platform calls validate() → returns {"isValid":false,"errorMessage":"Pick at least one seat to continue."}
Seats: 0 of 3
[Add seat] [Remove seat]
› click “Add seat”
Seats: 0 of 3
[Add seat] [Remove seat]
flow output seats = 1
› parent sets seats = 1
Seats: 1 of 3
[Add seat] [Remove seat]
› the platform calls validate() → returns {"isValid":true}
Seats: 1 of 3
[Add seat] [Remove seat]Notice the component doesn't set this.seats itself: it dispatches the event, the flow updates its variable and passes the new value back down (the "parent sets seats = 1" step). That keeps the flow as the single source of truth. To move the flow forward from a custom button, dispatch FlowNavigationNextEvent instead.
Quick actions
With target lightning__RecordAction, a component can be a quick action button on a record page. A screen action opens your component in a modal; a headless action runs code with no UI:
<?xml version="1.0" encoding="UTF-8"?>
<!-- markPrimary.js-meta.xml -->
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>65.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__RecordAction</target>
</targets>
<targetConfigs>
<targetConfig targets="lightning__RecordAction">
<actionType>Action</actionType> <!-- "ScreenAction" for a modal -->
</targetConfig>
</targetConfigs>
</LightningComponentBundle>
// markPrimary.js
import { LightningElement, api } from 'lwc';
import { updateRecord } from 'lightning/uiRecordApi';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
export default class MarkPrimary extends LightningElement {
@api recordId;
@api
async invoke() { // a headless action: Salesforce calls invoke()
await updateRecord({ fields: { Id: this.recordId, Is_Primary__c: true } });
this.dispatchEvent(new ShowToastEvent({ title: 'Marked as primary contact', variant: 'success' }));
}
}
<!-- markPrimary.html -->
<template></template>
(nothing rendered)
› the platform calls invoke()
(nothing rendered)
toast (success): Marked as primary contact
updateRecord called 1× with {"fields":{"Id":"003XX0000004Abc","Is_Primary__c":true}}For a screen action, the component renders a normal template (Salesforce wraps it in a modal) and closes itself with this.dispatchEvent(new CloseActionScreenEvent()) from lightning/actions. Note that recordId is set slightly later in quick actions than on record pages – read it in a getter or renderedCallback, not in connectedCallback.
Experience Cloud
| Topic | What to know |
|---|---|
| Targets | lightningCommunity__Page to appear in Experience Builder, lightningCommunity__Default for builder properties |
| Guest users | unauthenticated visitors have very limited access – design Apex and sharing for them explicitly |
| Navigation | use comm__namedPage page references; record pages work with the site's URLs |
| Styling | sites use their own theme; light DOM components can pick up site-wide CSS |
| Toasts | prefer lightning/toast, which works outside Lightning Experience |
Design components for several containers
Keep business logic out of container-specific code: a core component that takes inputs and emits events can be wrapped by a thin Flow screen, quick action or site component. You build and test the logic once.
Practice
Try it yourself
Write a Flow screen component with an input
questionand an outputanswer("Yes"/"No") set by two buttons.Show solution
<!-- yesNo.html --> <template> <p>{question}</p> <lightning-button label="Yes" onclick={yes}></lightning-button> <lightning-button label="No" onclick={no}></lightning-button> </template>// yesNo.js import { LightningElement, api } from 'lwc'; import { FlowAttributeChangeEvent } from 'lightning/flowSupport'; export default class YesNo extends LightningElement { @api question; @api answer; yes() { this.dispatchEvent(new FlowAttributeChangeEvent('answer', 'Yes')); } no() { this.dispatchEvent(new FlowAttributeChangeEvent('answer', 'No')); } }Renew the contract? [Yes] [No] › click “Yes” Renew the contract? [Yes] [No] flow output answer = "Yes"
Quick check
How does a flow screen component update a flow variable?
Dispatch FlowAttributeChangeEvent(propertyName, value).
What does Flow call before going to the next screen?
The component's @api validate(), which returns isValid and an errorMessage.
Which method does Salesforce call for a headless quick action?
@api invoke().
Summary
- Flow screen components: @api inputs/outputs, FlowAttributeChangeEvent, and validate().
- Quick actions: ScreenAction renders in a modal; headless actions implement invoke().
- Experience Cloud needs its own targets, guest-user security and lightning/toast.
Finished this lesson?
Mark it complete to track your progress, then carry on with the next lesson.