GraphQL Mutations in Data SDK

GraphQL mutations are operations that create, update, or delete Salesforce records. Use the graphql.mutate() method for all mutation operations.

Unlike queries, mutations:

  • Are never cached and don’t write to the cache
  • Return MutationResult<T> with only data and errors fields
  • Don’t provide subscription support
  • Don’t write to the cache, so they won’t affect subsequent query results
1const result = await dataSdk.graphql?.mutate<MutationResponse, MutationVariables>({
2  mutation: MUTATION_STRING,
3  variables: { ... },
4  operationName?: "..."
5});
6
7// Check for errors
8if (result?.errors?.length) {
9  throw new Error(result.errors.map((e) => e.message).join("; "));
10}
11
12// Use the returned data
13const createdRecord = result?.data;

Create Record 

Create a new record using the <ObjectName>Create mutation.

Step 1: Define the Mutation 

A mutation operation follows the RecordCreate name pattern, for example, AccountCreate. See Mutations Schema in the GraphQL API Developer Guide.

1# uiBundles/yourAppName/src/api/account/query/createAccount.graphql
2mutation CreateAccount($input: AccountCreateInput!) {
3  uiapi {
4    AccountCreate(input: $input) {
5      Record {
6        Id
7        Name {
8          value
9        }
10        Industry {
11          value
12        }
13        Phone {
14          value
15        }
16        Website {
17          value
18        }
19      }
20    }
21  }
22}

Step 2: Generate Types 

Run npm run graphql:codegen to generate TypeScript types.

1// Generated in src/api/graphql-operations-types.ts
2export type CreateAccountMutation = {
3  uiapi: {
4    AccountCreate: {
5      Record: {
6        Id: string;
7        Name: { value: string } | null;
8        Industry: { value: string } | null;
9        Phone: { value: string } | null;
10        Website: { value: string } | null;
11      };
12    };
13  };
14};
15
16export type CreateAccountMutationVariables = {
17  input: AccountCreateInput;
18};
19
20export type AccountCreateInput = {
21  Account: {
22    Name?: string;
23    Industry?: string;
24    Phone?: string;
25    Website?: string;
26    // ... other fields
27  };
28};

Step 3: Implement the Mutation Function 

Implement the client-side mutation function that invokes dataSdk.graphql?.mutate() with the generated types.

1// uiBundles/yourAppName/src/api/account/accountCreate.ts
2import { createDataSDK } from "@salesforce/platform-sdk/data";
3import CREATE_ACCOUNT from "./mutation/createAccount.graphql?raw";
4import type {
5  CreateAccountMutation,
6  CreateAccountMutationVariables,
7} from "../graphql-operations-types";
8
9export interface CreateAccountInput {
10  name: string;
11  industry?: string;
12  phone?: string;
13  website?: string;
14}
15
16export async function createAccount(input: CreateAccountInput) {
17  const dataSdk = await createDataSDK();
18
19  const result = await dataSdk.graphql?.mutate<
20    CreateAccountMutation,
21    CreateAccountMutationVariables
22  >({
23    mutation: CREATE_ACCOUNT,
24    variables: {
25      input: {
26        Account: {
27          Name: input.name,
28          Industry: input.industry,
29          Phone: input.phone,
30          Website: input.website,
31        },
32      },
33    },
34  });
35
36  if (result?.errors?.length) {
37    const errorMessages = result.errors.map((e) => e.message).join("; ");
38    throw new Error(`Failed to create account: ${errorMessages}`);
39  }
40
41  return result?.data?.uiapi?.AccountCreate?.Record;
42}

Step 4: Use in Your Component 

This component example shows how to call createAccount, handle loading and error states, and react to the created result.

1import { useState } from "react";
2import { createAccount } from "./api/accounts";
3
4function CreateAccountForm() {
5  const [name, setName] = useState("");
6  const [industry, setIndustry] = useState("");
7  const [loading, setLoading] = useState(false);
8  const [error, setError] = useState<string | null>(null);
9
10  async function handleSubmit(e: React.FormEvent) {
11    e.preventDefault();
12    setLoading(true);
13    setError(null);
14
15    try {
16      const newAccount = await createAccount({ name, industry });
17      console.log("Created account:", newAccount);
18      // Clear form
19      setName("");
20      setIndustry("");
21      // Optionally navigate or refresh data
22    } catch (err) {
23      setError((err as Error).message);
24    } finally {
25      setLoading(false);
26    }
27  }
28
29  return (
30    <form onSubmit={handleSubmit}>
31      <input
32        value={name}
33        onChange={(e) => setName(e.target.value)}
34        placeholder="Account Name"
35        required
36      />
37      <input
38        value={industry}
39        onChange={(e) => setIndustry(e.target.value)}
40        placeholder="Industry"
41      />
42      <button type="submit" disabled={loading}>
43        {loading ? "Creating..." : "Create Account"}
44      </button>
45      {error && <div className="error">{error}</div>}
46    </form>
47  );
48}

Update Record 

Update an existing record using the <ObjectName>Update mutation.

Step 1: Define the Mutation 

1# uiBundles/yourAppName/src/api/account/query/updateAccount.graphql
2mutation UpdateAccount($input: AccountUpdateInput!) {
3  uiapi {
4    AccountUpdate(input: $input) {
5      Record {
6        Id
7        Name {
8          value
9        }
10        Industry {
11          value
12        }
13        AnnualRevenue {
14          value
15        }
16        Phone {
17          value
18        }
19      }
20    }
21  }
22}

Step 2: Implement the Mutation Function 

Implement the update function: call dataSdk.graphql?.mutate() with the update payload, then check and handle any returned errors.

1import { createDataSDK } from "@salesforce/platform-sdk/data";
2import UPDATE_ACCOUNT from "./mutation/updateAccount.graphql?raw";
3import type {
4  UpdateAccountMutation,
5  UpdateAccountMutationVariables,
6} from "../graphql-operations-types";
7
8export interface UpdateAccountInput {
9  id: string;
10  name?: string;
11  industry?: string;
12  annualRevenue?: number;
13  phone?: string;
14}
15
16export async function updateAccount(input: UpdateAccountInput) {
17  const dataSdk = await createDataSDK();
18
19  const result = await dataSdk.graphql?.mutate<
20    UpdateAccountMutation,
21    UpdateAccountMutationVariables
22  >({
23    mutation: UPDATE_ACCOUNT,
24    variables: {
25      input: {
26        Id: input.id,
27        Account: {
28          Name: input.name,
29          Industry: input.industry,
30          AnnualRevenue: input.annualRevenue,
31          Phone: input.phone,
32        },
33      },
34    },
35  });
36
37  if (result?.errors?.length) {
38    const errorMessages = result.errors.map((e) => e.message).join("; ");
39    throw new Error(`Failed to update account: ${errorMessages}`);
40  }
41
42  return result?.data?.uiapi?.AccountUpdate?.Record;
43}

Step 3: Use in Your Component 

This form component demonstrates using updateAccount and calling onSuccess after a successful update, while displaying errors when present.

1// uiBundles/yourAppName/src/api/account/accountUpdate.ts
2import { useState, useEffect } from "react";
3import { updateAccount } from "./api/accounts";
4
5interface EditAccountFormProps {
6  accountId: string;
7  initialName: string;
8  initialIndustry?: string;
9  onSuccess?: () => void;
10}
11
12function EditAccountForm({
13  accountId,
14  initialName,
15  initialIndustry,
16  onSuccess,
17}: EditAccountFormProps) {
18  const [name, setName] = useState(initialName);
19  const [industry, setIndustry] = useState(initialIndustry || "");
20  const [loading, setLoading] = useState(false);
21  const [error, setError] = useState<string | null>(null);
22
23  async function handleSubmit(e: React.FormEvent) {
24    e.preventDefault();
25    setLoading(true);
26    setError(null);
27
28    try {
29      await updateAccount({
30        id: accountId,
31        name,
32        industry,
33      });
34      onSuccess?.();
35    } catch (err) {
36      setError((err as Error).message);
37    } finally {
38      setLoading(false);
39    }
40  }
41
42  return (
43    <form onSubmit={handleSubmit}>
44      <input
45        value={name}
46        onChange={(e) => setName(e.target.value)}
47        placeholder="Account Name"
48        required
49      />
50      <input
51        value={industry}
52        onChange={(e) => setIndustry(e.target.value)}
53        placeholder="Industry"
54      />
55      <button type="submit" disabled={loading}>
56        {loading ? "Updating..." : "Update Account"}
57      </button>
58      {error && <div className="error">{error}</div>}
59    </form>
60  );
61}

Delete Record 

Delete a record using the <ObjectName>Delete mutation.

Step 1: Define the Mutation 

Define the delete mutation in a .graphql file that accepts the delete input and returns the deleted record Id.

1# uiBundles/yourAppName/src/api/account/query/deleteAccount.graphql
2mutation DeleteAccount($input: AccountDeleteInput!) {
3  uiapi {
4    AccountDelete(input: $input) {
5      Id
6    }
7  }
8}

Delete mutations only return the ID of the deleted record, not a Record object.

Note

Step 2: Implement the Mutation Function 

Implement the delete function that calls dataSdk.graphql?.mutate(), validates the result, and returns the deleted ID to callers.

1import { createDataSDK } from "@salesforce/platform-sdk/data";
2import DELETE_ACCOUNT from "./mutation/deleteAccount.graphql?raw";
3import type {
4  DeleteAccountMutation,
5  DeleteAccountMutationVariables,
6} from "../graphql-operations-types";
7
8export async function deleteAccount(id: string): Promise<string> {
9  const dataSdk = await createDataSDK();
10
11  const result = await dataSdk.graphql?.mutate<
12    DeleteAccountMutation,
13    DeleteAccountMutationVariables
14  >({
15    mutation: DELETE_ACCOUNT,
16    variables: {
17      input: {
18        Id: id,
19      },
20    },
21  });
22
23  if (result?.errors?.length) {
24    const errorMessages = result.errors.map((e) => e.message).join("; ");
25    throw new Error(`Failed to delete account: ${errorMessages}`);
26  }
27
28  return result?.data?.uiapi?.AccountDelete?.Id || "";
29}

Step 3: Use in Your Component 

This example shows a button component that calls deleteAccount, displays progress, and handles any error returned by the mutation.

1// uiBundles/yourAppName/src/api/account/accountDelete.ts
2import { useState } from "react";
3import { deleteAccount } from "./api/accounts";
4
5interface DeleteAccountButtonProps {
6  accountId: string;
7  accountName: string;
8  onSuccess?: () => void;
9}
10
11function DeleteAccountButton({ accountId, accountName, onSuccess }: DeleteAccountButtonProps) {
12  const [loading, setLoading] = useState(false);
13  const [error, setError] = useState<string | null>(null);
14
15  async function handleDelete() {
16    const confirmed = window.confirm(`Are you sure you want to delete "${accountName}"?`);
17
18    if (!confirmed) return;
19
20    setLoading(true);
21    setError(null);
22
23    try {
24      await deleteAccount(accountId);
25      onSuccess?.();
26    } catch (err) {
27      setError((err as Error).message);
28    } finally {
29      setLoading(false);
30    }
31  }
32
33  return (
34    <div>
35      <button onClick={handleDelete} disabled={loading} className="danger-button">
36        {loading ? "Deleting..." : "Delete"}
37      </button>
38      {error && <div className="error">{error}</div>}
39    </div>
40  );
41}

Refresh Queries After Mutations 

Since mutations don’t update the cache, you need to manually refresh related queries to reflect the changes in the UI.

Pattern 1: Refresh with QueryResult 

If you have access to the QueryResult object from a previous query, call refresh():

1import { useEffect, useState } from "react";
2import { createDataSDK } from "@salesforce/platform-sdk/data";
3import { createAccount } from "./api/accounts";
4import type { QueryResult } from "@salesforce/platform-sdk/data";
5
6function AccountManager() {
7  const [accountsResult, setAccountsResult] = useState<QueryResult<AccountsData>>();
8  const [accounts, setAccounts] = useState<Account[]>([]);
9
10  useEffect(() => {
11    async function loadAccounts() {
12      const dataSdk = await createDataSDK();
13      const result = await dataSdk.graphql?.query<AccountsData>({
14        query: GET_ACCOUNTS,
15      });
16
17      setAccountsResult(result);
18      setAccounts(result?.data?.uiapi?.query?.Account?.edges?.map((e) => e.node) || []);
19
20      // Subscribe to updates
21      result?.subscribe((snapshot) => {
22        setAccounts(snapshot.data?.uiapi?.query?.Account?.edges?.map((e) => e.node) || []);
23      });
24    }
25    loadAccounts();
26  }, []);
27
28  async function handleCreateAccount(name: string) {
29    try {
30      // Perform the mutation
31      await createAccount({ name });
32
33      // Refresh the query to show the new record
34      if (accountsResult) {
35        await accountsResult.refresh();
36      }
37    } catch (error) {
38      console.error("Failed to create account:", error);
39    }
40  }
41
42  return (
43    <div>
44      <button onClick={() => handleCreateAccount("New Account")}>Create Account</button>
45      <ul>
46        {accounts.map((account) => (
47          <li key={account.Id}>{account.Name?.value}</li>
48        ))}
49      </ul>
50    </div>
51  );
52}

Pattern 2: Re-query After Mutation 

If you don’t have access to the QueryResult, re-execute the query.

1async function handleCreateAccount(name: string) {
2  try {
3    // Perform the mutation
4    await createAccount({ name });
5
6    // Re-query
7    await loadAccounts();
8  } catch (error) {
9    console.error("Failed to create account:", error);
10  }
11}

Pattern 3: Optimistic Updates 

You can choose to update the UI before the mutation completes.

1async function handleDeleteAccount(accountId: string) {
2  // Optimistically remove from UI
3  setAccounts((prev) => prev.filter((a) => a.Id !== accountId));
4
5  try {
6    await deleteAccount(accountId);
7    // Success - optimistic update was correct
8  } catch (error) {
9    console.error("Failed to delete account:", error);
10    // Revert optimistic update
11    if (accountsResult) {
12      await accountsResult.refresh();
13    }
14  }
15}

Mutation Best Practices 

Here are several best practices for working with GraphQL mutation.

Select Id in Return Fields 

Include Id in your mutation return fields.

1mutation CreateAccount($input: AccountCreateInput!) {
2  uiapi {
3    AccountCreate(input: $input) {
4      Record {
5        Id # Always include this
6        Name {
7          value
8        }
9      }
10    }
11  }
12}

Use Type-Safe Input Interfaces 

Define interfaces for mutation inputs for type safety.

1export interface CreateContactInput {
2  firstName: string;
3  lastName: string;
4  accountId?: string;
5  email?: string;
6  phone?: string;
7}
8
9export async function createContact(input: CreateContactInput) {
10  // Type-safe mutation implementation
11}

Handle Loading States 

Show loading states during mutations to notify users on what to expect.

1const [isCreating, setIsCreating] = useState(false);
2
3async function handleCreate() {
4  setIsCreating(true);
5  try {
6    await createAccount({ name: "New Account" });
7  } finally {
8    setIsCreating(false);
9  }
10}

Provide User Feedback 

Show success and error messages to communicate expectations with users.

1const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null);
2
3async function handleCreate() {
4  try {
5    await createAccount({ name: "New Account" });
6    setMessage({ type: "success", text: "Account created successfully!" });
7  } catch (error) {
8    setMessage({ type: "error", text: (error as Error).message });
9  }
10}

Clean Up Form State 

Reset form fields after successful mutations.

1async function handleSubmit(e: React.FormEvent) {
2  e.preventDefault();
3  try {
4    await createAccount({ name, industry });
5    // Clear form on success
6    setName("");
7    setIndustry("");
8  } catch (error) {
9    // Handle error
10  }
11}

Batch Related Mutations 

If you must perform multiple mutations, consider batching them.

1async function createAccountWithContacts(
2  accountData: CreateAccountInput,
3  contacts: CreateContactInput[],
4) {
5  // Create account first
6  const account = await createAccount(accountData);
7
8  if (!account?.Id) {
9    throw new Error("Failed to create account");
10  }
11
12  // Create contacts with the new account ID
13  const contactPromises = contacts.map((contact) =>
14    createContact({ ...contact, accountId: account.Id }),
15  );
16
17  await Promise.all(contactPromises);
18
19  return account;
20}

See Also