Salesforce LWC · GraphQL API · Full Guide

GraphQL in Salesforce LWC
Explained Visually

Two real components. Every concept broken down. From basic queries to nested children, variables, and live refresh.

🔷 graphqlBasicDemo 🏗️ graphqlAccountWithChildren ⚡ graphqlMutationsDemo lightning/graphql · API v66.0
The Mental Model

Before touching code, understand what GraphQL actually is and why it matters in Salesforce.

🪞

The Mirror Rule

Your query shape = your response shape. Whatever structure you write in the query, you get back data in that exact same shape. This is the #1 rule — everything else follows from it.

🍽️

REST vs GraphQL

REST: You call /api/accounts and get 50+ fields back whether you want them or not.

GraphQL: You ask for exactly Name + Industry. Server returns only those 2 fields. Surgical precision.

🗂️

Salesforce's Namespace

Salesforce wraps everything inside uiapi → query. Think of uiapi as the API gateway and query as the "read" section of the menu. Always the outer shell.

📦

The .value Quirk

Unlike standard GraphQL where Name returns a string, Salesforce wraps fields: Name { value }. This is because Salesforce adds displayValue metadata too. Only Id is a plain string — no { value } needed.

💡
Salesforce GraphQL is READ-ONLY The @wire(graphql) adapter is only for fetching data. For Create, Update, Delete — you still use lightning/uiRecordApi (createRecord, updateRecord, deleteRecord). After mutations, call refreshGraphQL() to re-sync the UI.
🔷 Component 1
Basic Query + CRUD

Fetch a list of Accounts with GraphQL, display them in a table, and Create / Update / Delete via uiRecordApi.

Query Dissection — Every Line Explained

PartCodeWhat it means
Operationquery AccountList { … }Named GraphQL operation. Always name your queries — helps with debugging and caching.
API Gatewayuiapi { … }Salesforce's root namespace. Every Salesforce GraphQL query starts here.
Read sectionquery { … }Inside uiapi — signals "I'm reading data." (Not mutating.) The word query appears twice — outer is the operation type, inner is uiapi's read section.
Object + limitAccount(first: 10)The sObject you're querying, and how many records max to return. Like SOQL LIMIT.
SortorderBy: { Name: { order: ASC } }Sorts results ascending by Name. Like SOQL ORDER BY Name ASC.
List wrapperedges { … }GraphQL "connection" pattern. Wraps the list of results. Always required when fetching multiple records.
Single recordnode { … }Each individual record in the list. Like one row in a database result.
System fieldIdThe record's Salesforce ID. Only field that returns a plain string — no { value } wrapper needed.
Standard fieldName { value }All standard fields return an object with { value } (and optionally displayValue). Must unwrap in JS.
Optional fieldIndustry { value }Nullable field. Use ?. in JS to safely access: Industry?.value ?? '—'

The Mirror Rule in Action — Query → Response → Flat Array

📝 What you write
query AccountList {
  uiapi {
    query {
      Account(first: 10) {
        edges {
          node {
            Id
            Name {
              value
            }
            Industry {
              value
            }
          }
        }
      }
    }
  }
}

mirrors
📦 What comes back (data)
{
  "uiapi": {
    "query": {
      "Account": {
        "edges": [
          {
            "node": {
              "Id": "001...",
              "Name": {
                "value": "Acme Corp"
              },
              "Industry": {
                "value": "Technology"
              }
            }
          }
        ]
      }
    }
  }
}

.map()
🎯 After .map() in JS
// Clean flat array:
[
  {
    Id: "001...",
    Name: "Acme Corp",
    Industry: "Technology"
  },
  ...
]

// Template uses clean names:
{acct.Name}
{acct.Industry}
// No acct.Name.value needed!

The .map() — "Flattening" the response

// data.uiapi.query.Account.edges is an array of { node: {...} } objects
// .map() loops through each edge and extracts what we need:

this.accounts = data.uiapi.query.Account.edges.map(edge => ({
  Id:       edge.node.Id,                       // Id = plain string, no .value
  Name:     edge.node.Name.value,              // unwrap .value → "Acme Corp"
  Industry: edge.node.Industry?.value ?? '—'  // ?. handles null, ?? sets fallback
}));

CRUD Pattern — Write via uiRecordApi, then Refresh

import { createRecord } from 'lightning/uiRecordApi';

async handleCreate() {
  await createRecord({
    apiName: 'Account',
    fields: { Name: this.newName.trim() }
  });
  await refreshGraphQL(this.wiredResult); // re-run the @wire query
}
import { updateRecord } from 'lightning/uiRecordApi';

async handleUppercase(event) {
  const id = event.target.dataset.id;
  const current = this.accounts.find(a => a.Id === id);
  await updateRecord({
    fields: { Id: id, Name: current.Name.toUpperCase() }
  });
  await refreshGraphQL(this.wiredResult);
}
import { deleteRecord } from 'lightning/uiRecordApi';

async handleDelete(event) {
  const id = event.target.dataset.id;
  await deleteRecord(id);
  await refreshGraphQL(this.wiredResult); // sync the list
}
// KEY PATTERN: Always store the wire result for later refresh

wiredResult; // ← class property

@wire(graphql, { query: gql`...` })
handleQuery(result) {
  this.wiredResult = result; // ← store it HERE
  const { data, errors } = result;
  // ... process data
}

// Later, after any mutation:
await refreshGraphQL(this.wiredResult); // ← use stored ref
🏗️ Component 2
Variables + Nested Children + displayValue

A detail-page component that fetches one Account plus its related Contacts and Opportunities — all in a single GraphQL query. Introduces dynamic variables, nested child queries, and formatted display values.

New Concepts in This Component

🎯 Variables ($accountId: ID!) — Dynamic Queries

Instead of hardcoding a filter value, you declare a variable with $accountId: ID! (the ! means required). The value is passed at runtime via the variables option in @wire. This makes the query reusable for any record.

⚙️ variables: '$variables' — Reactive Wire Variables

The string '$variables' tells the wire framework to call this.variables getter reactively. When this.recordId changes (e.g. user navigates to another record), the getter returns a new value and the wire automatically re-runs the query.

🏠 Nested Child Queries — One Call, Three Objects

Inside the Account node, you can query related Contacts and Opportunities directly. This is the superpower of GraphQL — what would be 3 REST API calls becomes 1 GraphQL query. Each child relationship can have its own first, orderBy, and where arguments.

💰 displayValue — Formatted vs Raw Value

Fields like AnnualRevenue and Amount support both value (raw number: 1500000) and displayValue (formatted string: "$1,500,000"). Use displayValue when showing to users, value for calculations.

🔢 totalCount — Count Without Mapping

Add totalCount alongside edges to get the total number of matching records (respects your where filter) without having to iterate. Useful for showing "37 Contacts" in a badge even if you only fetched the first 10.

📄 edges[0] — Single Record Pattern

On a detail page, your where clause filters to one record. The response still wraps it in edges[0] — GraphQL always returns a list. Access it with data.uiapi.query.Account.edges[0]. Always guard with a null check (if (!edge)).

The Full Query — Annotated

// $accountId: ID! declares a required variable of type Salesforce ID
query AccountDetail($accountId: ID!) {
  uiapi {
    query {
      // where: { Id: { eq: $accountId } } → runtime variable injected here
      Account(where: { Id: { eq: $accountId } }) {
        edges {
          node {
            Id
            Name { value }
            Industry { value }
            AnnualRevenue {
              value          // raw: 1500000
              displayValue   // formatted: "$1,500,000" ← show this to users
            }

            // ↓ NESTED CHILD QUERY — Contacts related to this Account
            Contacts(
              first: 50
              orderBy: { LastName: { order: ASC } }
            ) {
              edges {
                node {
                  Id
                  Name { value }
                  Title { value }
                  Email { value }
                }
              }
              totalCount  // total contacts, even if first: 50 limits display
            }

            // ↓ NESTED CHILD QUERY — Opportunities related to this Account
            Opportunities(
              first: 50
              orderBy: { CloseDate: { order: DESC } }
            ) {
              edges {
                node {
                  Id
                  Name { value }
                  StageName { value }
                  Amount {
                    value         // raw number for calculations
                    displayValue  // "$250,000" for display
                  }
                  CloseDate {
                    value         // "2025-06-30" (ISO)
                    displayValue  // "6/30/2025" (locale-formatted)
                  }
                }
              }
              totalCount
            }
          }
        }
      }
    }
  }
}

Variables Pattern — How $accountId Gets Its Value

In the Component JS
@api recordId; // platform sets this automatically

// Getter returns variables object
get variables() {
  return { accountId: this.recordId };
}

// '$variables' tells wire to call this.variables
@wire(graphql, {
  query: gql`query AccountDetail($accountId: ID!) { ... }`,
  variables: '$variables'  // ← the reactive link
})
handleResult(result) { ... }
What happens at runtime
// User is on Account record: 001abc123...
// Platform sets: this.recordId = "001abc123..."

// variables getter returns:
{ accountId: "001abc123..." }

// Wire runs query with:
where: { Id: { eq: "001abc123..." } }

// User navigates to different record?
// recordId changes → getter returns new value
// → wire auto-reruns! No manual refresh needed.

Mapping the Nested Response

const edge = data.uiapi.query.Account.edges[0]; // detail page = one record
if (!edge) { this.error = 'Account not found.'; return; }

const node = edge.node;

// 1. Parent account
this.account = {
  Id:       node.Id,
  Name:     node.Name.value,
  Industry: node.Industry?.value ?? '—',
  Revenue:  node.AnnualRevenue?.displayValue ?? '—'  // displayValue = "$1,500,000"
};

// 2. Child contacts — nested edges.map() inside parent node
this.contacts = node.Contacts.edges.map(e => ({
  Id:    e.node.Id,
  Name:  e.node.Name?.value ?? '—',
  Title: e.node.Title?.value ?? '—',
  Email: e.node.Email?.value ?? '—'
}));

// 3. Child opportunities
this.opportunities = node.Opportunities.edges.map(e => ({
  Id:        e.node.Id,
  Name:      e.node.Name?.value ?? '—',
  StageName: e.node.StageName?.value ?? '—',
  Amount:    e.node.Amount?.displayValue ?? '—',   // "$250,000"
  CloseDate: e.node.CloseDate?.displayValue ?? '—'  // "6/30/2025"
}));
⚡ Component 3
Native GraphQL Mutations — No uiRecordApi Needed

The third evolution: instead of mixing GraphQL reads with uiRecordApi writes, this component does everything in GraphQL — including Create, Update, and Delete — using the newer lightning/graphql module and executeMutation.

🔄
Module change: lightning/graphql vs lightning/uiGraphQLApi Components 1 & 2 import from lightning/uiGraphQLApi. This component imports from lightning/graphql — a newer module that adds executeMutation support and a different refresh mechanism. The @wire(graphql) syntax is the same; what changes is how you refresh and how you write data.

New Concepts in This Component

⚡ executeMutation — Native GraphQL Writes

Instead of calling createRecord() / updateRecord() / deleteRecord() from uiRecordApi, you write a mutation GraphQL operation and execute it with executeMutation({ query: mutation }). The write goes through the GraphQL API end-to-end — same transport, same namespace, consistent mental model.

🔄 refresh from Wire Result (not refreshGraphQL import)

In Components 1 & 2, you imported refreshGraphQL and called refreshGraphQL(this.wiredResult). In this pattern, the wire result itself hands you a refresh function: const { data, errors, refresh } = result. Store it and call it directly: await this.refreshGraphQL(). Cleaner API.

🏗️ Mutation Structure — uiapi Namespace Again

Mutations live inside the same uiapi { } namespace. The pattern is: ObjectCreate, ObjectUpdate, ObjectDelete. Each takes an input argument and returns a Record { } with the created/updated fields — or just Id for deletes.

🛡️ gqlString() — Safe Value Injection

Since values are interpolated directly into the gql template literal (e.g. ${safeName}), you must escape them first. JSON.stringify(raw) wraps the value in quotes and escapes any special characters — preventing injection issues with names like O'Brien "Co".

🗑️ LDS Auto-Prune on Delete

When you delete a record via GraphQL mutation, Salesforce's Lightning Data Service (LDS) cache automatically removes it from all active wire subscriptions. You don't need to call refresh after a delete — the list updates itself. (Calling refresh is still safe, just not required.)

Import: lightning/graphql vs lightning/uiGraphQLApi

Components 1 & 2 (uiGraphQLApi)
import { gql, graphql, refreshGraphQL }
  from 'lightning/uiGraphQLApi';

// Refresh: pass stored result reference
wiredResult;
handleQuery(result) {
  this.wiredResult = result;
}
await refreshGraphQL(this.wiredResult);

// Write: use separate uiRecordApi
import { createRecord } from 'lightning/uiRecordApi';
Component 3 (lightning/graphql)
import { gql, graphql, executeMutation }
  from 'lightning/graphql';

// Refresh: function comes FROM wire result
refreshGraphQL;
handleQuery(result) {
  const { data, errors, refresh } = result;
  if (refresh) this.refreshGraphQL = refresh;
}
await this.refreshGraphQL?.();

// Write: executeMutation — no uiRecordApi!

Mutation Anatomy — All Three Operations

// 1. Escape user input first — NEVER inject raw strings
const safeName = JSON.stringify(this.newName.trim()); // "Acme Corp" → '"Acme Corp"'

// 2. Build the mutation using template literal injection
const mutation = gql`
  mutation CreateAccount {
    uiapi {
      AccountCreate(input: {        // ObjectCreate pattern
        Account: { Name: ${`${`${`safeName`}`}`} }      // injected as: "Acme Corp"
      }) {
        Record {                     // returns the newly created record
          Id
          Name { value }
        }
      }
    }
  }
\`;

// 3. Execute and handle errors
const result = await executeMutation({ query: mutation });
if (result?.errors?.length) throw new Error(result.errors[0].message);

// 4. Refresh the wire query to show the new record
await this.refreshGraphQL?.();
// Update requires both Id + the fields to change
const safeId   = JSON.stringify(id);
const safeName = JSON.stringify(current.Name.toUpperCase());

const mutation = gql`
  mutation UpdateAccount {
    uiapi {
      AccountUpdate(input: {        // ObjectUpdate pattern
        Id: ${`${`${`safeId`}`}`},                  // which record to update
        Account: { Name: ${`${`${`safeName`}`}`} }  // what to change
      }) {
        Record {                     // returns the updated record
          Id
          Name { value }
        }
      }
    }
  }
\`;

const result = await executeMutation({ query: mutation });
// LDS cache often updates automatically for updates, but refresh to be safe
await this.refreshGraphQL?.();
// Delete only needs the Id — no other fields required
const safeId = JSON.stringify(id);

const mutation = gql`
  mutation DeleteAccount {
    uiapi {
      AccountDelete(input: { Id: ${`${`${`safeId`}`}`} }) {  // ObjectDelete pattern
        Id   // returns just the Id of the deleted record
      }
    }
  }
\`;

const result = await executeMutation({ query: mutation });

// 💡 LDS auto-prunes deleted records from all active wires
// No explicit refresh needed — the list updates automatically!
// WHY: If a user types:  O'Brien "Co" \
// and you inject it raw: Name: O'Brien "Co" \
// → GraphQL parse error (broken string literal)

// gqlString() helper uses JSON.stringify() to escape safely:

gqlString(raw) {
  return JSON.stringify(raw ?? '');
}

// Examples:
// "Acme Corp"           →  '"Acme Corp"'        ✅ plain string
// "O'Brien Co"          →  '"O\'Brien Co"'       ✅ apostrophe escaped
// 'Test "Name"'         →  '"Test \\"Name\\""'   ✅ quotes escaped
// "Backslash \\ test"   →  '"Backslash \\\\ test"' ✅ backslash escaped

// Always escape BEFORE injecting into gql template literals
// This is your protection against GraphQL injection

Side-by-Side: uiRecordApi vs GraphQL Mutations

OperationuiRecordApi (Components 1 & 2)GraphQL Mutation (Component 3)
CreatecreateRecord({ apiName, fields })AccountCreate(input: { Account: { Name: "…" } })
UpdateupdateRecord({ fields: { Id, Name } })AccountUpdate(input: { Id: "…", Account: { Name: "…" } })
DeletedeleteRecord(id)AccountDelete(input: { Id: "…" })
Refresh afterrefreshGraphQL(this.wiredResult)this.refreshGraphQL() (from wire result)
Auto-refresh on deleteNo — must call refreshGraphQLYes — LDS prunes automatically
Import modulelightning/uiGraphQLApi + lightning/uiRecordApilightning/graphql (single import)
Mental modelMixed: GraphQL reads + REST-style writesUnified: everything is GraphQL
When to use which approach? Use uiRecordApi when you need field-level validation, record type support, or are already using standard LWC patterns. Use GraphQL mutations when you want a fully unified GraphQL model, or when you're building components where the read and write logic should share the same transport and namespace.
Query Patterns

Common GraphQL query structures you'll use in Salesforce LWC projects.

📘
Basic QueryFetch records of any sObject, asking for exactly the fields you need. Server returns only those — nothing extra.
query BasicContacts {
  uiapi {
    query {
      Contact {
        edges {
          node {
            Id
            FirstName { value }
            LastName { value }
            Email { value }
          }
        }
      }
    }
  }
}
// You asked for 3 fields. Server returns 3 fields.
// REST would return 50+ fields. GraphQL is precise.
🔵
Where FilterFilter records with eq, like, in, gt, lt, and, or. Equivalent to SOQL WHERE clause.
query FilteredAccounts {
  uiapi {
    query {
      Account(
        where: {
          // Single field condition:
          Industry: { eq: "Technology" }

          // AND multiple conditions:
          and: [
            { Industry: { eq: "Technology" } }
            { AnnualRevenue: { gt: 1000000 } }
          ]

          // OR conditions:
          or: [
            { Industry: { eq: "Technology" } }
            { Industry: { eq: "Finance" } }
          ]

          // Contains (LIKE %word%):
          Name: { like: "%Salesforce%" }

          // Null check:
          AnnualRevenue: { isNull: false }
        }
      ) {
        edges { node { Id Name { value } } }
      }
    }
  }
}
// Operators: eq, ne, lt, lte, gt, gte, like, in, nin, isNull
🟠
Sort + LimitControl record count and ordering. Combine with where for powerful queries.
query Top10Accounts {
  uiapi {
    query {
      Account(
        first: 10           // LIMIT — max records returned
        orderBy: {         // ORDER BY
          Name: { order: ASC }   // ASC or DESC
        }
      ) {
        edges {
          node {
            Id
            Name { value }
            CreatedDate { value displayValue }
          }
        }
      }
    }
  }
}

// Multi-field sort (primary + secondary):
// orderBy: { Industry: { order: ASC } Name: { order: ASC } }
🟣
Cursor PaginationLoad the next page of results using a cursor — a bookmark pointing to where you left off. Perfect for "Load More" buttons.
// With a $after variable (cursor from previous page)
query PaginatedAccounts($after: String) {
  uiapi {
    query {
      Account(
        first: 10
        after: $after   // pass cursor from previous response
      ) {
        edges {
          cursor        // bookmark for this specific record
          node {
            Id
            Name { value }
          }
        }
        pageInfo {
          hasNextPage   // true if more records exist
          endCursor     // pass this as $after for next page
        }
        totalCount    // total matching records overall
      }
    }
  }
}
// In JS: pass variables: { after: this.endCursor } to @wire
🏗️
Nested Children — The GraphQL SuperpowerFetch parent + multiple child relationships in ONE network call. With REST, this would be 3+ API calls.
query Account360($accountId: ID!) {
  uiapi {
    query {
      Account(where: { Id: { eq: $accountId } }) {
        edges {
          node {
            Id
            Name { value }
            Industry { value }

            // Child relationship — same edges/node pattern
            Contacts(first: 50 orderBy: { LastName: { order: ASC } }) {
              edges {
                node {
                  Id
                  Name { value }
                  Email { value }
                }
              }
              totalCount  // total contacts for this account
            }

            Opportunities(
              first: 50
              orderBy: { CloseDate: { order: DESC } }
              where: { IsClosed: { eq: false } }
            ) {
              edges {
                node {
                  Id
                  Name { value }
                  Amount { value displayValue }
                  StageName { value }
                }
              }
              totalCount
            }
          }
        }
      }
    }
  }
}
// REST equivalent: GET /accounts/id + GET /contacts?AccountId=id
//                + GET /opportunities?AccountId=id = 3 calls
// GraphQL: 1 call. Done.
Multi-Object QueryFetch multiple unrelated objects in a single GraphQL call — great for dashboard components.
query DashboardData {
  uiapi {
    query {
      // Object 1 — same query, side by side
      Account(first: 5 orderBy: { Name: { order: ASC } }) {
        edges { node { Id Name { value } Industry { value } } }
      }

      // Object 2 — completely separate object in the same request
      Contact(first: 5) {
        edges { node { Id FirstName { value } LastName { value } } }
      }

      // Object 3 — with its own filter
      Opportunity(
        first: 5
        where: { StageName: { eq: "Closed Won" } }
      ) {
        edges { node { Id Name { value } Amount { displayValue } } }
      }
    }
  }
}
// 3 objects, 1 API call. REST equivalent: 3 separate calls.
🎯
VariablesMake queries dynamic. Pass runtime values (like recordId, search terms, dates) without rewriting the query string.
// STEP 1: Declare variable in query ($ prefix, type with !)
query FilterByStage($stage: String!, $minAmount: Decimal) {
  uiapi {
    query {
      Opportunity(
        where: {
          StageName: { eq: $stage }
          Amount: { gte: $minAmount }
        }
      ) {
        edges { node { Id Name { value } Amount { displayValue } } }
      }
    }
  }
}

// STEP 2: In your LWC JS — reactive getter
get variables() {
  return {
    stage: this.selectedStage,      // from UI picklist
    minAmount: this.minimumAmount   // from UI input
  };
}

// STEP 3: Wire it up
@wire(graphql, {
  query: gql`...the query above...`,
  variables: '$variables'  // reactive — reruns when getter returns new value
})
handleResult(result) { ... }
Complete Data Flow

From import to template — every step in the lifecycle of a Salesforce GraphQL component.

1
Import the three tools
Everything you need comes from one module. gql is a tagged template literal that marks your query string. graphql is the wire adapter. refreshGraphQL forces a re-fetch after mutations.
import { gql, graphql, refreshGraphQL } from 'lightning/uiGraphQLApi';
2
(Optional) Define a variables getter for dynamic queries
If your query needs runtime values (a record ID, a filter, a search term), put them in a getter. The '$variables' string in @wire tells the framework to watch this getter reactively.
get variables() { return { accountId: this.recordId }; }
3
Declare the @wire
The wire registers a reactive subscription. Salesforce runs the query on component load and automatically re-runs it when variables change or refreshGraphQL is called.
@wire(graphql, { query: gql`...`, variables: '$variables' })
handleResult(result) { ... }
4
Store the wire result reference
Critical step. Save result into a class property. You'll need this reference later to call refreshGraphQL() after any create/update/delete. Without it, you can't force a refresh.
this.wiredResult = result; // store FIRST, then process
const { data, errors } = result;
5
Map nested response to flat objects
The raw GraphQL response is deeply nested (edges → node → field.value). Use .map() to extract and flatten into simple objects your template can use without any property chaining.
this.accounts = data.uiapi.query.Account.edges.map(e => ({
  Id:       e.node.Id,
  Name:     e.node.Name.value,
  Industry: e.node.Industry?.value ?? '—'
}));
6
Mutate via uiRecordApi, then refresh
GraphQL is read-only. Writes go through uiRecordApi. After every mutation, call refreshGraphQL(this.wiredResult) to re-run the @wire query and sync the UI with the new data.
await createRecord({ apiName: 'Account', fields: { Name: 'Acme' } });
await refreshGraphQL(this.wiredResult); // triggers step 4-5 again
7
Template iterates the clean flat array
Because of the .map() in step 5, the template just uses clean field names. No nested access, no .value — just {acct.Name}.
<template for:each={accounts} for:item="acct">
  <tr key={acct.Id}>
    <td>{acct.Name}</td>     <!-- clean, not {acct.Name.value} -->
    <td>{acct.Industry}</td>
  </tr>
</template>
Cheat Sheet

Everything in one place. Bookmark this page.

ConceptSyntaxNotes
Importimport { gql, graphql, refreshGraphQL } from 'lightning/uiGraphQLApi'All 3 from the same module
Wire decorator@wire(graphql, { query: gql`...` })Runs on load, re-runs on variable change
Named queryquery MyQueryName { ... }Always name queries — helps debugging
API gatewayuiapi { query { ... } }Always the outer shell in Salesforce GraphQL
Limit recordsAccount(first: 10)Like SOQL LIMIT — always set this
SortorderBy: { Name: { order: ASC } }ASC or DESC. Multiple fields supported
Filter equalswhere: { Industry: { eq: "Technology" } }Like WHERE in SOQL
Filter containsName: { like: "%Acme%" }% is wildcard — like SOQL LIKE
AND / ORand: [...], or: [...]Array of condition objects
List wrapperedges { node { ... } }Always required for list results
Id fieldIdPlain string — no { value } needed
Standard fieldName { value }All fields need { value } in Salesforce GraphQL
Formatted valueAmount { value displayValue }displayValue = "$1,500,000" (locale-formatted)
Variable declarequery MyQuery($id: ID!) { ... }! = required. Types: ID!, String, Decimal, Boolean
Variable usewhere: { Id: { eq: $id } }$ prefix when using the variable
Reactive variablesvariables: '$variables'Calls this.variables getter; reruns on change
Single recordedges[0].nodeDetail pages — always guard with null check
Total counttotalCountAlongside edges — total matching records
Pagination infopageInfo { hasNextPage endCursor }For Load More / infinite scroll
Null-safe JSfield?.value ?? 'fallback'Always use for nullable fields
Force refreshawait refreshGraphQL(this.wiredResult)Call after every create/update/delete
Error handlingconst { data, errors } = resultCheck both — can have partial errors
⚡ GraphQL Mutations (lightning/graphql module)
Mutation importimport { gql, graphql, executeMutation } from 'lightning/graphql'Different module from uiGraphQLApi
Refresh from wireconst { data, errors, refresh } = result; this.fn = refresh;No need to import refreshGraphQL separately
Create mutationAccountCreate(input: { Account: { Name: "…" } }) { Record { Id } }Inside uiapi { } namespace
Update mutationAccountUpdate(input: { Id: "…", Account: { Name: "…" } }) { Record { Id } }Id + fields to change
Delete mutationAccountDelete(input: { Id: "…" }) { Id }Returns Id of deleted record
Execute mutationconst result = await executeMutation({ query: mutation })mutation is built with gql``
String safetyJSON.stringify(rawValue)Always escape before gql template injection
LDS auto-prune(no code needed after delete)Deletes auto-remove from wire — no refresh required
⚠️
The 5 Salesforce GraphQL Gotchas 1. Always { value } — every field (except Id) is wrapped. Name.value, not Name.
2. GraphQL reads are read-only by default — use uiRecordApi or executeMutation (lightning/graphql) for writes.
3. Store refresh reference — either this.wiredResult = result (uiGraphQLApi) or capture refresh from wire result (lightning/graphql).
4. Wrong module for mutationsexecuteMutation only exists in lightning/graphql, not lightning/uiGraphQLApi.
5. Escape mutation inputs — always JSON.stringify() before injecting user values into gql template literals.