Part 90: Building LWC for Quick Actions and Global Actions
Welcome back to the Salesforce blog series. If you have been following along since the early admin-focused posts, you might remember Part 6 where we covered quick actions from a declarative, point-and-click perspective. Back then, we were creating actions using standard Salesforce configuration — picking fields, choosing record types, and dropping actions onto page layouts. Now that we are deep into Topic 3 and building Lightning Web Components, it is time to revisit quick actions and global actions from a developer’s perspective. And in Part 87, we explored targeting and how to control where your components appear. Quick actions are one of the most practical applications of that knowledge.
Quick actions and global actions are one of the most common ways end users interact with custom functionality in Salesforce. That “New Case” button on an Account record? That is a quick action. The “Log a Call” button in the global header? That is a global action. As a developer, being able to build LWCs that power these actions gives you a tremendous amount of control over the user experience. You can validate data before submission, pre-populate fields based on context, call Apex on the fly, and create interfaces that go far beyond what a standard action can offer.
In this post, we will cover what quick actions and global actions are, how to build an LWC for each type, how to handle record context, and finish with a hands-on project where you build a reusable global action for creating cases.
What Are Quick Actions and Global Actions?
Before we write any code, let us make sure the terminology is clear.
A quick action is an action that lives on a specific object. It appears on record pages for that object — for example, an action on the Account object will only show up when a user is viewing an Account record. Quick actions have access to the record context, meaning your component automatically knows which record the user is looking at. This is incredibly useful because you can read fields from that record, create related records, or perform operations that are contextual to the data the user is working with.
A global action is an action that is not tied to any specific object. It lives in the global actions menu in the Salesforce header — that utility bar area at the top of every page. Because global actions are not tied to a record, they do not automatically receive record context. They are designed for tasks that make sense from anywhere in the app, like logging a call, creating a new task, or in our project later, creating a case from scratch.
Both quick actions and global actions can be powered by LWCs. In Salesforce terminology, when an LWC opens as a modal dialog for the user to interact with, it is called a screen action. There are also headless actions where the LWC runs logic without displaying a UI, but screen actions are by far the more common use case and what we will focus on here.
The key difference to remember: quick actions get record context for free, global actions do not. This single distinction drives most of the architectural decisions you will make when building action components.
How to Create an LWC for a Quick Action
Let us build a quick action that allows a user to update the rating on an Account record. This is a simple example, but it covers all the foundational concepts you need.
The HTML Template
<!-- updateAccountRating.html -->
<template>
<lightning-quick-action-panel header="Update Account Rating">
<div class="slds-m-around_medium">
<lightning-combobox
name="rating"
label="Account Rating"
value={selectedRating}
options={ratingOptions}
onchange={handleRatingChange}>
</lightning-combobox>
</div>
<div slot="footer">
<lightning-button
variant="neutral"
label="Cancel"
onclick={handleCancel}>
</lightning-button>
<lightning-button
variant="brand"
label="Save"
onclick={handleSave}
class="slds-m-left_x-small">
</lightning-button>
</div>
</lightning-quick-action-panel>
</template>
Notice the lightning-quick-action-panel component. This is the wrapper you must use when building screen actions. It gives you the standard modal appearance with a header and a footer slot for your buttons. Without this wrapper, your action will not render correctly inside the Salesforce modal.
The JavaScript Controller
// updateAccountRating.js
import { LightningElement, api } from "lwc";
import { CloseActionScreenEvent } from "lightning/actions";
import updateRating from "@salesforce/apex/AccountActionController.updateRating";
import { ShowToastEvent } from "lightning/platformShowToastEvent";
export default class UpdateAccountRating extends LightningElement {
@api recordId;
selectedRating = "";
get ratingOptions() {
return [
{ label: "Hot", value: "Hot" },
{ label: "Warm", value: "Warm" },
{ label: "Cold", value: "Cold" },
];
}
handleRatingChange(event) {
this.selectedRating = event.detail.value;
}
handleCancel() {
this.dispatchEvent(new CloseActionScreenEvent());
}
async handleSave() {
try {
await updateRating({
accountId: this.recordId,
rating: this.selectedRating,
});
this.dispatchEvent(
new ShowToastEvent({
title: "Success",
message: "Account rating updated.",
variant: "success",
})
);
this.dispatchEvent(new CloseActionScreenEvent());
} catch (error) {
this.dispatchEvent(
new ShowToastEvent({
title: "Error",
message: error.body.message,
variant: "error",
})
);
}
}
}
There are a few critical things happening here. First, the @api recordId property is automatically populated by Salesforce because this component is running as a quick action on a record page. You do not need to do anything special to get the record ID — the framework hands it to you. Second, the CloseActionScreenEvent is the standard way to close the action modal. You import it from lightning/actions and dispatch it whenever you want the modal to dismiss. Third, we are calling an Apex method to perform the actual update. You could also use the Lightning Data Service with updateRecord, but calling Apex gives you more control over validation and error handling.
The XML Configuration
<?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__RecordAction</target>
</targets>
<targetConfigs>
<targetConfig targets="lightning__RecordAction">
<actionType>ScreenAction</actionType>
</targetConfig>
</targetConfigs>
</LightningComponentBundle>
The target lightning__RecordAction is what tells Salesforce this component can be used as a quick action. The actionType of ScreenAction means it will open as a modal dialog. If you recall from Part 87, the targets in your XML metadata file control where your component can appear. For quick actions, this specific target and action type configuration is required.
After deploying, you create the action in Setup by going to Object Manager, selecting your object (Account in this case), navigating to Buttons, Links, and Actions, and creating a new action. Choose “Lightning Web Component” as the action type, select your component, and then add the action to the page layout. Once it is on the layout, users will see it as a button on the record page.
How to Create an LWC for a Global Action
Global actions work similarly, but with a few important differences. Let us build a global action that creates a new Task record from anywhere in the app.
The HTML Template
<!-- createGlobalTask.html -->
<template>
<lightning-quick-action-panel header="Create New Task">
<div class="slds-m-around_medium">
<lightning-input
label="Subject"
value={subject}
onchange={handleSubjectChange}
required>
</lightning-input>
<lightning-textarea
label="Description"
value={description}
onchange={handleDescriptionChange}
class="slds-m-top_small">
</lightning-textarea>
<lightning-combobox
name="priority"
label="Priority"
value={priority}
options={priorityOptions}
onchange={handlePriorityChange}
class="slds-m-top_small">
</lightning-combobox>
</div>
<div slot="footer">
<lightning-button
variant="neutral"
label="Cancel"
onclick={handleCancel}>
</lightning-button>
<lightning-button
variant="brand"
label="Create Task"
onclick={handleCreate}
class="slds-m-left_x-small">
</lightning-button>
</div>
</lightning-quick-action-panel>
</template>
The JavaScript Controller
// createGlobalTask.js
import { LightningElement } from "lwc";
import { CloseActionScreenEvent } from "lightning/actions";
import createTask from "@salesforce/apex/GlobalTaskController.createTask";
import { ShowToastEvent } from "lightning/platformShowToastEvent";
export default class CreateGlobalTask extends LightningElement {
subject = "";
description = "";
priority = "Normal";
get priorityOptions() {
return [
{ label: "High", value: "High" },
{ label: "Normal", value: "Normal" },
{ label: "Low", value: "Low" },
];
}
handleSubjectChange(event) {
this.subject = event.target.value;
}
handleDescriptionChange(event) {
this.description = event.target.value;
}
handlePriorityChange(event) {
this.priority = event.detail.value;
}
handleCancel() {
this.dispatchEvent(new CloseActionScreenEvent());
}
async handleCreate() {
if (!this.subject) {
this.dispatchEvent(
new ShowToastEvent({
title: "Error",
message: "Subject is required.",
variant: "error",
})
);
return;
}
try {
await createTask({
subject: this.subject,
description: this.description,
priority: this.priority,
});
this.dispatchEvent(
new ShowToastEvent({
title: "Success",
message: "Task created successfully.",
variant: "success",
})
);
this.dispatchEvent(new CloseActionScreenEvent());
} catch (error) {
this.dispatchEvent(
new ShowToastEvent({
title: "Error",
message: error.body.message,
variant: "error",
})
);
}
}
}
Notice the biggest difference here: there is no @api recordId. Because this is a global action, there is no record context. The component does not know what record the user was looking at when they clicked the action, and that is by design. If you need to associate the task with a specific record, you would need to add a lookup field to the form and let the user search for it.
The XML Configuration
<?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__GlobalAction</target>
</targets>
<targetConfigs>
<targetConfig targets="lightning__GlobalAction">
<actionType>ScreenAction</actionType>
</targetConfig>
</targetConfigs>
</LightningComponentBundle>
The only change in the XML is the target: lightning__GlobalAction instead of lightning__RecordAction. This tells Salesforce the component is available as a global action. To deploy it, you go to Setup, search for Global Actions, create a new action, select Lightning Web Component as the type, pick your component, and then add it to the Publisher Global Actions layout.
One thing worth mentioning: a single LWC can target both lightning__RecordAction and lightning__GlobalAction if you want the same component to work in both contexts. In your JavaScript, you would conditionally check whether recordId is populated to determine which context you are running in. This is a powerful pattern for building truly reusable components.
Section Notes
Here are the key points to remember about building LWCs for quick actions and global actions:
- Use
lightning-quick-action-panelas the root element of any screen action. It provides the standard modal header and footer slot. CloseActionScreenEventis the standard way to close an action modal. Import it fromlightning/actionsand dispatch it from your component.- Quick actions get
recordIdautomatically. Declare it with@api recordIdand the framework populates it for you. - Global actions do not receive record context. You must design your UI to collect any necessary information from the user.
- Use
lightning__RecordActionfor quick actions andlightning__GlobalActionfor global actions in your XML targets. - Always set
actionTypetoScreenActionin your target config when building actions that display a UI. Headless actions useActioninstead. - Validation matters. Because you control the entire form, you are responsible for validating input before submitting. Use
reportValidity()on input components or write custom validation logic. - Toast messages are your friend. Always provide feedback to the user after a successful operation or when something goes wrong. Import
ShowToastEventfromlightning/platformShowToastEvent. - A single component can target both action types by including both targets in the XML. Check for the presence of
recordIdto determine the current context.
PROJECT: Create a Reusable LWC Global Action That Allows You to Auto Create a Case from a Record
Let us put everything together with a practical project. We are going to build a global action that creates a Case record. But here is the twist — even though it is a global action, we are going to make it smart enough to accept an optional record ID through a design attribute, so it can also be used as a quick action that pre-fills the Account on the case. This makes it truly reusable.
The Apex Controller
First, we need an Apex class to handle the case creation.
public with sharing class CaseCreationController {
@AuraEnabled
public static Case createCase(String subject, String description,
String priority, String accountId) {
Case newCase = new Case();
newCase.Subject = subject;
newCase.Description = description;
newCase.Priority = priority;
newCase.Origin = 'Web';
newCase.Status = 'New';
if (String.isNotBlank(accountId)) {
newCase.AccountId = accountId;
}
insert newCase;
return newCase;
}
@AuraEnabled(cacheable=true)
public static Account getAccountName(String accountId) {
if (String.isBlank(accountId)) {
return null;
}
return [SELECT Id, Name FROM Account
WHERE Id = :accountId LIMIT 1];
}
}
The Apex is straightforward. The createCase method accepts an optional accountId. If provided, it links the case to that account. The getAccountName method lets the component display the account name when record context is available.
The HTML Template
<!-- quickCaseCreator.html -->
<template>
<lightning-quick-action-panel header="Create New Case">
<div class="slds-m-around_medium">
<template lwc:if={accountName}>
<div class="slds-box slds-box_x-small slds-m-bottom_small
slds-theme_shade">
<p class="slds-text-body_small">
Creating case for: <strong>{accountName}</strong>
</p>
</div>
</template>
<lightning-input
label="Subject"
value={subject}
onchange={handleFieldChange}
data-field="subject"
required>
</lightning-input>
<lightning-textarea
label="Description"
value={description}
onchange={handleFieldChange}
data-field="description"
class="slds-m-top_small">
</lightning-textarea>
<lightning-combobox
name="priority"
label="Priority"
value={priority}
options={priorityOptions}
onchange={handleFieldChange}
data-field="priority"
class="slds-m-top_small">
</lightning-combobox>
<template lwc:if={!accountName}>
<lightning-record-picker
label="Account"
placeholder="Search for an account..."
object-api-name="Account"
onchange={handleAccountSelect}
class="slds-m-top_small">
</lightning-record-picker>
</template>
</div>
<div slot="footer">
<lightning-button
variant="neutral"
label="Cancel"
onclick={handleCancel}>
</lightning-button>
<lightning-button
variant="brand"
label="Create Case"
onclick={handleCreateCase}
disabled={isSubmitting}
class="slds-m-left_x-small">
</lightning-button>
</div>
</lightning-quick-action-panel>
</template>
The template is designed to adapt to its context. If an account name is available (meaning we have record context), it displays an info box showing which account the case will be linked to and hides the account search field. If there is no account context, it shows a lightning-record-picker so the user can search for and select an account manually. This conditional rendering is what makes the component reusable across both action types.
The JavaScript Controller
// quickCaseCreator.js
import { LightningElement, api, wire } from "lwc";
import { CloseActionScreenEvent } from "lightning/actions";
import createCase from "@salesforce/apex/CaseCreationController.createCase";
import getAccountName from "@salesforce/apex/CaseCreationController.getAccountName";
import { ShowToastEvent } from "lightning/platformShowToastEvent";
export default class QuickCaseCreator extends LightningElement {
@api recordId;
subject = "";
description = "";
priority = "Medium";
selectedAccountId = "";
accountName = "";
isSubmitting = false;
get priorityOptions() {
return [
{ label: "High", value: "High" },
{ label: "Medium", value: "Medium" },
{ label: "Low", value: "Low" },
];
}
@wire(getAccountName, { accountId: "$recordId" })
wiredAccount({ data, error }) {
if (data) {
this.accountName = data.Name;
this.selectedAccountId = data.Id;
} else if (error) {
this.accountName = "";
}
}
handleFieldChange(event) {
const field = event.target.dataset.field;
if (field === "priority") {
this[field] = event.detail.value;
} else {
this[field] = event.target.value;
}
}
handleAccountSelect(event) {
this.selectedAccountId = event.detail.recordId || "";
}
handleCancel() {
this.dispatchEvent(new CloseActionScreenEvent());
}
async handleCreateCase() {
if (!this.subject) {
this.dispatchEvent(
new ShowToastEvent({
title: "Validation Error",
message: "Subject is required to create a case.",
variant: "error",
})
);
return;
}
this.isSubmitting = true;
try {
const result = await createCase({
subject: this.subject,
description: this.description,
priority: this.priority,
accountId: this.selectedAccountId,
});
this.dispatchEvent(
new ShowToastEvent({
title: "Case Created",
message: `Case ${result.CaseNumber || ""} created successfully.`,
variant: "success",
})
);
this.dispatchEvent(new CloseActionScreenEvent());
} catch (error) {
this.dispatchEvent(
new ShowToastEvent({
title: "Error Creating Case",
message: error.body
? error.body.message
: "An unexpected error occurred.",
variant: "error",
})
);
} finally {
this.isSubmitting = false;
}
}
}
The @wire decorator calls getAccountName reactively. When recordId is populated (quick action context), the wire fetches the account name and stores the ID. When recordId is undefined (global action context), the wire simply does not fire, and the user gets the manual account picker instead. The handleFieldChange method uses data-field attributes to handle multiple inputs with a single handler — a clean pattern that reduces boilerplate. The isSubmitting flag disables the button during submission to prevent duplicate case creation.
The XML Configuration
<?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__RecordAction</target>
<target>lightning__GlobalAction</target>
</targets>
<targetConfigs>
<targetConfig targets="lightning__RecordAction">
<actionType>ScreenAction</actionType>
</targetConfig>
<targetConfig targets="lightning__GlobalAction">
<actionType>ScreenAction</actionType>
</targetConfig>
</targetConfigs>
</LightningComponentBundle>
This XML includes both targets, which is the key to making the component truly reusable. You deploy it once and create two separate actions from it — one quick action on the Account object and one global action in the global actions menu. The same code, the same logic, but the user experience adapts based on context.
To test it, deploy the component and the Apex class to your org. Create a quick action on the Account object and a global action in Setup. Add both to their respective layouts. When you open the quick action from an Account record, you should see the account name displayed and no account picker. When you open the global action from the header, you should see the account picker field appear. Both paths should successfully create a case.
This pattern of building context-aware, dual-purpose action components is something you will use over and over in real-world Salesforce development. It reduces code duplication, simplifies maintenance, and gives your users a consistent experience regardless of where they trigger the action.