Part 91: LWC and Flows in Salesforce
If you have been following this series, you have already spent a lot of time in two separate worlds. Back in Part 24, we covered Flows in depth — building automations, screen flows, record-triggered flows, and understanding when to use declarative tools versus code. Then starting in Part 72 with LWC, we have been deep in the code world — building components, handling events, calling Apex, and managing state. What we have not done yet is connect those two worlds together.
That connection is one of the most powerful patterns on the Salesforce platform. Flows give you declarative logic, branching, and easy screen assembly that admins can maintain. LWC gives you full control over the user interface — custom inputs, dynamic rendering, API calls, and polished user experiences that are impossible with standard flow screen components alone. When you combine them, you get flows that look and behave like custom applications, but with all the benefits of the flow engine handling navigation, data operations, and business logic behind the scenes.
In Part 87, we touched on targeting LWC components for flow screens. This post goes much deeper. We will cover how to build an LWC that works inside a screen flow, how to embed an entire flow inside an LWC, when each approach makes sense, and we will finish with a project that ties everything together.
This post covers:
- How to Create an LWC for a Screen Flow — Building a component that participates in flow navigation and data passing.
- How to Incorporate a Screen Flow into Your LWC — Embedding a flow inside a Lightning Web Component.
- Useful Scenarios for LWC’s to Be Incorporated into Flows — Real-world use cases where combining these tools solves problems.
- Section Notes — Key takeaways and patterns to remember.
- PROJECT — Create a multi-screen screen flow that uses a reusable dynamic LWC in both screens.
Let’s get into it.
How to Create an LWC for a Screen Flow
When you build an LWC for a screen flow, you are creating a component that the flow engine treats like any other screen component. The flow can pass data into your component through input attributes, and your component can pass data back to the flow through output attributes. The flow handles navigation — moving between screens, going back, finishing — and your component handles the user interface on a particular screen.
The key to making this work is the component’s XML configuration file. You have to explicitly declare that your component is available for flow screens, and you have to define which properties are inputs, which are outputs, and which are both.
The XML Configuration
Here is a basic XML configuration that makes a component available in screen flows:
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>59.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__FlowScreen</target>
</targets>
<targetConfigs>
<targetConfig targets="lightning__FlowScreen">
<property name="label" type="String" label="Field Label" role="inputOnly" />
<property name="value" type="String" label="Field Value" role="outputOnly" />
<property name="required" type="Boolean" label="Required" default="false" role="inputOnly" />
</targetConfig>
</targetConfigs>
</LightningComponentBundle>
There are a few things to notice here. The target is set to lightning__FlowScreen, which tells the platform this component should appear in the Flow Builder when admins are adding components to a screen element. The targetConfig section defines the properties and their roles. The role attribute is what controls the direction of data flow:
inputOnlymeans the flow can pass a value into the component, but the component cannot send a value back through that property.outputOnlymeans the component sends a value back to the flow, but the flow cannot set an initial value.- If you omit the
roleattribute entirely, the property is bidirectional — the flow can set it, and the component can update it.
Supported types for flow properties include String, Boolean, Integer, Date, DateTime, and ContentReference. You can also use @salesforce/schema references for sobject types, but for most use cases, the primitive types are what you need.
The JavaScript Controller
On the JavaScript side, every property you defined in the XML must be decorated with @api so the flow engine can read from and write to it. Here is a simple component that acts as a custom text input for a flow:
import { LightningElement, api } from 'lwc';
export default class FlowCustomInput extends LightningElement {
@api label = '';
@api value = '';
@api required = false;
handleChange(event) {
this.value = event.detail.value;
}
@api
validate() {
if (this.required && (!this.value || this.value.trim() === '')) {
return {
isValid: false,
errorMessage: `${this.label} is required. Please enter a value before continuing.`
};
}
return { isValid: true };
}
}
The validate() method is important. The flow engine calls this method automatically when the user clicks Next or Finish on a screen. If your method returns { isValid: false } with an error message, the flow will not advance and will display your error message. If it returns { isValid: true }, the flow proceeds normally. This gives you custom validation logic that is far more flexible than what standard flow validation rules can do.
Notice that the validate method must be decorated with @api. If you forget that decorator, the flow engine cannot call it, and your validation will silently never run.
The HTML Template
The template can be whatever you need:
<template>
<div class="slds-form-element">
<label class="slds-form-element__label">
<template if:true={required}>
<abbr class="slds-required" title="required">*</abbr>
</template>
{label}
</label>
<div class="slds-form-element__control">
<lightning-input
type="text"
value={value}
onchange={handleChange}
variant="label-hidden"
></lightning-input>
</div>
</div>
</template>
Once you deploy this component, it will appear in Flow Builder under the custom component section when you add components to a screen element. The admin can map flow variables to the input and output properties, and the component will behave as a first-class participant in the flow.
Handling Flow Navigation Events
In most cases, you let the flow handle navigation — the standard Next, Previous, and Finish buttons on the screen do the work. But there are situations where you want your component to trigger navigation programmatically. You can do this by dispatching a FlowNavigationNextEvent, FlowNavigationBackEvent, FlowNavigationFinishEvent, or FlowNavigationPauseEvent.
import { LightningElement, api } from 'lwc';
import { FlowNavigationNextEvent } from 'lightning/flowSupport';
export default class FlowAutoAdvance extends LightningElement {
@api selectedValue;
handleSelection(event) {
this.selectedValue = event.detail.value;
const nextEvent = new FlowNavigationNextEvent();
this.dispatchEvent(nextEvent);
}
}
When the user makes a selection, the component updates the output property and immediately advances the flow to the next screen. This is great for wizard-like experiences where selecting an option should immediately move forward without requiring the user to click Next.
You need to import these events from the lightning/flowSupport module. The available events are:
FlowNavigationNextEvent— moves to the next screen.FlowNavigationBackEvent— moves to the previous screen.FlowNavigationFinishEvent— finishes the flow.FlowNavigationPauseEvent— pauses the flow.
One caveat: when you dispatch a navigation event, the flow engine still calls your validate() method first. If validation fails, the navigation will not happen. This is actually a good safety net — it means you cannot accidentally skip past required data.
How to Incorporate a Screen Flow into Your LWC
The other direction is equally useful. Instead of putting an LWC inside a flow, you can embed an entire flow inside an LWC. This is done using the lightning-flow base component.
Why would you do this? Because sometimes you have a complex LWC page — maybe a custom record page or a community page — and you want one section of it to run a flow. The flow handles a specific process (like a guided intake form or an approval workflow), while the rest of the page is controlled by your LWC. You get the best of both worlds: custom UI where you need it, and declarative flow logic where that makes more sense.
Basic Flow Embedding
Here is the simplest version:
<template>
<lightning-card title="Complete Your Intake">
<div class="slds-p-around_medium">
<lightning-flow
flow-api-name="Intake_Form_Flow"
onstatuschange={handleFlowStatusChange}
></lightning-flow>
</div>
</lightning-card>
</template>
import { LightningElement } from 'lwc';
export default class IntakeFlowContainer extends LightningElement {
handleFlowStatusChange(event) {
if (event.detail.status === 'FINISHED') {
this.handleFlowComplete(event.detail.outputVariables);
}
}
handleFlowComplete(outputVariables) {
const recordId = outputVariables.find(
v => v.name === 'createdRecordId'
);
if (recordId) {
console.log('Flow created record:', recordId.value);
}
}
}
The flow-api-name attribute takes the API name of the flow you want to run. The onstatuschange event fires whenever the flow’s status changes. The most important statuses are:
STARTED— the flow has begun.PAUSED— the user paused the flow.FINISHED— the flow completed successfully.FINISHED_SCREEN— the flow finished and was on a screen (relevant for some navigation patterns).ERROR— something went wrong.
When the flow finishes, event.detail.outputVariables gives you an array of the flow’s output variables. Each entry has a name, value, and dataType. This is how you get data back from the flow into your LWC.
Passing Input Variables to the Flow
You can also pass data into the flow when it starts:
<template>
<lightning-flow
flow-api-name="Account_Update_Flow"
flow-input-variables={inputVariables}
onstatuschange={handleFlowStatusChange}
></lightning-flow>
</template>
import { LightningElement, api } from 'lwc';
export default class AccountFlowLauncher extends LightningElement {
@api recordId;
get inputVariables() {
return [
{
name: 'recordId',
type: 'String',
value: this.recordId
},
{
name: 'source',
type: 'String',
value: 'CustomComponent'
}
];
}
handleFlowStatusChange(event) {
if (event.detail.status === 'FINISHED') {
this.dispatchEvent(new CustomEvent('flowcomplete'));
}
}
}
The flow-input-variables attribute takes an array of objects, each with a name (matching a flow input variable), a type, and a value. This lets you seed the flow with context from the surrounding LWC — a record ID, a user selection, or any other data your flow needs to operate.
Starting and Restarting the Flow Programmatically
The lightning-flow component renders and starts the flow automatically when it appears in the DOM. But sometimes you need to restart it or start it conditionally. You can show and hide the flow using a boolean flag:
<template>
<template if:true={showFlow}>
<lightning-flow
flow-api-name="Quick_Action_Flow"
flow-input-variables={inputVariables}
onstatuschange={handleFlowStatusChange}
></lightning-flow>
</template>
<template if:false={showFlow}>
<lightning-button label="Start Process" onclick={startFlow}></lightning-button>
</template>
</template>
import { LightningElement } from 'lwc';
export default class FlowToggle extends LightningElement {
showFlow = false;
startFlow() {
this.showFlow = true;
}
handleFlowStatusChange(event) {
if (event.detail.status === 'FINISHED') {
this.showFlow = false;
}
}
}
When showFlow is false, the flow is not in the DOM. Setting it to true renders the lightning-flow component, which starts the flow. When the flow finishes, you set showFlow back to false, removing it from the DOM. If the user clicks the button again, a fresh instance of the flow starts from the beginning. This pattern is clean and avoids stale state issues.
Useful Scenarios for LWC’s to Be Incorporated into Flows
Understanding the mechanics is one thing. Knowing when to use each approach is what makes you effective. Here are the scenarios where combining LWC and flows pays off.
Custom Data Entry Components
Standard flow screen components give you text inputs, picklists, checkboxes, and a few other basics. But what if you need a multi-select component with search, a file upload with preview, an address lookup that calls a geocoding API, or a rich text editor? Build those as LWCs and drop them into the flow. The admin still controls the flow logic and screen order. You just provide better UI components.
Dynamic Conditional Rendering
Flows have conditional visibility rules for screen components, but they are limited. With an LWC, you can build a single component that dynamically shows different fields, sections, or layouts based on complex conditions — previous answers, user permissions, record data, or external API responses. The component receives inputs from the flow and handles all the dynamic UI logic internally.
Real-Time Validation and Calculation
Suppose you have a pricing calculator in a flow. The user enters a quantity and a discount code, and you need to calculate the final price in real time, validate the discount code against an external system, and show the breakdown before the user proceeds. A standard flow screen cannot do this. An LWC can make imperative Apex calls, call external APIs, and update the display instantly as the user types.
Guided Processes Inside Custom Pages
When you have a custom Lightning page — maybe a community portal or an internal tool built with LWC — and one part of that page is a guided process with multiple steps, embedding a flow makes sense. The flow handles the step-by-step logic, branching, and data operations. The LWC handles everything else on the page. This way, admins can modify the guided process without touching code.
Reusable Components Across Multiple Flows
This is where the real leverage is. Build a single LWC — say, a contact search component or an address verification widget — and it can be used in dozens of different flows across the org. The component is built once, tested once, and maintained in one place. Each flow admin just drags it onto their screen and maps the variables. This is the pattern we will use in the project below.
Section Notes
A few things to keep in mind as you work with LWC and flows together.
The validate() method is your best friend for flow-embedded LWCs. It runs automatically on navigation, it can return custom error messages, and it prevents the user from advancing past incomplete or invalid data. Always decorate it with @api.
When embedding a flow inside an LWC, the onstatuschange event is the only reliable way to know what the flow is doing. You cannot reach into the flow and inspect its state. You get notified when the status changes, and you get output variables when it finishes. Design your integration around that event-driven model.
Property roles in the XML configuration matter. If you set a property to inputOnly and then try to update it in JavaScript to send data back to the flow, the flow will not receive the update. If you need bidirectional data flow, leave the role attribute off entirely.
Flow-embedded LWCs cannot use lightning-navigation to navigate to other pages. The flow controls navigation. If your component tries to navigate away, it breaks the flow’s state. Use the FlowNavigation events to move between screens and let the flow handle post-completion navigation using its own finish behavior settings.
Testing flow-embedded components requires deploying them to an org and testing inside an actual flow. You can unit test the JavaScript logic with Jest, but the flow integration — property binding, validation, navigation events — has to be tested manually or with tools like Salesforce’s flow debug mode. Use the Flow Builder’s debug feature to pass test values into your component and verify its behavior screen by screen.
Keep your flow-embedded LWCs focused on UI concerns. The flow should handle data operations (create, update, delete records), branching logic, and process orchestration. The LWC should handle user interaction, display logic, and validation. When you split responsibilities this way, both the flow and the component stay maintainable.
PROJECT: Create a Multi-Screen Screen Flow That Uses a Reusable Dynamic LWC in Both Screens
Let’s build something that puts all of this together. We are going to create a reusable LWC that acts as a dynamic field input — it can render as a text input, a number input, a date picker, or a picklist based on a configuration property. Then we will build a two-screen flow that uses this same component on both screens with different configurations.
Step 1: Build the Reusable Dynamic Input Component
dynamicFlowInput.js
import { LightningElement, api } from 'lwc';
export default class DynamicFlowInput extends LightningElement {
@api label = 'Input';
@api fieldType = 'text';
@api value = '';
@api required = false;
@api helpText = '';
@api picklistOptions = '';
get isText() {
return this.fieldType === 'text';
}
get isNumber() {
return this.fieldType === 'number';
}
get isDate() {
return this.fieldType === 'date';
}
get isPicklist() {
return this.fieldType === 'picklist';
}
get parsedPicklistOptions() {
if (!this.picklistOptions) return [];
try {
return this.picklistOptions.split(';').map(opt => {
const trimmed = opt.trim();
return { label: trimmed, value: trimmed };
});
} catch (e) {
return [];
}
}
handleChange(event) {
this.value = event.detail.value;
}
@api
validate() {
if (this.required && (!this.value || String(this.value).trim() === '')) {
return {
isValid: false,
errorMessage: `${this.label} is required.`
};
}
if (this.fieldType === 'number' && this.value) {
const num = Number(this.value);
if (isNaN(num)) {
return {
isValid: false,
errorMessage: `${this.label} must be a valid number.`
};
}
}
return { isValid: true };
}
}
dynamicFlowInput.html
<template>
<div class="slds-form-element">
<template if:true={helpText}>
<div class="slds-form-element__help slds-m-bottom_x-small">
{helpText}
</div>
</template>
<template if:true={isText}>
<lightning-input
label={label}
type="text"
value={value}
required={required}
onchange={handleChange}
></lightning-input>
</template>
<template if:true={isNumber}>
<lightning-input
label={label}
type="number"
value={value}
required={required}
onchange={handleChange}
></lightning-input>
</template>
<template if:true={isDate}>
<lightning-input
label={label}
type="date"
value={value}
required={required}
onchange={handleChange}
></lightning-input>
</template>
<template if:true={isPicklist}>
<lightning-combobox
label={label}
value={value}
options={parsedPicklistOptions}
required={required}
onchange={handleChange}
placeholder="Select an option..."
></lightning-combobox>
</template>
</div>
</template>
dynamicFlowInput.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>59.0</apiVersion>
<isExposed>true</isExposed>
<masterLabel>Dynamic Flow Input</masterLabel>
<description>A reusable input component that renders different field types based on configuration.</description>
<targets>
<target>lightning__FlowScreen</target>
</targets>
<targetConfigs>
<targetConfig targets="lightning__FlowScreen">
<property name="label" type="String" label="Field Label" role="inputOnly" default="Input" />
<property name="fieldType" type="String" label="Field Type (text, number, date, picklist)" role="inputOnly" default="text" />
<property name="value" type="String" label="Field Value" />
<property name="required" type="Boolean" label="Required" role="inputOnly" default="false" />
<property name="helpText" type="String" label="Help Text" role="inputOnly" />
<property name="picklistOptions" type="String" label="Picklist Options (semicolon-separated)" role="inputOnly" />
</targetConfig>
</targetConfigs>
</LightningComponentBundle>
Step 2: Build the Flow
Now create a screen flow in Flow Builder. Here is the configuration:
Flow Variables:
contactName(Text, available for input/output)contactEmail(Text, available for input/output)contactPriority(Text, available for input/output)contactBirthdate(Text, available for input/output)budgetAmount(Text, available for input/output)projectNotes(Text, available for input/output)
Screen 1: Contact Information
Add three instances of the Dynamic Flow Input component to the first screen:
- First instance — Label: “Full Name”, Field Type: “text”, Value:
{!contactName}, Required: true, Help Text: “Enter the contact’s full legal name.” - Second instance — Label: “Email Address”, Field Type: “text”, Value:
{!contactEmail}, Required: true, Help Text: “A valid email address for follow-up.” - Third instance — Label: “Priority Level”, Field Type: “picklist”, Value:
{!contactPriority}, Required: true, Picklist Options: “Low;Medium;High;Critical”, Help Text: “Select the priority for this contact request.”
Screen 2: Project Details
Add three more instances of the same component on the second screen:
- First instance — Label: “Birthdate”, Field Type: “date”, Value:
{!contactBirthdate}, Required: false, Help Text: “Optional. Used for personalized communications.” - Second instance — Label: “Budget Amount”, Field Type: “number”, Value:
{!budgetAmount}, Required: true, Help Text: “Estimated project budget in USD.” - Third instance — Label: “Project Notes”, Field Type: “text”, Value:
{!projectNotes}, Required: false, Help Text: “Any additional context about the project.”
After Screen 2:
Add a Create Records element that creates a Contact record using the collected data, or a custom object record if you prefer. Map the flow variables to the appropriate fields.
Step 3: Test the Flow
Open the flow in debug mode. On Screen 1, try clicking Next without filling in the required fields. The validate() method in each component instance will fire, and you should see the custom error messages. Fill in the fields and proceed to Screen 2. Notice that the same component is being used on both screens, but with different configurations — a date picker on one, a number input on another, a picklist on the first screen. The flow engine passes different property values to each instance.
After completing Screen 2, the flow finishes and creates the record. The single dynamicFlowInput component handled six different fields across two screens with three different input types, and the flow admin configured all of it without touching code.
This is the power of combining LWC and flows. You write one well-tested, flexible component. Admins reuse it everywhere. The flow handles the process. The component handles the experience. Everyone works in the layer that suits their skills.