Custom Labels in Salesforce: Text You Can Change Without Code
Every Salesforce org has text that changes: error messages, button labels, terms-of-service URLs, environment-specific API endpoints. If those strings live in Apex code or JavaScript, changing them means a deployment. Custom Labels solve that. They are named string values stored as metadata, editable through Setup without touching code, and accessible from Apex, LWC, Flows, and Visualforce simultaneously.
What Custom Labels Are
A Custom Label is a key-value pair — a developer-defined name maps to a string value that can be up to 1,000 characters. The value is stored in org metadata, which means:
- Admins can change the value in Setup without a code deployment.
- The same label name resolves to different values for different languages if translations are configured.
- The label is available everywhere in the platform: Apex, Lightning Web Components, Flows, Visualforce, and formula fields.
Common use cases where Custom Labels pay off immediately:
- Error messages — centralise all user-facing exception text so admins can reword without a developer.
- UI strings — page titles, button text, confirmation messages that vary by org or locale.
- Configurable URLs — terms-of-service pages, help portal links, external API base URLs (non-sensitive).
- Environment flags — feature toggle descriptions, sandbox-vs-production messaging.
Creating a Custom Label
Navigate to Setup → Custom Labels → New. The fields that matter most are:
Name(the API name) — this is what you reference in code. It must be unique across the org and cannot be changed after creation.Short Description— shown in the Setup list; describe the label's purpose clearly.Value— the default string value returned when no language-specific override exists.Protected Component— whether the label is restricted to its defining package namespace (covered in the Protected Labels section below).Language— the language for the default value.Categories— optional comma-separated tags for filtering in the Setup list.
Naming convention matters at scale. Use a prefix that encodes the category:
Error_InvalidInput,Error_NoAccess,Error_RecordLocked— exception messagesUI_ConfirmDelete,UI_SaveSuccessful— component stringsURL_TermsPage,URL_HelpPortal— external linksConfig_MaxRetries,Config_BatchSize— configurable numeric strings
Avoid generic names like Message1 or Label_A. Six months later, nobody
knows what they're for — and you can't rename them.
Accessing Custom Labels in Apex
The syntax is System.Label.YourLabelName. The return type is String.
The System namespace qualifier is optional — Label.YourLabelName also
works — but including it makes the source clear at a glance.
// Minimal usage
String errorMsg = System.Label.Error_NoAccess;
throw new AuraHandledException(errorMsg);
Because the result is a plain String, you can use it anywhere a String
fits: concatenation, String.format(), exception constructors, log messages.
The following example shows a validation service class that uses Custom Labels for every user-facing message. The labels referenced are:
Error_AccountNameRequired— value: "Account name is required."Error_InvalidEmail— value: "The email address format is not valid."Error_NoEditAccess— value: "You do not have permission to edit this record."Error_DuplicateAccount— value: "An account with this name already exists."
public with sharing class AccountValidationService {
public static void validateBeforeSave(Account acc) {
if (String.isBlank(acc.Name)) {
throw new AuraHandledException(System.Label.Error_AccountNameRequired);
}
if (String.isNotBlank(acc.Email__c) && !isValidEmail(acc.Email__c)) {
throw new AuraHandledException(System.Label.Error_InvalidEmail);
}
if (!Schema.SObjectType.Account.isUpdateable()) {
throw new AuraHandledException(System.Label.Error_NoEditAccess);
}
List<Account> dupes = [
SELECT Id
FROM Account
WHERE Name = :acc.Name
AND Id != :acc.Id
LIMIT 1
];
if (!dupes.isEmpty()) {
throw new AuraHandledException(System.Label.Error_DuplicateAccount);
}
}
private static Boolean isValidEmail(String email) {
String emailRegex = '^[a-zA-Z0-9._|\\\\%#~`=?&/$^*!{}_+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z]{2,4}$';
Pattern emailPattern = Pattern.compile(emailRegex);
return emailPattern.matcher(email).matches();
}
}
If an admin rewrites Error_AccountNameRequired from "Account name is required."
to "Please enter a name for this account.", the change is live the moment they save it in
Setup — no deployment, no release window.
Labels in Test Classes
Custom Labels are read directly in test context the same way they are in production. No special setup is needed, and they do not count against SOQL limits.
@IsTest
private class AccountValidationServiceTest {
@IsTest
static void blankName_throwsExpectedMessage() {
Account acc = new Account(Name = '');
try {
AccountValidationService.validateBeforeSave(acc);
Assert.fail('Expected exception was not thrown');
} catch (AuraHandledException e) {
Assert.areEqual(System.Label.Error_AccountNameRequired, e.getMessage());
}
}
}
Accessing Custom Labels in Lightning Web Components
LWC uses static imports. Each label requires its own import statement at the top of
the JavaScript file. The module path is @salesforce/label/c.YourLabelName for labels
in the default namespace; replace c with your namespace prefix if using a managed
package namespace.
import LABEL_NAME from '@salesforce/label/c.Label_Api_Name';
The imported value is a String. Assign it to a class property to expose it in the
template.
The following complete LWC component displays an account edit form and uses Custom Labels for all visible strings. The labels used are:
UI_EditAccountTitle— value: "Edit Account"UI_AccountNamePlaceholder— value: "Enter account name"UI_SaveButton— value: "Save"UI_CancelButton— value: "Cancel"Error_AccountNameRequired— value: "Account name is required."
accountEditForm.html
<template>
<lightning-card title={labels.editAccountTitle}>
<div class="slds-p-around_medium">
<template lwc:if={errorMessage}>
<p class="slds-text-color_error">{errorMessage}</p>
</template>
<lightning-input
label="Name"
value={accountName}
placeholder={labels.namePlaceholder}
onchange={handleNameChange}>
</lightning-input>
<div class="slds-m-top_medium">
<lightning-button
label={labels.saveButton}
variant="brand"
onclick={handleSave}>
</lightning-button>
<lightning-button
label={labels.cancelButton}
class="slds-m-left_x-small"
onclick={handleCancel}>
</lightning-button>
</div>
</div>
</lightning-card>
</template>
accountEditForm.js
import { LightningElement, api } from 'lwc';
import LABEL_EDIT_TITLE from '@salesforce/label/c.UI_EditAccountTitle';
import LABEL_PLACEHOLDER from '@salesforce/label/c.UI_AccountNamePlaceholder';
import LABEL_SAVE from '@salesforce/label/c.UI_SaveButton';
import LABEL_CANCEL from '@salesforce/label/c.UI_CancelButton';
import LABEL_NAME_REQUIRED from '@salesforce/label/c.Error_AccountNameRequired';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import saveAccount from '@salesforce/apex/AccountValidationService.saveAccount';
export default class AccountEditForm extends LightningElement {
@api recordId;
accountName = '';
errorMessage = '';
labels = {
editAccountTitle : LABEL_EDIT_TITLE,
namePlaceholder : LABEL_PLACEHOLDER,
saveButton : LABEL_SAVE,
cancelButton : LABEL_CANCEL
};
handleNameChange(event) {
this.accountName = event.target.value;
this.errorMessage = '';
}
async handleSave() {
if (!this.accountName.trim()) {
this.errorMessage = LABEL_NAME_REQUIRED;
return;
}
try {
await saveAccount({ recordId: this.recordId, name: this.accountName });
this.dispatchEvent(
new ShowToastEvent({ title: 'Saved', variant: 'success' })
);
} catch (error) {
this.errorMessage = error.body?.message ?? 'An unexpected error occurred.';
}
}
handleCancel() {
this.dispatchEvent(new CustomEvent('cancel'));
}
}
A few important points about LWC label imports:
- The imported constant is always a
String— it resolves at runtime to the current user's language if translations exist. - You cannot import labels dynamically by composing the label name at runtime. The module specifier must be a static string literal.
- Labels used inside JavaScript (not just the template) do not need to be assigned to a class property — the imported constant is directly usable:
this.errorMessage = LABEL_NAME_REQUIRED;
Accessing Custom Labels in Flows
Flows use merge field syntax. The global variable for Custom Labels is $Label, and
the full reference for a label in the default namespace is:
{!$Label.c.Your_Label_Name}
Replace c with your namespace prefix for managed packages. This merge field works in:
- Screen components — Display Text components, input field labels, and help text fields all accept merge fields.
- Decision element conditions — compare a variable to a label value without hardcoding the string in the Flow itself.
- Assignment elements — assign a label's value to a text variable for downstream use.
- Custom error messages — fault path messages shown to users when a record operation fails.
Example: a Screen Flow that shows a localised welcome message uses a Display Text component with the value:
{!$Label.c.UI_WelcomeMessage}
If the running user's language is French and a French translation exists for
UI_WelcomeMessage, they see the French version automatically — no conditional
logic in the Flow required.
Translations
Custom Labels support per-language overrides through the Translation Workbench. Enable it first at Setup → Translation Workbench → Enable, then navigate to Translate → Setup Component: Custom Label → Language → select your target language.
The resolution order at runtime is:
- Look for a translation matching the running user's language and locale.
- If none exists, fall back to the organisation's default language.
- If still not found, use the label's default
Value.
In Apex, this resolution happens automatically — System.Label.My_Label returns the
correct language string for the current user without any extra code. The same is true for LWC
imports and Flow merge fields.
This means a single label name like Error_AccountNameRequired can hold:
- English: "Account name is required."
- French: "Le nom du compte est obligatoire."
- German: "Der Kontoname ist erforderlich."
Your Apex and LWC code references the same label name in all cases. Translators work entirely in Setup.
Protected Custom Labels
When creating a Custom Label, the Protected Component checkbox controls namespace
visibility:
- Protected = false (default) — the label is accessible by any code in the org, including unmanaged code installed after your package.
- Protected = true — the label is only accessible from code within the same managed package namespace. Subscriber orgs cannot read or reference it from their own Apex or LWC.
This distinction only matters if you are building a managed package for distribution on AppExchange. For single-org development, leave it unchecked.
If your managed package has internal configuration strings — error codes, internal API paths, feature toggle names — that you do not want subscriber developers depending on, mark them protected. If a subscriber's code attempted to reference a protected label outside your namespace, it would result in a compile error.
Metadata Deployment
Custom Labels are metadata and deploy like any other metadata component. The metadata type is
CustomLabel, and labels live in the labels/ directory in a Salesforce
DX project:
force-app/
└── main/
└── default/
└── labels/
└── CustomLabels.labels-meta.xml
All Custom Labels for the org or package live in a single file. A typical entry looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<CustomLabels xmlns="http://soap.sforce.com/2006/04/metadata">
<labels>
<fullName>Error_AccountNameRequired</fullName>
<language>en_US</language>
<protected>false</protected>
<shortDescription>Validation error when account name is blank</shortDescription>
<value>Account name is required.</value>
</labels>
<labels>
<fullName>URL_TermsPage</fullName>
<language>en_US</language>
<protected>false</protected>
<shortDescription>URL for the external terms of service page</shortDescription>
<value>https://example.com/terms</value>
</labels>
</CustomLabels>
Deploy with sf project deploy start or include in your change set. Retrieve labels
with sf project retrieve start -m CustomLabel.
Best Practices
One label per logical message
Do not split a sentence across multiple labels and concatenate them. Translations do not preserve word order across languages — a sentence that reads correctly in English when concatenated from two labels may be grammatically wrong in German or Japanese. Each complete user-facing string is one label.
Never store sensitive data
Custom Labels are org metadata. Any user who can access Setup → Custom Labels can read every label value. Do not store API tokens, passwords, client secrets, or any value you would not want visible to a Salesforce admin. Use Named Credentials or Custom Settings (with proper field-level security) for sensitive configuration.
Centralise all user-facing strings
If text appears in the UI or in an exception message, it belongs in a Custom Label. This is especially true for error messages: hardcoded strings scattered across fifty Apex classes are a maintenance problem. When legal asks to change the wording of a consent message, one label update beats a search-and-replace across the codebase.
Use for ops-changeable configuration
Anything that a non-developer might need to adjust between deployments — a help page URL, a maximum item count shown in a component, a product name that marketing keeps renaming — belongs in a Custom Label. The cost to create one is one minute in Setup. The savings when you do not have to deploy a change just to update a string are real.
Name labels to survive turnover
API names are permanent. Error_NoAccess is self-documenting six months after the
developer who created it has left the team. Label42 is not. Invest thirty seconds
in a clear name now.
Quick Reference
| Context | Syntax |
|---|---|
| Apex | System.Label.Your_Label_Name |
| LWC import | import LBL from '@salesforce/label/c.Your_Label_Name'; |
| LWC template | {labelProperty} (assigned from the import) |
| Flow merge field | {!$Label.c.Your_Label_Name} |
| Visualforce | {!$Label.Your_Label_Name} |
| Formula field | $Label.c.Your_Label_Name |
Custom Labels are the correct solution whenever you have a string that lives at the boundary between code and configuration — anything a developer writes today that an admin or ops team might need to change tomorrow. Create them with a clear naming convention, keep one complete message per label, never store secrets in them, and leverage the built-in translation mechanism rather than building your own language-switching logic. The platform handles the rest.
Frequently Asked Questions
Can a Custom Label's API name be changed after it's created?
No — once a Custom Label is saved, its API name is permanent. Any code, Flow, or component referencing the old name would break if a rename were possible, so Salesforce locks it. Choose the name carefully upfront, using a category prefix like Error_, UI_, or URL_ to keep the org organized long-term.
What is the Protected Component setting on a Custom Label?
When Protected is checked, the label is restricted to the package namespace that defined it and cannot be referenced by code outside that package. This matters for ISV partners building managed packages — it prevents subscriber orgs from directly accessing or overriding internal labels. For labels in an unpackaged org, the setting has no practical effect.
Are Custom Labels a safe place to store API keys or passwords?
No — Custom Label values are stored as plain-text metadata and are visible to anyone with Setup access or access to the org's metadata. Sensitive credentials should be stored in Named Credentials or Custom Settings/Custom Metadata with restricted access. Custom Labels are appropriate for non-sensitive configuration strings like endpoint base URLs, feature descriptions, or user-facing messages.
How do Custom Labels handle multiple languages in a multilingual org?
Each Custom Label has a default value tied to a base language, and language-specific overrides can be added through the Translation Workbench. When a user's locale matches a configured translation, Salesforce automatically returns the translated string without any changes to the referencing Apex, LWC, or Flow. This makes Custom Labels the recommended approach for any user-facing text in orgs with international users.