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;
}
<?xml version="1.0" encoding="UTF-8"?>
<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>