LearnFrenzy
Sign in Take a free test

Forms, Flows and Production · Lesson 22 of 25

LWC in Flows, Quick Actions and Experience Cloud

6 min read Updated Nov 8, 2026 Saurabh Samir

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.' };
    }
}

Result, with the flow's side of the conversation

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>

What happens when the user clicks the action

(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

TopicWhat to know
TargetslightningCommunity__Page to appear in Experience Builder, lightningCommunity__Default for builder properties
Guest usersunauthenticated visitors have very limited access – design Apex and sharing for them explicitly
Navigationuse comm__namedPage page references; record pages work with the site's URLs
Stylingsites use their own theme; light DOM components can pick up site-wide CSS
Toastsprefer 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

  1. Write a Flow screen component with an input question and an output answer ("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'));
        }
    }

    Result

    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.

Next lesson

Popular