Salesforce Data Access Data SDK Requirement All Salesforce data access MUST use the Data SDK ( @salesforce/sdk-data ). The SDK handles authentication, CSRF, and base URL resolution. import { createDataSDK , gql } from "@salesforce/sdk-data" ; import type { ResponseTypeQuery } from "../graphql-operations-types" ; const sdk = await createDataSDK ( ) ; // GraphQL for record queries/mutations (PREFERRED) const response = await sdk . graphql ?. < ResponseTypeQuery
( query , variables ) ; // REST for Connect REST, Apex REST, UI API (when GraphQL insufficient) const res = await sdk . fetch ?. ( "/services/apexrest/my-resource" ) ; Always use optional chaining ( sdk.graphql?.() , sdk.fetch?.() ) — these methods may be undefined in some surfaces. Preconditions — verify before starting
Requirement
How to verify
If missing
1
@salesforce/sdk-data
installed
Check
package.json
in the UI bundle dir
Cannot proceed — tell user to install it
2
schema.graphql
at project root
Check if file exists
Run
npm run graphql:schema
from UI bundle dir
3
Custom objects/fields deployed
Run
graphql-search.sh
Multiple entities:
bash
scripts/graphql-search.sh Account Contact Opportunity
The script outputs seven sections per entity:
Type definition
— all queryable fields and relationships
Filter options
— available fields for
where:
conditions
Sort options
— available fields for
orderBy:
Create mutation wrapper
—
Parent relationship (non-polymorphic)
Owner @optional { Name { value } }
Parent relationship (polymorphic — use fragments)
What @optional { ... WhatAccount ... WhatOpportunity }
Child relationship — max 1 level, no grandchildren
Contacts @optional ( first : 10 ) { edges { node { Name @optional { value } } } } } } pageInfo { hasNextPage endCursor } } } } } fragment WhatAccount on Account { Id Name @optional { value } } fragment WhatOpportunity on Opportunity { Id Name @optional { value } } Consuming code must defend against missing fields: const name = node . Name ?. value ?? "" ; const relatedName = node . Owner ?. Name ?. value ?? "N/A" ; Filtering
Implicit AND
Account ( where : { Industry : { eq : "Technology" } , AnnualRevenue : { gt : 1000000 } } )
Explicit OR
Account ( where : { OR : [ { Industry : { eq : "Technology" } } , { Industry : { eq : "Finance" } } ] } )
NOT
Account ( where : { NOT : { Industry : { eq : "Technology" } } } )
Date literal
Opportunity ( where : { CloseDate : { eq : { value : "2024-12-31" } } } )
Relative date
Opportunity ( where : { CloseDate : { gte : { literal : TODAY } } } )
Relationship filter (nested objects, NOT dot notation)
Contact ( where : { Account : { Name : { like : "Acme%" } } } )
Polymorphic relationship filter
Account ( where : { Owner : { User : { Username : { like : "admin%" } } } } ) String equality ( eq ) is case-insensitive. Both 15-char and 18-char record IDs are accepted. Ordering Account ( first : 10 , orderBy : { Name : { order : ASC } , CreatedDate : { order : DESC } } ) { ... } Unsupported for ordering: multi-select picklist, rich text, long text area, encrypted fields. Add Id as tie-breaker for deterministic ordering. UpperBound Pagination (v59+) For >200 records per page or >4,000 total records, use upperBound . first must be 200–2000 when set. Account ( first : 2000 , after : $cursor , upperBound : 10000 ) { edges { node { Id Name @optional { value } } } pageInfo { hasNextPage endCursor } } Semi-Join and Anti-Join Filter a parent entity by conditions on child entities using inq (semi-join) or ninq (anti-join) on the parent's Id . If the only condition is child existence, use Id: { ne: null } . query SemiJoinExample { uiapi { query { Account ( where : { Id : { inq : { Contact : { LastName : { like : "Smith%" } } ApiName : "AccountId" } } } , first : 10 ) { edges { node { Id Name @optional { value } } } } } } } Replace inq with ninq for anti-join. Restrictions: no OR in subquery, no orderBy in subquery, no nesting joins within each other. Current User Use uiapi.currentUser (no arguments) instead of the standard query pattern: query CurrentUser { uiapi { currentUser { Id Name { value } } } } Field Value Wrappers Schema fields use typed wrappers — access via .value : Wrapper Type Underlying Wrapper Type Underlying StringValue String BooleanValue Boolean IntValue Int DoubleValue Double CurrencyValue Currency PercentValue Percent DateTimeValue DateTime DateValue Date PicklistValue Picklist LongValue Long IDValue ID TextAreaValue TextArea EmailValue Email PhoneNumberValue PhoneNumber UrlValue Url All wrappers also expose displayValue: String (server-rendered via toLabel() / format() ) — use for UI display instead of formatting client-side. Mutation Template Mutations are GA in API v66+. Three operations: Create , Update , Delete .
Create
mutation CreateAccount ( $input : AccountCreateInput ! ) { uiapi ( input : { allOrNone : true } ) { AccountCreate ( input : $input ) { Record { Id Name { value } } } } }
Update — must include Id
- mutation
- UpdateAccount
- {
- uiapi
- (
- input
- :
- {
- allOrNone
- :
- true
- }
- )
- {
- AccountUpdate
- (
- input
- :
- {
- Id
- :
- "001xx000003GYkZAAW"
- ,
- Account
- :
- {
- Name
- :
- "New Name"
- }
- }
- )
- {
- Record
- {
- Id
- Name
- {
- value
- }
- }
- }
- }
- }
- Input constraints:
- Create
-
- Required fields (unless
- defaultedOnCreate
- ), only
- createable
- fields, no child relationships. Reference fields set by
- ApiName
- (e.g.,
- AccountId
- ).
- Update
-
- Must include
- Id
- , only
- updateable
- fields, no child relationships.
- Delete
- :
- Id
- only.
- IdOrRef
- type
-
- The
- Id
- field in Update and Delete inputs uses the
- IdOrRef
- type, which accepts either a literal record ID (e.g.,
- "001xx..."
- ) or a mutation chaining reference (
- "@{Alias}"
- ). Reference fields in Create inputs (e.g.,
- AccountId
- ) also accept
- @{Alias}
- for chaining.
- Raw values
- No commas, currency symbols, or locale formatting (e.g.,
80000
not
"$80,000"
).
Output constraints:
Create/Update: Exclude child relationships, exclude navigated reference fields (only
ApiName
member allowed). Output field is always named
Record
.
Delete:
Id
only.
allOrNone
semantics:
true
(default) — All operations succeed or all roll back.
false
— Independent operations succeed individually, but dependent operations (using
@{alias}
) still roll back together.
Mutation Chaining
Chain related mutations using
@{alias}
references to
Id
from earlier mutations. Required for parent-child creation (nested child creates are not supported).
mutation
CreateAccountAndContact
{
uiapi
(
input
:
{
allOrNone
:
true
}
)
{
AccountCreate
(
input
:
{
Account
:
{
Name
:
"Acme"
}
}
)
{
Record
{
Id
}
}
ContactCreate
(
input
:
{
Contact
:
{
LastName
:
"Smith"
,
AccountId
:
"@{AccountCreate}"
}
}
)
{
Record
{
Id
}
}
}
}
Rules:
A
must come before
B
in the query.
@{A}
is always the
Id
from mutation
A
. Only
Create
or
Delete
can be chained from (not
Update
).
Delete Mutation
Delete uses generic
RecordDeleteInput
(not entity-specific). Output is
Id
only — no
Record
field.
mutation
DeleteAccount
(
$id
:
ID
!
)
{
uiapi
(
input
:
{
allOrNone
:
true
}
)
{
AccountDelete
(
input
:
{
Id
:
$id
}
)
{
Id
}
}
}
Object Metadata & Picklist Values
Use
uiapi { objectInfos(...) }
to fetch field metadata or picklist values. Pass
either
apiNames
or
objectInfoInputs
— never both.
// Object metadata
const
GET_OBJECT_INFO
=
gql
query GetObjectInfo($apiNames: [String!]!) { uiapi { objectInfos(apiNames: $apiNames) { ApiName label labelPlural fields { ApiName label dataType updateable createable } } } }; // Picklist values (use objectInfoInputs + inline fragment) const GET_PICKLIST_VALUES = gqlquery GetPicklistValues($objectInfoInputs: [ObjectInfoInput!]!) { uiapi { objectInfos(objectInfoInputs: $objectInfoInputs) { ApiName fields { ApiName ... on PicklistField { picklistValuesByRecordTypeIDs { recordTypeID picklistValues { label value } } } } } } }; Step 4: Generate Types (codegen) After writing the query (whether in a .graphql file or inline with gql ), generate TypeScript types:
Run from UI bundle dir
- npm
- run graphql:codegen
- Output:
- src/api/graphql-operations-types.ts
- Generated type naming conventions:
Query - /
Mutation - — response types
QueryVariables - /
MutationVariables - — variable types
- Always import and use the generated types
- when calling
- sdk.graphql
- :
- import
- type
- {
- GetAccountsQuery
- ,
- GetAccountsQueryVariables
- }
- from
- "../graphql-operations-types"
- ;
- const
- response
- =
- await
- sdk
- .
- graphql
- ?.
- <
- GetAccountsQuery
- ,
- GetAccountsQueryVariables
- >
- (
- GET_ACCOUNTS
- ,
- variables
- )
- ;
- Use
- NodeOfConnection
- to extract the node type from a Connection for cleaner typing:
- import
- {
- type
- NodeOfConnection
- }
- from
- "@salesforce/sdk-data"
- ;
- type
- AccountNode
- =
- NodeOfConnection
- <
- GetAccountsQuery
- [
- "uiapi"
- ]
- [
- "query"
- ]
- [
- "Account"
- ]
- >
- ;
- Step 5: Validate & Test
- Lint
- :
- npx eslint
- from UI bundle dir
- codegen
- :
- npm run graphql:codegen
- from UI bundle dir
- Common Error patterns
- Error Contains
- Resolution
- Cannot query field
- /
- ValidationError
- Field name wrong — re-run
- graphql-search.sh
- Unknown type
- Type name wrong — verify PascalCase entity name via script
- Unknown argument
- Argument wrong — check Filter/OrderBy sections in script output
- invalid syntax
- /
- InvalidSyntax
- Fix syntax per error message
- VariableTypeMismatch
- /
- UnknownType
- Correct argument type from schema
- invalid cross reference id
- Entity deleted — ask for valid Id
- OperationNotSupported
- Check object availability and API version
- is not currently available in mutation results
- Remove field from mutation output
- Cannot invoke JsonElement.isJsonObject()
- Use API version 64+ for update mutation
- Record
- selection
- On PARTIAL
- If a mutation returns both data and errors (partial success): Report inaccessible fields, explain they cannot be in mutation output, offer to remove them.
- Wait for user consent
- before changing.
- UI Bundle Integration (React)
- Two integration patterns:
- Pattern 1 — External
- .graphql
- file (complex queries)
- One operation per
- .graphql
- file.
- Each file contains exactly one
- query
- or
- mutation
- (plus its fragments). Do not combine multiple operations in a single file.
- import
- {
- createDataSDK
- ,
- type
- NodeOfConnection
- }
- from
- "@salesforce/sdk-data"
- ;
- import
- MY_QUERY
- from
- "./query/myQuery.graphql?raw"
- ;
- // ?raw suffix required
- import
- type
- {
- GetMyDataQuery
- ,
- GetMyDataQueryVariables
- }
- from
- "../graphql-operations-types"
- ;
- const
- sdk
- =
- await
- createDataSDK
- (
- )
- ;
- const
- response
- =
- await
- sdk
- .
- graphql
- ?.
- <
- GetMyDataQuery
- ,
- GetMyDataQueryVariables
- >
- (
- MY_QUERY
- ,
- variables
- )
- ;
- After creating/changing
- .graphql
- files, run
- npm run graphql:codegen
- to generate types into
- src/api/graphql-operations-types.ts
- .
- Pattern 2 — Inline
- gql
- tag (simple queries)
- Must use
- gql
- — plain template strings bypass ESLint schema validation.
- import
- {
- createDataSDK
- ,
- gql
- }
- from
- "@salesforce/sdk-data"
- ;
- import
- type
- {
- GetAccountsQuery
- }
- from
- "../graphql-operations-types"
- ;
- const
- GET_ACCOUNTS
- =
- gql
- `
- query GetAccounts {
- uiapi {
- query {
- Account(first: 10) {
- edges { node { Id Name @optional { value } } }
- }
- }
- }
- }
- `
- ;
- const
- sdk
- =
- await
- createDataSDK
- (
- )
- ;
- const
- response
- =
- await
- sdk
- .
- graphql
- ?.
- <
- GetAccountsQuery
- >
- (
- GET_ACCOUNTS
- )
- ;
- Error Handling
- // Strict (default) — any errors = failure
- if
- (
- response
- ?.
- errors
- ?.
- length
- )
- {
- throw
- new
- Error
- (
- response
- .
- errors
- .
- map
- (
- e
- =>
- e
- .
- message
- )
- .
- join
- (
- "; "
- )
- )
- ;
- }
- // Tolerant — log errors, use available data
- if
- (
- response
- ?.
- errors
- ?.
- length
- )
- {
- console
- .
- warn
- (
- "GraphQL partial errors:"
- ,
- response
- .
- errors
- )
- ;
- }
- // Discriminated — fail only when no data returned
- if
- (
- !
- response
- ?.
- data
- &&
- response
- ?.
- errors
- ?.
- length
- )
- {
- throw
- new
- Error
- (
- response
- .
- errors
- .
- map
- (
- e
- =>
- e
- .
- message
- )
- .
- join
- (
- "; "
- )
- )
- ;
- }
- const
- accounts
- =
- response
- ?.
- data
- ?.
- uiapi
- ?.
- query
- ?.
- Account
- ?.
- edges
- ?.
- map
- (
- e
- =>
- e
- .
- node
- )
- ??
- [
- ]
- ;
- REST API Patterns
- Use
- sdk.fetch
- when GraphQL is insufficient. See the
- Supported APIs
- table for the full allowlist.
- declare
- const
- SF_API_VERSION
- :
- string
- ;
- const
- API_VERSION
- =
- typeof
- SF_API_VERSION
- !==
- "undefined"
- ?
- SF_API_VERSION
- :
- "65.0"
- ;
- // Connect — file upload config
- const
- res
- =
- await
- sdk
- .
- fetch
- ?.
- (
- `
- /services/data/v
- ${
- API_VERSION
- }
- /connect/file/upload/config
- `
- )
- ;
- // Apex REST (no version in path)
- const
- res
- =
- await
- sdk
- .
- fetch
- ?.
- (
- "/services/apexrest/auth/login"
- ,
- {
- method
- :
- "POST"
- ,
- body
- :
- JSON
- .
- stringify
- (
- {
- ,
- password
- }
- )
- ,
- headers
- :
- {
- "Content-Type"
- :
- "application/json"
- }
- ,
- }
- )
- ;
- // UI API — record with metadata (prefer GraphQL for simple reads)
- const
- res
- =
- await
- sdk
- .
- fetch
- ?.
- (
- `
- /services/data/v
- ${
- API_VERSION
- }
- /ui-api/records/
- ${
- recordId
- }
- `
- )
- ;
- // Einstein LLM
- const
- res
- =
- await
- sdk
- .
- fetch
- ?.
- (
- `
- /services/data/v
- ${
- API_VERSION
- }
- /einstein/llm/prompt/generations
- `
- ,
- {
- method
- :
- "POST"
- ,
- body
- :
- JSON
- .
- stringify
- (
- {
- promptTextorId
- :
- prompt
- }
- )
- ,
- }
- )
- ;
- Current user
- Do not use Chatter (
/chatter/users/me
). Use GraphQL instead:
const
GET_CURRENT_USER
=
gql
query CurrentUser { uiapi { currentUser { Id Name { value } } } }; const response = await sdk . graphql ?. ( GET_CURRENT_USER ) ; Directory Structure/ ← SFDX project root ├── schema.graphql ← grep target (lives here) ├── sfdx-project.json ├── scripts/graphql-search.sh ← schema lookup script └── force-app/main/default/uiBundles/ / ← UI bundle dir ├── package.json ← npm scripts └── src/ Command Run From Why npm run graphql:schema UI bundle dir Script in UI bundle's package.json npm run graphql:codegen UI bundle dir Generate GraphQL types npx eslint UI bundle dir Reads eslint.config.js bash scripts/graphql-search.sh project root Schema lookup Quick Reference Schema Lookup (from project root) Run the search script to get all relevant schema info in one step: bash scripts/graphql-search.sh < EntityName Script Output Section Used For Type definition Field names, parent/child relationships Filter options where: conditions Sort options orderBy: CreateRepresentation Create mutation field list UpdateRepresentation Update mutation field list Error Categories Error Contains Resolution Cannot query field Field name is wrong — run graphql-search.sh
and use the exact name from the Type definition section Unknown type Type name is wrong — run graphql-search.sh to confirm the correct PascalCase entity name Unknown argument Argument name is wrong — run graphql-search.sh and check Filter or OrderBy sections invalid syntax Fix syntax per error message validation error Field name is wrong — run graphql-search.sh to verify VariableTypeMismatch Correct argument type from schema invalid cross reference id Entity deleted — ask for valid Id Checklist All field names verified via search script (Step 2) @optional applied to all record fields (reads) Mutations use uiapi(input: { allOrNone: ... }) wrapper first: specified in every query Optional chaining in consuming code errors array checked in response handling Lint passes: npx eslint