SfSkills Domains Docs GitHub

Home / admin / custom-button-to-action-migration

custom-button-to-action-migration

Migrating Classic Custom Buttons (Detail Page Buttons, List Buttons, Mass Action Buttons) and JavaScript Buttons to Lightning Quick Actions, Screen Flow Actions, LWC Quick Actions, and headless Quick Actions.

Open the skill folder on GitHub


Custom Button to Action Migration

This skill activates when a practitioner needs to migrate Classic Custom Buttons (especially JavaScript buttons, which are not supported in Lightning Experience) to Lightning Quick Actions, Screen Flow Actions, LWC Quick Actions, or other Lightning-native action surfaces.


Before Starting

Gather this context before working on anything in this domain:


Core Concepts

1. Classic Button Type → Lightning Replacement

Classic Button TypeContentLightning ReplacementMigration Difficulty
Detail Page Button — URL (relative path)/apex/MyVfPage?id={!Account.Id}Quick Action with Visualforce override OR navigation Quick ActionLow
Detail Page Button — URL (absolute external)https://external.com/lookup?id={!Account.Id}Quick Action with URL action type, or LWC Quick Action with window.openLow
Detail Page Button — JavaScript (simple Apex call)sforce.apex.execute('MyController','myMethod', {id: '{!Id}'})LWC Quick Action calling imperative ApexMedium
Detail Page Button — JavaScript (complex DOM manipulation)var elem = document.getElementById(...); elem.style...Re-architect as Screen Flow with Display fields OR LWC Quick ActionHigh
Detail Page Button — Visualforce Page overrideCustom VF pageQuick Action with Lightning component override OR retained as VFMedium
List Button — URL (relative)URL navigates to a list-context pageList View Action OR Mass Quick ActionMedium
List Button — JavaScript (operates on selected records)Reads getRecordIds() and processesMass Quick Action invoking Apex/Flow with selected record IDsHigh
Mass Action ButtonOperates on multiple records from a list viewMass Quick Action OR List View Action invoking Flow/ApexHigh
S-Control buttonsLegacy S-ControlNOT supported anywhere in modern Lightning; rebuild from scratchHigh

2. JavaScript Buttons Have NO Direct Lightning Equivalent

Salesforce explicitly does not support JavaScript Buttons in Lightning Experience. The button definition still exists in metadata (and continues to work in Classic), but in Lightning UI it does not render or fires only an "unsupported" message.

Classic JS PatternLightning Replacement
alert('Saved')ShowToastEvent from lightning/platformShowToastEvent
confirm('Delete?') then conditional logicLightningConfirm from lightning/confirm
window.location.href = '/...'NavigationMixin.Navigate({type: 'standard__webPage', attributes: {url: '...'}})
sforce.apex.execute('Class', 'method', params)Imperative Apex import + await method({...})
sforce.connection.update(records)import { updateRecord } from 'lightning/uiRecordApi' OR @AuraEnabled Apex with explicit DML
getRecordIds() (list buttons)@api recordIds exposed by Mass Quick Action / List Action
document.getElementById(...) DOM manipulationLWC this.template.querySelector(...) (within own component only)

3. Three Lightning Action Surfaces

Action TypeWhen to useFiles / Components
Quick Action — Create / Update / Log a CallStandard CRUD or activity loggingSetup configuration only; no code
Screen Flow ActionMulti-step user input, branching logic, no codeFlow Builder; deploy as Flow
LWC Quick Action (screen)Custom UI for input + Apex processingLWC bundle with lightningRecordPage Quick Action target
LWC Quick Action (headless)One-click action with no UI (just Apex + toast)LWC with actionType set to Action and headless invocation
Aura Quick ActionLegacy; only when LWC is insufficient (rare)Aura component bundle

LWC Quick Actions are the modern preferred replacement for most non-trivial JavaScript buttons. Headless LWC Quick Actions specifically replace the "click button → confirmation alert → save" pattern.

4. List View / Mass Action Replacement

Classic PatternLightning Replacement
List Button with JS that loops over getRecordIds()Mass Quick Action that exposes @api recordIds to an LWC, which iterates and calls Apex
Mass Action Button (e.g., "Mass Update Status")List View Action invoking a Screen Flow that takes the record collection as input
Custom Mass Action with complex UILWC List Action with internal logic
List Button that just navigates to a URLList View URL Action (Lightning supports URL list actions)

5. URL Button Translation Pitfalls

URL buttons in Lightning handle absolute external URLs cleanly. Relative paths break:

Classic URL PatternLightning Behavior
/apex/MyVfPage?id={!Account.Id}Lightning rewrites to /lightning/cmp/... — sometimes works, often doesn't
/{!Account.Id} (record navigation)Use NavigationMixin instead
/{!Account.Id}/d (record detail in Classic)Lightning has no "detail mode" URL convention
Absolute external URLWorks as-is in both surfaces
URL with target="_self" vs _blankWindow-target hints not always honored in Lightning

For relative paths, replace with a Quick Action that uses the Lightning navigation framework, not a URL.


Common Patterns

Pattern 1: JavaScript Button That Calls Apex → Headless LWC Quick Action

When to use: A Classic JS button that calls an Apex method, shows a confirmation/result, and refreshes the page. This is the most common JS button pattern.

How it works:

  1. Identify the Apex class and method the JS button calls.
  2. Convert the Apex method to @AuraEnabled (typically NOT cacheable since it performs side effects).
  3. Build an LWC Quick Action with actionType=Action (headless mode) and target lightning__RecordAction.
  4. The LWC's invoke() method calls the Apex imperatively, fires a toast for success/error, and dispatches RefreshRecordEvent from @salesforce/apex to refresh the page.
  5. Configure the Quick Action in Setup → Object Manager → Buttons, Links, and Actions.
  6. Add the Quick Action to the page layout (Lightning Actions section).

Why not the alternative: Screen Flow is also a valid replacement but adds a UI step (typically a confirmation screen) that users may not want for a simple "do it now" action.

Pattern 2: JavaScript Button with User Input → Screen Flow

When to use: A Classic JS button that prompts for input (a prompt() or a custom HTML form) and processes the result.

How it works:

  1. Build a Screen Flow with the input fields the button collected.
  2. Add an Action (Apex Invocable, Update Records, etc.) for the processing logic.
  3. Configure as a Screen Flow Quick Action on the object.
  4. Optional: add a confirmation screen for user feedback.

Why not the alternative: Building a custom LWC for a simple input form is overkill when Screen Flow handles it declaratively.

Pattern 3: JavaScript List Button → Mass Quick Action with LWC

When to use: A Classic JS list button that processes selected records via getRecordIds().

How it works:

  1. Build an LWC component targeted at lightning__RecordAction with actionType=ScreenAction.
  2. Expose @api recordIds (set automatically when invoked from a list).
  3. The LWC's invoke() method (or the connectedCallback if always invoked once mounted) iterates this.recordIds and calls Apex with the collection.
  4. Handle partial successes — some records may fail processing.
  5. Configure as a List View Action in Setup.

Why not the alternative: Direct Apex Invocable Action from a Flow can also work, but loses the per-record progress UI an LWC can provide.

Pattern 4: URL Button to External System → Lightning URL Action

When to use: A Classic button that opens an external system URL with merge field substitution (e.g., a LinkedIn lookup, a CTI dial-out, a partner portal SSO).

How it works:

  1. In Setup → Object Manager → Buttons, Links, and Actions, create a new Action with type "Custom Visualforce" or "URL".
  2. Set the URL with merge fields: https://external.com/lookup?id={!Account.Id}&name={!Account.Name}.
  3. Configure target window behavior (open in new tab vs replace current).
  4. Add to the page layout.

Why not the alternative: For a simple URL navigation, an LWC Quick Action is overkill — URL Actions handle this natively.

Pattern 5: Coexistence — Retain Classic Button, Add Lightning Action

When to use: The org has both Classic and Lightning users. Removing the Classic button breaks Classic users; not adding the Lightning equivalent breaks Lightning users.

How it works:

  1. Keep the Classic Custom Button as-is (works in Classic).
  2. Build the Lightning Quick Action / LWC Quick Action / Screen Flow Action equivalent.
  3. Both surface independently per UI mode.
  4. Eventually, when all users are on Lightning, retire the Classic button.

Why not the alternative: Removing the Classic button before all users have moved to Lightning breaks production for Classic users immediately. Coexistence is the safe path during the transition window.


Decision Guidance

SituationRecommended ApproachReason
Classic JS button, calls Apex, shows alertPattern 1: Headless LWC Quick ActionClosest UX parity for one-click actions
Classic JS button, prompts for inputPattern 2: Screen FlowDeclarative input collection; no LWC needed
Classic JS list button, processes selected recordsPattern 3: Mass Quick Action with LWCHandles record-collection processing with progress UI
Classic URL button, external systemPattern 4: Lightning URL ActionNative URL action; no code
Classic URL button, relative Salesforce pathQuick Action with NavigationMixinRelative paths often broken; explicit navigation is safer
Visualforce page override buttonQuick Action with VF override OR retain as VFSome VF overrides have no Lightning equivalent
S-Control buttonsRebuild from scratchS-Controls are fully retired
Classic and Lightning both in usePattern 5: CoexistenceDon't break Classic users prematurely
JS button does complex DOM manipulationLWC Quick Action with internal DOM logicDOM access scoped to the LWC's own template
Mass action operates on >50 recordsInvocable Apex via Mass Action with batch considerationLarge collections may need async processing
Button only used in Salesforce MobileCheck mobile-Quick-Action support per patternSome patterns (LWC headless) work on mobile, others don't

Step-by-step instructions for an AI agent or practitioner working on this task:

  1. Inventory custom buttons. Run the Tooling API query for WebLink. Group by BehaviorType and ContentSource. Read each JS button's OnClick body and capture what it does (Apex call, navigation, alert, DOM manipulation).
  2. Categorize each button by pattern. Apply the Decision Guidance table. Most JS buttons fall into Pattern 1 (Apex call) or Pattern 2 (input prompt). URL buttons usually map cleanly to Pattern 4.
  3. Audit usage. Confirm which buttons are actively used. Buttons that haven't been invoked in 6+ months should be retired without replacement (validated with the user-team).
  4. Build the Lightning replacements. Implement per-pattern. Test each against the same scenarios the original button served.
  5. Configure on page layouts and list views. Each Lightning Action must be added to relevant page layouts (Lightning Actions section) or list view button bars.
  6. Test in sandbox with users from each affected profile. Specifically: do users find the new action where they expect it? Does the action's UX match the original's intent?
  7. Phased production rollout with audit log. Coexistence first (keep Classic, add Lightning); monitor adoption via the Last_Action_Source__c field. Maintain a Custom_Button_Migration_Log__c mapping (Classic name → Lightning replacement → status). Retire Classic buttons when adoption hits the agreed threshold (typically ≥95% Lightning over 30 days).

Review Checklist

Run through these before marking work in this area complete:


Salesforce-Specific Gotchas

Non-obvious platform behaviors that cause real production problems:

  1. JavaScript buttons silently render as nothing in Lightning Experience. A user navigating Lightning sees the button's place in the page layout — but the button doesn't render. There's no error message, no "this button isn't supported" indicator. Users assume the action is missing entirely. Audit page layouts for JS buttons and either replace them or flag to users that they're Classic-only.
  2. Mass Action limit on records is 200 by default. A list view Mass Action that operates on selected records is governor-limited. Selecting 500 records and invoking a Mass Action processes only the first 200. Users notice silently (the unselected 300 just don't get the action applied). For Mass Actions on larger collections, design a Flow with batch processing or an async Apex job.
  3. {!Account.Id} URL merge fields don't work the same way in all Lightning Action types. Lightning URL Actions support some merge fields, but the {!Object.Field} syntax differs from the Classic Custom Button merge field syntax in subtle ways. Quick Actions of type "URL" use a slightly different merge syntax than Classic. Test each merge field after migration.
  4. Quick Actions invoked from a Console subtab don't navigate the same way as standard Lightning. In Service Cloud Console, a Quick Action that uses NavigationMixin.Navigate({type: 'standard__recordPage'}) opens the navigation in the console workspace — sometimes as a new tab, sometimes replacing the current tab, depending on the action's launch context. If the original button assumed a specific navigation behavior, test in Console mode explicitly.
  5. List View Actions don't appear if the list view filter doesn't match any records. A Mass Quick Action requires at least one selectable record. Empty list views hide the action entirely. Users assume the action is missing for that list view, when actually no records match the filter.
  6. window.opener and cross-window communication patterns from Classic JS don't translate. Some Classic JS buttons opened a popup window, then communicated back to the parent via window.opener.location.reload(). LWC and Lightning Quick Actions have no equivalent — windows are sandboxed differently. Re-architect these flows as Quick Actions that complete in the same context (no popups).
  7. PageReference redirects from Classic Apex callbacks don't work in Lightning Quick Actions. A Classic JS button that called sforce.apex.execute and the Apex method returned a PageReference — Classic followed the redirect. In Lightning, the redirect is ignored. Re-architect: Apex returns data; LWC explicitly navigates via NavigationMixin after the call resolves.
  8. Hard-coded record IDs in JS buttons don't translate to merge fields cleanly. Some JS buttons hardcoded record IDs (anti-pattern) for "demo" records or admin actions. The migration is the moment to remove the hardcoded IDs (use Custom Settings, Custom Metadata, or proper record-context resolution). Quick Actions and Flows have no equivalent for "always operate on record X regardless of context."

Output Artifacts

ArtifactDescription
Lightning Quick ActionsObject-specific actions replacing Detail Page Buttons
LWC Quick Actions (Headless / Screen)Replacements for JS buttons that need Apex execution and/or custom UI
Screen Flow ActionsReplacements for JS buttons that need user input or branching
List View Actions / Mass Quick ActionsReplacements for List Buttons that operate on selected records
URL ActionsReplacements for URL buttons with absolute external URLs
Updated page layoutsLightning Actions section configured to expose new Quick Actions
Migration audit logOld button → new action mapping with status (Active, Retained, Retired)
Coexistence retention listClassic buttons kept for Classic users with a planned retirement date

Package files