
GraphQL mutations support to Lightning Web Components via executeMutation from lightning/graphql. If you have ever built LWC using GraphQL to reads records but update/delete were lightning/uiRecordApi or Apex. with Spring ’26, you can now query and mutate UI API data through on GraphQL surface.
What changed in Spring’26:
- Queries: still best done reactively with graphql wire adapter.
- Mutations: now supported imperatively with executeMutation for create/update/delete
lightning/uiRecordApi is still great for straightforward CRUD, but if your component is already GraphQL-first, then executeMutation let you keep reads and writes in one GraphQL flow, return exactly the fields you need in the mutation payload, and refresh/recompute the UI from a single source of truth after each change.
the exampleLWC pattern (high-level)
- Load 100 Accounts with a GraphQL query @wire(graphql,{query,variables})
- Support inline edits: on save, decide create vs update based on whether the row is new – call Update, variables: { input:…})
- Support delete: call executeMutation ({query: DELETE, variables: {input:{Id}}})
- After any mutation: Refresh the GraphQL wire result – recompute derived UI state (filter + summary)
The chart/UI summary is just derived from the same queried data. The key point is: once your data comes from GraphQL and your writes also go through GraphQL, it’s easy to keep “counts by Type/Industry” accurate because you refresh from a single source of truth.
<template> <lightning-card title="Account Overview" icon-name="standard:account"> <div class="slds-p-around_small"> <template if:true={errorMessage}> <div class="slds-text-color_error slds-m-bottom_small"> {errorMessage} </div> </template> <div class="slds-grid slds-wrap slds-m-bottom_small"> <div class="slds-col slds-size_1-of-1 slds-medium-size_2-of-3"> <div class="chart-panel"> <div class="chart-title">Accounts by Type</div> <div class="type-chart"> <template for:each={typeShare} for:item="item"> <div key={item.label} class="type-row" title={item.tooltip}> <div class="type-row-label"> <span class="industry-swatch" style={item.dotStyle}></span> {item.label} </div> <div class="type-row-bar"> <span class="type-row-fill" style={item.barStyle}></span> </div> <div class="type-row-value">{item.percent}</div> </div> </template> </div> </div> </div> <div class="slds-col slds-size_1-of-1 slds-medium-size_1-of-3 slds-p-left_small"> <div class="summary-card"> <p class="summary-label">{filteredCountLabel}</p> <p class="summary-sub">Grouped by Industry</p> <p class="summary-meta">Showing up to 100 Accounts</p> <div class="slds-m-top_small"> <lightning-button label="New Account" variant="brand" onclick={handleAddRow}></lightning-button> </div> <div class="slds-m-top_small"> <lightning-combobox label="Filter by Industry" value={selectedIndustry} options={industryOptions} onchange={handleIndustryChange}> </lightning-combobox> </div> </div> </div> </div> <template if:true={isLoading}> <div class="slds-is-relative slds-m-vertical_medium"> <lightning-spinner size="small"></lightning-spinner> </div> </template> <div class="account-table slds-scrollable_y"> <lightning-datatable key-field="id" data={filteredAccounts} columns={columns} draft-values={draftValues} onsave={handleSave} onrowaction={handleRowAction} hide-checkbox-column show-row-number-column> </lightning-datatable> </div> </div> </lightning-card></template>
import { LightningElement, wire } from 'lwc';import { executeMutation, gql, graphql } from 'lightning/graphql';const LIMIT_SIZE = 100;const INDUSTRY_ALL = 'ALL';const INDUSTRY_COLORS = [ '#1b96ff', '#3bb3ff', '#5d9cfe', '#7b7cff', '#9b6dff', '#c06bff', '#ff7ab6', '#ff9f5a', '#f3c246', '#6cc56f'];const OTHER_COLOR = '#9aa4b2';const COLUMNS = [ { label: 'Name', fieldName: 'name', type: 'text', wrapText: true, editable: true }, { label: 'Phone', fieldName: 'phone', type: 'phone', editable: true }, { label: 'Website', fieldName: 'website', type: 'url', editable: true }, { label: 'Type', fieldName: 'type', type: 'text', editable: true }, { label: 'Industry', fieldName: 'industry', type: 'text', editable: true }, { label: 'Billing City', fieldName: 'billingCity', type: 'text', editable: true }, { label: 'Billing Country', fieldName: 'billingCountry', type: 'text', editable: true }, { label: 'Created', fieldName: 'createdDate', type: 'date' }];const ROW_ACTIONS = [{ label: 'Delete', name: 'delete' }];const ACTION_COLUMN = { type: 'action', typeAttributes: { rowActions: ROW_ACTIONS, menuAlignment: 'right' }};const ACCOUNT_QUERY = gql` query AccountList($limit: Int) { uiapi { query { Account(first: $limit, orderBy: { Name: { order: ASC } }) { edges { node { Id Name { value } Phone { value } Website { value } Type { value } Industry { value } BillingCity { value } BillingCountry { value } CreatedDate { value } } } } } } }`;const CREATE_ACCOUNT_MUTATION = gql` mutation CreateAccount($input: AccountCreateInput!) { uiapi { AccountCreate(input: $input) { Record { Id Name { value } Phone { value } Website { value } Type { value } Industry { value } BillingCity { value } BillingCountry { value } CreatedDate { value } } } } }`;const UPDATE_ACCOUNT_MUTATION = gql` mutation UpdateAccount($input: AccountUpdateInput!) { uiapi { AccountUpdate(input: $input) { Record { Id Name { value } Phone { value } Website { value } Type { value } Industry { value } BillingCity { value } BillingCountry { value } CreatedDate { value } } } } }`;const DELETE_ACCOUNT_MUTATION = gql` mutation DeleteAccount($input: AccountDeleteInput!) { uiapi { AccountDelete(input: $input) { Id } } }`;export default class ExampleLwc extends LightningElement { accounts = []; errorMessage; columns = [...COLUMNS, ACTION_COLUMN]; draftValues = []; typeShare = []; industryOptions = []; selectedIndustry = INDUSTRY_ALL; isDataLoading = true; wiredResult; get variables() { return { limit: LIMIT_SIZE }; } wire(graphql, { query: ACCOUNT_QUERY, variables: '$variables' }) wiredAccounts(result) { this.wiredResult = result; this.isDataLoading = false; if (result.data) { const edges = result.data.uiapi?.query?.Account?.edges ?? []; this.accounts = edges .map((edge) => edge.node) .filter((node) => !!node?.Id) .map((node) => ({ id: node.Id, name: node.Name?.value || '', phone: node.Phone?.value, website: node.Website?.value, type: node.Type?.value, industry: node.Industry?.value, billingCity: node.BillingCity?.value, billingCountry: node.BillingCountry?.value, createdDate: node.CreatedDate?.value })); this.errorMessage = undefined; this.updateVisuals(); return; } if (result.errors && result.errors.length > 0) { this.accounts = []; this.errorMessage = this.reduceError(result.errors); this.updateVisuals(); } } get isLoading() { return this.isDataLoading; } get filteredCountLabel() { if (this.selectedIndustry === INDUSTRY_ALL) { return `${this.accounts.length} Accounts`; } return `${this.filteredAccounts.length} of ${this.accounts.length} Accounts`; } get filteredAccounts() { if (this.selectedIndustry === INDUSTRY_ALL) { return this.accounts; } return this.accounts.filter((account) => { const label = account.industry && account.industry.trim().length > 0 ? account.industry : 'Unknown'; return label === this.selectedIndustry; }); } handleAddRow() { const id = `NEW_${Date.now()}`; this.accounts = [ { id, name: '', phone: '', website: '', type: '', industry: '', billingCity: '', billingCountry: '', createdDate: '' }, ...this.accounts ]; } handleIndustryChange(event) { this.selectedIndustry = event.detail.value || INDUSTRY_ALL; this.updateVisuals(); } async handleSave(event) { const drafts = event.detail.draftValues || []; if (drafts.length === 0) { return; } this.isDataLoading = true; this.errorMessage = undefined; try { const mutationResults = await Promise.all( drafts.map((draft) => { const recordId = String(draft.id || ''); const fields = this.buildFields(draft); if (recordId.startsWith('NEW_')) { return executeMutation({ query: CREATE_ACCOUNT_MUTATION, variables: { input: { Account: fields } } }); } if (recordId) { return executeMutation({ query: UPDATE_ACCOUNT_MUTATION, variables: { input: { Id: recordId, Account: fields } } }); } return Promise.resolve({ data: undefined, errors: [{ message: 'Missing record id.' }] }); }) ); const errors = mutationResults.flatMap((result) => result.errors || []); if (errors.length > 0) { this.errorMessage = this.reduceError(errors); return; } this.draftValues = []; await this.refreshGraphql(); } catch (error) { this.errorMessage = this.reduceError(error); } finally { this.isDataLoading = false; } } async handleRowAction(event) { if (event.detail.action.name !== 'delete') { return; } const recordId = event.detail.row.id; if (recordId && recordId.startsWith('NEW_')) { this.accounts = this.accounts.filter((row) => row.id !== recordId); return; } this.isDataLoading = true; this.errorMessage = undefined; try { const result = await executeMutation({ query: DELETE_ACCOUNT_MUTATION, variables: { input: { Id: recordId } } }); if (result.errors && result.errors.length > 0) { this.errorMessage = this.reduceError(result.errors); return; } await this.refreshGraphql(); } catch (error) { this.errorMessage = this.reduceError(error); } finally { this.isDataLoading = false; } } async refreshGraphql() { if (this.wiredResult?.refresh) { await this.wiredResult.refresh(); } } buildFields(draft) { const fields = {}; if (draft.name !== undefined) fields.Name = draft.name; if (draft.phone !== undefined) fields.Phone = draft.phone; if (draft.website !== undefined) fields.Website = draft.website; if (draft.type !== undefined) fields.Type = draft.type; if (draft.industry !== undefined) fields.Industry = draft.industry; if (draft.billingCity !== undefined) fields.BillingCity = draft.billingCity; if (draft.billingCountry !== undefined) fields.BillingCountry = draft.billingCountry; return fields; } updateVisuals() { const industry = this.buildDistribution(this.accounts, 'industry', undefined, false); this.industryOptions = this.buildIndustryOptions(industry.labels); if (!this.industryOptions.some((option) => option.value === this.selectedIndustry)) { this.selectedIndustry = INDUSTRY_ALL; } const filtered = this.filteredAccounts; const typeDistribution = this.buildDistribution(filtered, 'type', undefined, false); this.typeShare = this.buildShare(typeDistribution, filtered.length); } buildDistribution(rows, key, limit, groupOther = true) { const counts = new Map(); rows.forEach((account) => { const raw = account[key]; const label = raw && raw.trim().length > 0 ? raw : 'Unknown'; counts.set(label, (counts.get(label) || 0) + 1); }); const entries = Array.from(counts.entries()).sort((a, b) => b[1] - a[1]); const safeLimit = typeof limit === 'number' && limit > 0 ? limit : entries.length; const top = entries.slice(0, safeLimit); if (groupOther && entries.length > safeLimit) { const remainder = entries.slice(safeLimit); const othersTotal = remainder.reduce((sum, [, value]) => sum + value, 0); if (othersTotal > 0) { top.push(['Other', othersTotal]); } } return { labels: top.map(([label]) => label), values: top.map(([, value]) => value), colors: top.map(([label], index) => (label === 'Other' ? OTHER_COLOR : INDUSTRY_COLORS[index % INDUSTRY_COLORS.length]) ) }; } buildShare(distribution, total) { return distribution.labels.map((label, index) => { const count = distribution.values[index] ?? 0; const percentValue = total > 0 ? Math.round((count / total) * 100) : 0; const color = distribution.colors[index] ?? OTHER_COLOR; return { label, count, percent: `${percentValue}%`, barStyle: `width: ${percentValue}%; background-color: ${color};`, dotStyle: `background-color: ${color};`, tooltip: `${label}: ${count} (${percentValue}%)` }; }); } buildIndustryOptions(labels) { const options = labels.map((label) => ({ label, value: label })); return [{ label: 'All Industries', value: INDUSTRY_ALL }, ...options]; } reduceError(error) { if (!error) { return 'Unknown error'; } if (Array.isArray(error)) { return error.map((item) => item?.message).filter(Boolean).join(', '); } if (Array.isArray(error.body)) { return error.body.map((item) => item.message).filter(Boolean).join(', '); } if (error.body && error.body.message) { return error.body.message; } if (error.message) { return error.message; } try { return JSON.stringify(error); } catch { return 'Unknown error'; } }}
.chart-panel { background: linear-gradient(135deg, #f8fbff, #eef5ff); border-radius: 12px; border: 1px solid #d9e6ff; padding: 12px; min-height: 320px;}.chart-title { font-size: 13px; font-weight: 600; color: #1b2a3a; margin-bottom: 8px;}.type-chart { display: flex; flex-direction: column; gap: 10px; padding-top: 8px; min-height: 220px;}.account-table { height: 420px;}.type-row { display: grid; grid-template-columns: minmax(0, 1fr) minmax(120px, 2fr) auto; gap: 8px; align-items: center; font-size: 12px;}.type-row-label { display: inline-flex; align-items: center; gap: 6px; color: #5f6f86; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}.type-row-bar { height: 8px; background: rgba(27, 42, 58, 0.08); border-radius: 999px; overflow: hidden;}.type-row-fill { display: block; height: 100%; border-radius: 999px;}.type-row-value { font-size: 11px; font-weight: 600; color: #0a2342;}@media (max-width: 768px) { .chart { height: 240px; }}.summary-card { background: #ffffff; border: 1px solid #e5e5e5; border-radius: 12px; padding: 16px; height: 100%; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);}.summary-label { font-size: 18px; font-weight: 700; color: #1b2a3a; margin-bottom: 8px;}.summary-sub { font-size: 14px; color: #4c5c70; margin-bottom: 8px;}.summary-meta { font-size: 12px; color: #7a8aa0;}.industry-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: inline-flex; align-items: center; gap: 6px;}.industry-swatch { width: 10px; height: 10px; border-radius: 999px; border: 1px solid rgba(0, 0, 0, 0.1); flex: 0 0 auto;}
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata"> <apiVersion>66.0</apiVersion> <isExposed>true</isExposed> <targets> <target>lightning__AppPage</target> <target>lightning__HomePage</target> <target>lightning__RecordPage</target> </targets></LightningComponentBundle>