Salesforce

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 messages
  • UI_ConfirmDelete, UI_SaveSuccessful — component strings
  • URL_TermsPage, URL_HelpPortal — external links
  • Config_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:

  1. Look for a translation matching the running user's language and locale.
  2. If none exists, fall back to the organisation's default language.
  3. 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.

← All articles

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.