SfSkills Domains Docs GitHub

Home / lwc / lwc-console-workspace-api

lwc-console-workspace-api

Use when an LWC needs to programmatically manipulate Service Console workspace tabs and the utility bar — opening tabs and subtabs, refreshing or closing tabs, setting tab labels and icons, detecting console context, and integrating with the utility bar — via the `lightning/platformWorkspaceApi` and `lightning/platformUtilityBarApi` modules.

Open the skill folder on GitHub


LWC Console Workspace API

Activate when an LWC needs to manipulate Service Console tabs or the utility bar programmatically — opening a new subtab in response to a button click, refreshing a tab after an external update, setting the tab label to a dynamic record name, closing the current tab when a multi-step action completes, or behaving differently when running inside a console vs. a standard Lightning app. The modern surface is the lightning/platformWorkspaceApi and lightning/platformUtilityBarApi modules, which replace the Aura-only lightning:workspaceAPI and lightning:utilityBarAPI for new development.

This skill is the runtime-API counterpart to admin/service-console-configuration (which covers declarative Service Console setup) and complements lwc/lightning-navigation-dead-link-handling (which handles navigation failures) and lwc/lwc-cross-tab-state-sync (which coordinates between browser tabs, not console subtabs).


Before Starting

Gather this context before writing tab-manipulation code:


Core Concepts

lightning/platformWorkspaceApi Surface

The module exports tab-lifecycle and tab-metadata functions, plus a context-detection wire adapter.

import {
    openTab,             // open a new primary workspace tab
    openSubtab,          // open a subtab under a specified primary tab
    closeTab,            // close by tabId
    refreshTab,          // re-fetch the tab's underlying record / page; invalidates LDS for that tab
    focusTab,            // bring a tab to the foreground
    getFocusedTabInfo,   // info about the currently focused workspace tab
    getAllTabInfo,       // info about every open tab
    getTabInfo,          // info about a specific tabId
    setTabLabel,         // change the displayed label on the tab
    setTabIcon,          // change the tab icon (SLDS icon path)
    setTabHighlighted,   // visually emphasize the tab (e.g. unread badge)
    getEnclosingTabId,   // tabId of the tab containing the currently-running LWC
    IsConsoleNavigation, // wire adapter that emits true if host is console
    EnclosingTabId       // wire adapter exposing the enclosing tabId
} from 'lightning/platformWorkspaceApi';

Each lifecycle function returns a Promise. Discovery functions (getFocusedTabInfo, etc.) also return promises and resolve to plain JS objects.

Console Context Detection

IsConsoleNavigation is the canonical detection mechanism for "am I in a console?":

import { LightningElement, wire } from 'lwc';
import { IsConsoleNavigation } from 'lightning/platformWorkspaceApi';

export default class RecordActions extends LightningElement {
    @wire(IsConsoleNavigation) isConsole;

    handleRowAction() {
        if (this.isConsole) {
            // call openSubtab / refreshTab
        } else {
            // fall back to NavigationMixin.Navigate or imperative action
        }
    }
}

IsConsoleNavigation is a wire adapter, not a synchronous property. The first render may not have it set; defer console-specific actions to user-triggered handlers or renderedCallback.

Tab Identity

The tabId is an opaque string the platform issues per tab open. It is stable across the tab's lifetime but does not survive page reload. Cache it in component state only; do not persist it to record data or URL parameters.

getEnclosingTabId() returns the tab containing the calling LWC. From a subtab's LWC, this returns the subtab's id, not the primary tab's id — to get the primary, walk via getTabInfo(subtabId).parentTabId.

Utility Bar API

lightning/platformUtilityBarApi mirrors the workspace API for utility-bar-mounted LWCs:

import {
    openUtility,
    minimizeUtility,
    getEnclosingUtilityId,
    getUtilityInfo,
    setUtilityIcon,
    setUtilityLabel,
    setUtilityHighlighted,
    EnclosingUtilityId,
    IsUtilityOpen
} from 'lightning/platformUtilityBarApi';

A utility-bar LWC can open or minimize itself, change its own icon/label, and subscribe to its own open/closed state. It cannot directly manipulate workspace tabs — to bridge to the workspace, import lightning/platformWorkspaceApi alongside.

Refresh Semantics

refreshTab(tabId) invalidates the LDS cache for records visible in that tab and triggers re-render. It does not return a promise that resolves "when re-render is complete" — it returns a promise that resolves "when the refresh signal has been dispatched." Wires reactivate asynchronously after.

For "refresh THIS tab", a record-page LWC commonly calls:

refreshTab(await getEnclosingTabId());

For "refresh a sibling subtab" (e.g., subtab A updates an Account; subtab B for that same Account should re-render), iterate getAllTabInfo() and refresh matching subtabs by recordId.


Common Patterns

Pattern: Open subtab from a row action

When to use: A list LWC on a console primary tab; clicking a row should open the record in a subtab without leaving the current primary.

How it works:

import { LightningElement, wire } from 'lwc';
import {
    IsConsoleNavigation,
    openSubtab,
    getFocusedTabInfo
} from 'lightning/platformWorkspaceApi';
import { NavigationMixin } from 'lightning/navigation';

export default class CaseList extends NavigationMixin(LightningElement) {
    @wire(IsConsoleNavigation) isConsole;

    async handleRowClick(event) {
        const recordId = event.detail.row.Id;
        if (this.isConsole) {
            const focused = await getFocusedTabInfo();
            await openSubtab(focused.tabId, {
                pageReference: {
                    type: 'standard__recordPage',
                    attributes: { recordId, objectApiName: 'Case', actionName: 'view' }
                },
                focus: true
            });
        } else {
            this[NavigationMixin.Navigate]({
                type: 'standard__recordPage',
                attributes: { recordId, objectApiName: 'Case', actionName: 'view' }
            });
        }
    }
}

Why not the alternative: Calling NavigationMixin.Navigate in a console primary tab opens a new primary tab, not a subtab — which loses the user's place. The workspace API gives the correct subtab placement.

Pattern: Dynamic tab label from record data

When to use: A record page LWC where the tab label should show meaningful record context (e.g., "Case 00001234 — High Priority").

How it works:

import { LightningElement, wire, api } from 'lwc';
import { getRecord, getFieldValue } from 'lightning/uiRecordApi';
import {
    getEnclosingTabId,
    setTabLabel,
    setTabIcon,
    IsConsoleNavigation
} from 'lightning/platformWorkspaceApi';
import CASE_NUMBER from '@salesforce/schema/Case.CaseNumber';
import CASE_PRIORITY from '@salesforce/schema/Case.Priority';

export default class CaseTabLabel extends LightningElement {
    @api recordId;
    @wire(IsConsoleNavigation) isConsole;

    @wire(getRecord, { recordId: '$recordId', fields: [CASE_NUMBER, CASE_PRIORITY] })
    async wiredCase({ data }) {
        if (!data || !this.isConsole) return;
        const number = getFieldValue(data, CASE_NUMBER);
        const priority = getFieldValue(data, CASE_PRIORITY);
        const tabId = await getEnclosingTabId();
        await setTabLabel(tabId, `Case ${number} — ${priority}`);
        if (priority === 'High') {
            await setTabIcon(tabId, 'standard:case', { iconAlt: 'High priority' });
        }
    }
}

Why not the alternative: Letting the platform default tab label (which uses the record's Name field) misses contextual signals like priority. Dynamic labels reduce agent cognitive load when switching between many tabs.

Pattern: Refresh tab after an Apex DML call

When to use: An LWC button updates a record via imperative Apex; the workspace tab should re-fetch to reflect the change.

How it works:

import { LightningElement, wire, api } from 'lwc';
import {
    getEnclosingTabId,
    refreshTab,
    IsConsoleNavigation
} from 'lightning/platformWorkspaceApi';
import updateCase from '@salesforce/apex/CaseController.markResolved';

export default class CaseResolveButton extends LightningElement {
    @api recordId;
    @wire(IsConsoleNavigation) isConsole;

    async handleResolve() {
        await updateCase({ caseId: this.recordId });
        if (this.isConsole) {
            const tabId = await getEnclosingTabId();
            await refreshTab(tabId);
        }
        // Outside console: rely on a wired record + refreshApex pattern instead.
    }
}

Why not the alternative: Mutating a record via imperative Apex bypasses LDS notification. Without refreshTab (in console) or refreshApex (outside), the surrounding components show stale values until the user clicks the platform refresh icon.

Pattern: Utility-bar quick-action launcher

When to use: A utility-bar LWC offers quick actions; clicking one should open the affected record in a workspace subtab.

How it works:

import { LightningElement } from 'lwc';
import { openTab, getFocusedTabInfo, openSubtab } from 'lightning/platformWorkspaceApi';
import { minimizeUtility } from 'lightning/platformUtilityBarApi';

export default class QuickRecordOpener extends LightningElement {
    async handleOpenAsSubtab(event) {
        const recordId = event.target.dataset.recordId;
        const focused = await getFocusedTabInfo();
        const pageReference = {
            type: 'standard__recordPage',
            attributes: { recordId, objectApiName: 'Account', actionName: 'view' }
        };
        if (focused && focused.tabId) {
            await openSubtab(focused.tabId, { pageReference, focus: true });
        } else {
            await openTab({ pageReference, focus: true });
        }
        await minimizeUtility();
    }
}

Why not the alternative: Always opening a new primary tab clutters the workspace; respecting the focused-tab context places the record where the agent is already working.


Decision Guidance

SituationRecommended ApproachReason
Component runs in both console and non-console hostsWire IsConsoleNavigation and branch on its booleanAvoids lightning/platformWorkspaceApi throw when running outside console
Need the tab containing the running LWCgetEnclosingTabId()The tab id is stable for the tab's lifetime
Need the currently-focused tab (may differ from enclosing)getFocusedTabInfo()Use when opening subtabs from a utility bar or modal
Open a record in the same workspace contextopenSubtab(focusedTabId, ...)Preserves the agent's primary tab
Open a new primaryopenTab(...)When the new record is a separate work unit
Refresh "this" tab after a writerefreshTab(await getEnclosingTabId())LDS-aware invalidation; sibling components re-wire
Refresh a sibling tab showing the same recordIterate getAllTabInfo(), match recordId, refreshNo built-in "refresh all tabs for record X"
Dynamic tab labelssetTabLabel(tabId, ...) from a record wireDecreases cognitive switching cost for agents
LWC sits in the utility barlightning/platformUtilityBarApi for self-state, lightning/platformWorkspaceApi for tab actionsTwo modules cover the two surfaces
Aura component coexists in the same workspaceBoth APIs share state; refresh and label changes cross frameworksDon't mix event subscriptions across frameworks

  1. Confirm host posture. Will the LWC run only inside a console app? Add IsConsoleNavigation even if so — non-console preview surfaces (App Builder, Experience Cloud staging) will exercise the no-console path.
  2. Choose tab actions. From the user story, list the precise tab actions (open, focus, refresh, close, label, icon, highlight) the component must perform. Each maps to one workspace-API call.
  3. Import only what you use. import { openSubtab, refreshTab } from 'lightning/platformWorkspaceApi'; is preferable to a namespace import — it keeps the bundle's static-analysis dependencies clean.
  4. Wire IsConsoleNavigation and branch. Every console-specific action sits behind if (this.isConsole) { ... } else { ...fallback... }. The fallback is usually NavigationMixin.Navigate, refreshApex, or a toast.
  5. Test in App Builder preview (non-console) AND in the actual Service Console. App Builder catches the no-console fallback; the Service Console catches subtab parentage and focus state.
  6. Handle promise rejections. Every workspace-API call returns a promise; wrap in try { ... } catch (e) { ... } and degrade gracefully — Service Console upgrades and edge cases can reject calls that worked yesterday.
  7. Document tab lifecycle in the component header. A reader should know which calls the component makes (open / refresh / close / label) without grepping the JS.

Review Checklist


Salesforce-Specific Gotchas

  1. lightning/platformWorkspaceApi throws outside a console. Calling openSubtab from a standard Lightning app surface raises an error. Always gate on IsConsoleNavigation or a try/catch.
  2. IsConsoleNavigation is a wire adapter, not a property. First render may have this.isConsole === undefined. Defer console-only actions to user handlers or renderedCallback, not connectedCallback.
  3. refreshTab doesn't await re-render. It resolves when the refresh has been dispatched, not when wires have re-evaluated. Tests that assert post-refresh DOM state must await one microtask tick after.
  4. getEnclosingTabId and getFocusedTabInfo can differ. A subtab LWC's enclosing is the subtab; the focused tab is whatever the user is on (could be a different primary). Pick the right one for the action.
  5. Tab IDs are opaque and ephemeral. Persisting a tab id to record data, URL params, or browser storage is a bug — it does not survive reload.
  6. Aura and LWC workspace APIs share state but are different module identities. Refresh and label changes work cross-framework; subscribing to Aura lightning:workspaceAPI events from LWC does not.
  7. refreshTab invalidates LDS for the tab's host record but not for unrelated records the tab also displays. A subtab showing Case AND its parent Account may refresh Case but show stale Account. Pair with refreshApex on imperative-Apex wires when needed.
  8. setTabLabel is not throttled. Setting it on every keystroke causes flicker; debounce to one update per record change.
  9. Console-related URL hash navigation differs in Lightning Experience. window.location.hash changes do not behave like a normal SPA inside the console iframe stack — rely on the workspace API for tab placement.

Output Artifacts

ArtifactDescription
Console-aware LWC bundleJS, HTML, meta-xml with IsConsoleNavigation gating and fallback path
Jest test pairOne test asserting console-path behavior, one asserting non-console-path
Tab lifecycle documentationComponent header listing which workspace-API calls the component makes
Refresh/wire ordering noteComment on each refreshTab documenting which wired records will re-evaluate

Package files