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:
- Host app type. Is the LWC guaranteed to run inside a console app (Service Console, custom console-navigation app), or can it also run in a standard Lightning app (App Builder Home page, Record Page in a non-console app, Experience Cloud)? Console-only callsites can call the workspace API directly; mixed callsites must detect console context first.
- Where the LWC is mounted. A workspace tab (primary), a subtab, the utility bar, an overlay/modal, or an Experience Cloud site?
getFocusedTabInfo()andgetEnclosingTabId()behave differently for each. - The action's transactional boundary. Tab manipulation after an Apex DML call has subtle ordering issues —
refreshTab()invalidates the LDS cache but does not re-run wires synchronously. Plan for the user-perceived latency. - Aura coexistence. If the surrounding workspace contains Aura components also using
lightning:workspaceAPI, the LWC API and the Aura API share the same underlying state but use different module identities. Cross-framework refreshes work, but cross-framework event subscriptions don't.
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
| Situation | Recommended Approach | Reason |
|---|---|---|
| Component runs in both console and non-console hosts | Wire IsConsoleNavigation and branch on its boolean |
Avoids lightning/platformWorkspaceApi throw when running outside console |
| Need the tab containing the running LWC | getEnclosingTabId() |
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 context | openSubtab(focusedTabId, ...) |
Preserves the agent's primary tab |
| Open a new primary | openTab(...) |
When the new record is a separate work unit |
| Refresh "this" tab after a write | refreshTab(await getEnclosingTabId()) |
LDS-aware invalidation; sibling components re-wire |
| Refresh a sibling tab showing the same record | Iterate getAllTabInfo(), match recordId, refresh |
No built-in "refresh all tabs for record X" |
| Dynamic tab labels | setTabLabel(tabId, ...) from a record wire |
Decreases cognitive switching cost for agents |
| LWC sits in the utility bar | lightning/platformUtilityBarApi for self-state, lightning/platformWorkspaceApi for tab actions |
Two modules cover the two surfaces |
| Aura component coexists in the same workspace | Both APIs share state; refresh and label changes cross frameworks | Don't mix event subscriptions across frameworks |
Recommended Workflow
- Confirm host posture. Will the LWC run only inside a console app? Add
IsConsoleNavigationeven if so — non-console preview surfaces (App Builder, Experience Cloud staging) will exercise the no-console path. - 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.
- 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. - Wire
IsConsoleNavigationand branch. Every console-specific action sits behindif (this.isConsole) { ... } else { ...fallback... }. The fallback is usuallyNavigationMixin.Navigate,refreshApex, or a toast. - 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.
- 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. - 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
-
IsConsoleNavigationwired and used to gate every workspace-API call - Non-console fallback path explicit (no silent no-op)
- Each workspace-API promise has rejection handling
-
getEnclosingTabId/getFocusedTabInfoused correctly for the lifecycle action (enclosing for "this tab"; focused for "wherever the user is") -
refreshTabpaired with the DML/imperative-Apex write it should reflect - Dynamic labels source from a wire (not from imperative call results that won't re-evaluate)
- Tab id never persisted across page reload
- Jest test covers both
isConsole === trueandisConsole === falsebranches - Component header documents which tab lifecycle calls it makes
- No Aura-only
lightning:workspaceAPIevent subscriptions left in the bundle (use module imports)
Salesforce-Specific Gotchas
lightning/platformWorkspaceApithrows outside a console. CallingopenSubtabfrom a standard Lightning app surface raises an error. Always gate onIsConsoleNavigationor a try/catch.IsConsoleNavigationis a wire adapter, not a property. First render may havethis.isConsole === undefined. Defer console-only actions to user handlers orrenderedCallback, notconnectedCallback.refreshTabdoesn'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.getEnclosingTabIdandgetFocusedTabInfocan 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.- 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.
- Aura and LWC workspace APIs share state but are different module identities. Refresh and label changes work cross-framework; subscribing to Aura
lightning:workspaceAPIevents from LWC does not. refreshTabinvalidates 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 withrefreshApexon imperative-Apex wires when needed.setTabLabelis not throttled. Setting it on every keystroke causes flicker; debounce to one update per record change.- Console-related URL hash navigation differs in Lightning Experience.
window.location.hashchanges do not behave like a normal SPA inside the console iframe stack — rely on the workspace API for tab placement.
Output Artifacts
| Artifact | Description |
|---|---|
| Console-aware LWC bundle | JS, HTML, meta-xml with IsConsoleNavigation gating and fallback path |
| Jest test pair | One test asserting console-path behavior, one asserting non-console-path |
| Tab lifecycle documentation | Component header listing which workspace-API calls the component makes |
| Refresh/wire ordering note | Comment on each refreshTab documenting which wired records will re-evaluate |
Related Skills
admin/service-console-configuration— Declarative setup of the Service Console app (workspace tabs, utility bar, navigation rules)lwc/lightning-navigation-dead-link-handling— Handling navigation failures (deleted records, missing pages) that interact with console subtab fallbackslwc/lwc-cross-tab-state-sync— BroadcastChannel between browser tabs (different surface from console subtabs)lwc/lwc-navigation-mixin—NavigationMixin.Navigatepatterns for non-console hosts; the typical fallback whenIsConsoleNavigationis falselwc/lwc-wire-refresh-patterns—refreshApexand wire-reactivity patterns that complementrefreshTabafter imperative writes