GraphQL Mutations in Data SDK (Beta)

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
const result = await dataSdk.graphql?.mutate<MutationResponse, MutationVariables>({
  mutation: MUTATION_STRING,
  variables: { ... },
  operationName?: "..."
});

// Check for errors
if (result?.errors?.length) {
  throw new Error(result.errors.map((e) => e.message).join("; "));
}

// Use the returned data
const 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.

# uiBundles/yourAppName/src/api/account/query/createAccount.graphql
mutation CreateAccount($input: AccountCreateInput!) {
  uiapi {
    AccountCreate(input: $input) {
      Record {
        Id
        Name {
          value
        }
        Industry {
          value
        }
        Phone {
          value
        }
        Website {
          value
        }
      }
    }
  }
}

Step 2: Generate Types

Run npm run graphql:codegen to generate TypeScript types.

// Generated in src/api/graphql-operations-types.ts
export type CreateAccountMutation = {
  uiapi: {
    AccountCreate: {
      Record: {
        Id: string;
        Name: { value: string } | null;
        Industry: { value: string } | null;
        Phone: { value: string } | null;
        Website: { value: string } | null;
      };
    };
  };
};

export type CreateAccountMutationVariables = {
  input: AccountCreateInput;
};

export type AccountCreateInput = {
  Account: {
    Name?: string;
    Industry?: string;
    Phone?: string;
    Website?: string;
    // ... other fields
  };
};

Step 3: Implement the Mutation Function

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

// uiBundles/yourAppName/src/api/account/accountCreate.ts
import { createDataSDK } from "@salesforce/platform-sdk/data";
import CREATE_ACCOUNT from "./mutation/createAccount.graphql?raw";
import type {
  CreateAccountMutation,
  CreateAccountMutationVariables,
} from "../graphql-operations-types";

export interface CreateAccountInput {
  name: string;
  industry?: string;
  phone?: string;
  website?: string;
}

export async function createAccount(input: CreateAccountInput) {
  const dataSdk = await createDataSDK();

  const result = await dataSdk.graphql?.mutate<
    CreateAccountMutation,
    CreateAccountMutationVariables
  >({
    mutation: CREATE_ACCOUNT,
    variables: {
      input: {
        Account: {
          Name: input.name,
          Industry: input.industry,
          Phone: input.phone,
          Website: input.website,
        },
      },
    },
  });

  if (result?.errors?.length) {
    const errorMessages = result.errors.map((e) => e.message).join("; ");
    throw new Error(`Failed to create account: ${errorMessages}`);
  }

  return result?.data?.uiapi?.AccountCreate?.Record;
}

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.

import { useState } from "react";
import { createAccount } from "./api/accounts";

function CreateAccountForm() {
  const [name, setName] = useState("");
  const [industry, setIndustry] = useState("");
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setLoading(true);
    setError(null);

    try {
      const newAccount = await createAccount({ name, industry });
      console.log("Created account:", newAccount);
      // Clear form
      setName("");
      setIndustry("");
      // Optionally navigate or refresh data
    } catch (err) {
      setError((err as Error).message);
    } finally {
      setLoading(false);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Account Name"
        required
      />
      <input
        value={industry}
        onChange={(e) => setIndustry(e.target.value)}
        placeholder="Industry"
      />
      <button type="submit" disabled={loading}>
        {loading ? "Creating..." : "Create Account"}
      </button>
      {error && <div className="error">{error}</div>}
    </form>
  );
}

Update Record

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

Step 1: Define the Mutation

# uiBundles/yourAppName/src/api/account/query/updateAccount.graphql
mutation UpdateAccount($input: AccountUpdateInput!) {
  uiapi {
    AccountUpdate(input: $input) {
      Record {
        Id
        Name {
          value
        }
        Industry {
          value
        }
        AnnualRevenue {
          value
        }
        Phone {
          value
        }
      }
    }
  }
}

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.

import { createDataSDK } from "@salesforce/platform-sdk/data";
import UPDATE_ACCOUNT from "./mutation/updateAccount.graphql?raw";
import type {
  UpdateAccountMutation,
  UpdateAccountMutationVariables,
} from "../graphql-operations-types";

export interface UpdateAccountInput {
  id: string;
  name?: string;
  industry?: string;
  annualRevenue?: number;
  phone?: string;
}

export async function updateAccount(input: UpdateAccountInput) {
  const dataSdk = await createDataSDK();

  const result = await dataSdk.graphql?.mutate<
    UpdateAccountMutation,
    UpdateAccountMutationVariables
  >({
    mutation: UPDATE_ACCOUNT,
    variables: {
      input: {
        Id: input.id,
        Account: {
          Name: input.name,
          Industry: input.industry,
          AnnualRevenue: input.annualRevenue,
          Phone: input.phone,
        },
      },
    },
  });

  if (result?.errors?.length) {
    const errorMessages = result.errors.map((e) => e.message).join("; ");
    throw new Error(`Failed to update account: ${errorMessages}`);
  }

  return result?.data?.uiapi?.AccountUpdate?.Record;
}

Step 3: Use in Your Component

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

// uiBundles/yourAppName/src/api/account/accountUpdate.ts
import { useState, useEffect } from "react";
import { updateAccount } from "./api/accounts";

interface EditAccountFormProps {
  accountId: string;
  initialName: string;
  initialIndustry?: string;
  onSuccess?: () => void;
}

function EditAccountForm({
  accountId,
  initialName,
  initialIndustry,
  onSuccess,
}: EditAccountFormProps) {
  const [name, setName] = useState(initialName);
  const [industry, setIndustry] = useState(initialIndustry || "");
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setLoading(true);
    setError(null);

    try {
      await updateAccount({
        id: accountId,
        name,
        industry,
      });
      onSuccess?.();
    } catch (err) {
      setError((err as Error).message);
    } finally {
      setLoading(false);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Account Name"
        required
      />
      <input
        value={industry}
        onChange={(e) => setIndustry(e.target.value)}
        placeholder="Industry"
      />
      <button type="submit" disabled={loading}>
        {loading ? "Updating..." : "Update Account"}
      </button>
      {error && <div className="error">{error}</div>}
    </form>
  );
}

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.

# uiBundles/yourAppName/src/api/account/query/deleteAccount.graphql
mutation DeleteAccount($input: AccountDeleteInput!) {
  uiapi {
    AccountDelete(input: $input) {
      Id
    }
  }
}

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

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.

import { createDataSDK } from "@salesforce/platform-sdk/data";
import DELETE_ACCOUNT from "./mutation/deleteAccount.graphql?raw";
import type {
  DeleteAccountMutation,
  DeleteAccountMutationVariables,
} from "../graphql-operations-types";

export async function deleteAccount(id: string): Promise<string> {
  const dataSdk = await createDataSDK();

  const result = await dataSdk.graphql?.mutate<
    DeleteAccountMutation,
    DeleteAccountMutationVariables
  >({
    mutation: DELETE_ACCOUNT,
    variables: {
      input: {
        Id: id,
      },
    },
  });

  if (result?.errors?.length) {
    const errorMessages = result.errors.map((e) => e.message).join("; ");
    throw new Error(`Failed to delete account: ${errorMessages}`);
  }

  return result?.data?.uiapi?.AccountDelete?.Id || "";
}

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.

// uiBundles/yourAppName/src/api/account/accountDelete.ts
import { useState } from "react";
import { deleteAccount } from "./api/accounts";

interface DeleteAccountButtonProps {
  accountId: string;
  accountName: string;
  onSuccess?: () => void;
}

function DeleteAccountButton({ accountId, accountName, onSuccess }: DeleteAccountButtonProps) {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  async function handleDelete() {
    const confirmed = window.confirm(`Are you sure you want to delete "${accountName}"?`);

    if (!confirmed) return;

    setLoading(true);
    setError(null);

    try {
      await deleteAccount(accountId);
      onSuccess?.();
    } catch (err) {
      setError((err as Error).message);
    } finally {
      setLoading(false);
    }
  }

  return (
    <div>
      <button onClick={handleDelete} disabled={loading} className="danger-button">
        {loading ? "Deleting..." : "Delete"}
      </button>
      {error && <div className="error">{error}</div>}
    </div>
  );
}

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():

import { useEffect, useState } from "react";
import { createDataSDK } from "@salesforce/platform-sdk/data";
import { createAccount } from "./api/accounts";
import type { QueryResult } from "@salesforce/platform-sdk/data";

function AccountManager() {
  const [accountsResult, setAccountsResult] = useState<QueryResult<AccountsData>>();
  const [accounts, setAccounts] = useState<Account[]>([]);

  useEffect(() => {
    async function loadAccounts() {
      const dataSdk = await createDataSDK();
      const result = await dataSdk.graphql?.query<AccountsData>({
        query: GET_ACCOUNTS,
      });

      setAccountsResult(result);
      setAccounts(result?.data?.uiapi?.query?.Account?.edges?.map((e) => e.node) || []);

      // Subscribe to updates
      result?.subscribe((snapshot) => {
        setAccounts(snapshot.data?.uiapi?.query?.Account?.edges?.map((e) => e.node) || []);
      });
    }
    loadAccounts();
  }, []);

  async function handleCreateAccount(name: string) {
    try {
      // Perform the mutation
      await createAccount({ name });

      // Refresh the query to show the new record
      if (accountsResult) {
        await accountsResult.refresh();
      }
    } catch (error) {
      console.error("Failed to create account:", error);
    }
  }

  return (
    <div>
      <button onClick={() => handleCreateAccount("New Account")}>Create Account</button>
      <ul>
        {accounts.map((account) => (
          <li key={account.Id}>{account.Name?.value}</li>
        ))}
      </ul>
    </div>
  );
}

Pattern 2: Re-query After Mutation

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

async function handleCreateAccount(name: string) {
  try {
    // Perform the mutation
    await createAccount({ name });

    // Re-query
    await loadAccounts();
  } catch (error) {
    console.error("Failed to create account:", error);
  }
}

Pattern 3: Optimistic Updates

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

async function handleDeleteAccount(accountId: string) {
  // Optimistically remove from UI
  setAccounts((prev) => prev.filter((a) => a.Id !== accountId));

  try {
    await deleteAccount(accountId);
    // Success - optimistic update was correct
  } catch (error) {
    console.error("Failed to delete account:", error);
    // Revert optimistic update
    if (accountsResult) {
      await accountsResult.refresh();
    }
  }
}

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.

mutation CreateAccount($input: AccountCreateInput!) {
  uiapi {
    AccountCreate(input: $input) {
      Record {
        Id # Always include this
        Name {
          value
        }
      }
    }
  }
}

Use Type-Safe Input Interfaces

Define interfaces for mutation inputs for type safety.

export interface CreateContactInput {
  firstName: string;
  lastName: string;
  accountId?: string;
  email?: string;
  phone?: string;
}

export async function createContact(input: CreateContactInput) {
  // Type-safe mutation implementation
}

Handle Loading States

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

const [isCreating, setIsCreating] = useState(false);

async function handleCreate() {
  setIsCreating(true);
  try {
    await createAccount({ name: "New Account" });
  } finally {
    setIsCreating(false);
  }
}

Provide User Feedback

Show success and error messages to communicate expectations with users.

const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null);

async function handleCreate() {
  try {
    await createAccount({ name: "New Account" });
    setMessage({ type: "success", text: "Account created successfully!" });
  } catch (error) {
    setMessage({ type: "error", text: (error as Error).message });
  }
}

Clean Up Form State

Reset form fields after successful mutations.

async function handleSubmit(e: React.FormEvent) {
  e.preventDefault();
  try {
    await createAccount({ name, industry });
    // Clear form on success
    setName("");
    setIndustry("");
  } catch (error) {
    // Handle error
  }
}

Batch Related Mutations

If you must perform multiple mutations, consider batching them.

async function createAccountWithContacts(
  accountData: CreateAccountInput,
  contacts: CreateContactInput[],
) {
  // Create account first
  const account = await createAccount(accountData);

  if (!account?.Id) {
    throw new Error("Failed to create account");
  }

  // Create contacts with the new account ID
  const contactPromises = contacts.map((contact) =>
    createContact({ ...contact, accountId: account.Id }),
  );

  await Promise.all(contactPromises);

  return account;
}

See Also