Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 44 additions & 17 deletions skills/using-webapp-salesforce-data/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Use this skill when the user wants to:

## Data SDK Requirement

> **All Salesforce data access MUST use the Data SDK** (`@salesforce/sdk-data`). The SDK handles authentication, CSRF, and base URL resolution. Never use `fetch()` or `axios` directly.
> **All Salesforce data access MUST use the Data SDK** (`@salesforce/sdk-data`). The SDK handles authentication, CSRF, and base URL resolution.

```typescript
import { createDataSDK, gql } from "@salesforce/sdk-data";
Expand Down Expand Up @@ -67,6 +67,24 @@ const res = await sdk.fetch?.("/services/apexrest/my-resource");

---

## GraphQL Non-Negotiable Rules

These rules exist because Salesforce GraphQL has platform-specific behaviors that differ from standard GraphQL. Violations cause silent runtime failures.

1. **Schema is the single source of truth** — Every entity name, field name, and type must be confirmed via the schema search script before use in a query. Never guess — Salesforce field names are case-sensitive, relationships may be polymorphic, and custom objects use suffixes (`__c`, `__e`). See [Schema Introspection](references/schema-introspection.md) for entity identification and iterative lookup procedures.

2. **`@optional` on all record fields** (read queries) — Salesforce field-level security (FLS) causes queries to fail entirely if the user lacks access to even one field. The `@optional` directive (v65+) tells the server to omit inaccessible fields instead of failing. Apply it to every scalar field, parent relationship, and child relationship. Consuming code must use optional chaining (`?.`) and nullish coalescing (`??`).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not related to your pr (let's add it to the back burner): i would love to tweak this, @optional on everything may be too extreme and masks real permission misconfigurations during development, fields critical to business logic (Id, fields used in filtering/routing) should fail loudly, not silently return undefined

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup, I replied to this in the webapps PR, and also agreed we should tweak it (but in a follow up PR)


3. **Correct mutation syntax** — Mutations wrap under `uiapi(input: { allOrNone: true/false })`, not bare `uiapi { ... }`. Always set `allOrNone` explicitly. Output fields cannot include child relationships or navigated reference fields. See [Mutation Query Generation](references/mutation-query-generation.md).

4. **Explicit pagination** — Always include `first:` in every query. If omitted, the server silently defaults to 10 records. Include `pageInfo { hasNextPage endCursor }` for any query that may need pagination.

5. **SOQL-derived execution limits** — Max 10 subqueries per request, max 5 levels of child-to-parent traversal, max 1 level of parent-to-child (no grandchildren), max 2,000 records per subquery. If a query would exceed these, split into multiple requests.

6. **HTTP 200 does not mean success** — Salesforce returns HTTP 200 even when operations fail. Always parse the `errors` array in the response body.

---

## GraphQL Workflow

### Step 1: Acquire Schema
Expand All @@ -75,18 +93,18 @@ The `schema.graphql` file (265K+ lines) is the source of truth. **Never open or

1. Check if `schema.graphql` exists at the SFDX project root
2. If missing, run from the **webapp dir**: `npm run graphql:schema`
3. Custom objects appear only after metadata is deployed
3. Custom objects appear only after metadata is deployed — invoke the `deploying-webapp-to-salesforce` skill if deployment is needed

### Step 2: Look Up Entity Schema

Map user intent to PascalCase names ("accounts" → `Account`), then **run the search script from the project root**:

```bash
# From project root — look up all relevant schema info for one or more entities
bash .a4drules/skills/using-salesforce-data/graphql-search.sh Account
# Look up all relevant schema info for one or more entities
bash scripts/graphql-search.sh Account

# Multiple entities at once
bash .a4drules/skills/using-salesforce-data/graphql-search.sh Account Contact Opportunity
bash scripts/graphql-search.sh Account Contact Opportunity
```

The script outputs five sections per entity:
Expand All @@ -96,11 +114,11 @@ The script outputs five sections per entity:
4. **Create input** — fields accepted by create mutations
5. **Update input** — fields accepted by update mutations

Use this output to determine exact field names before writing any query or mutation. **Maximum 2 script runs.** If the entity still can't be found, ask the user — the object may not be deployed.
Use this output to determine exact field names before writing any query or mutation. **Maximum 2 script runs.** If the entity still can't be found, ask the user — the object may not be deployed. For entity identification procedures (`_Record` suffix, `__c` conventions) and iterative introspection cycles, see [Schema Introspection](references/schema-introspection.md).

### Step 3: Generate Query

Use the templates below. Every field name **must** be verified from the script output in Step 2.
Use the templates below. Every field name **must** be verified from the script output in Step 2. For detailed generation rules, filtering, pagination, ordering, semi-joins, and field value wrappers, see [Read Query Generation](references/read-query-generation.md). For mutation chaining, input/output constraints, and transactional semantics, see [Mutation Query Generation](references/mutation-query-generation.md).

#### Read Query Template

Expand Down Expand Up @@ -138,7 +156,7 @@ const name = node.Name?.value ?? "";

```graphql
mutation CreateAccount($input: AccountCreateInput!) {
uiapi {
uiapi(input: { allOrNone: true }) {
AccountCreate(input: $input) {
Record { Id Name { value } }
}
Expand Down Expand Up @@ -222,15 +240,20 @@ const fields = response?.data?.uiapi?.objectInfos?.[0]?.fields ?? [];

```bash
# From project root — re-check the entity that caused the error
bash .a4drules/skills/using-salesforce-data/graphql-search.sh <EntityName>
bash scripts/graphql-search.sh <EntityName>
```

Then fix the query using the exact names from the script output.
Then fix the query using the exact names from the script output. For detailed error categories, status handling, and retry strategy, see [Query Testing](references/query-testing.md).

---

## Webapp Integration (React)

Two integration patterns are available:

- **Pattern 1 — External `.graphql` file** (recommended for complex queries): Create a `.graphql` file, run `npm run graphql:codegen`, import with `?raw` suffix
- **Pattern 2 — Inline `gql` tag** (for simple queries): Use the `gql` template tag from `@salesforce/sdk-data`. **Must use `gql`** — plain template strings bypass ESLint schema validation.

```typescript
import { createDataSDK, gql } from "@salesforce/sdk-data";

Expand All @@ -242,8 +265,9 @@ const GET_ACCOUNTS = gql`
edges {
node {
Id
Name @optional { value }
Industry @optional { value }
Name @optional {
value
}
}
}
}
Expand All @@ -254,14 +278,14 @@ const GET_ACCOUNTS = gql`

const sdk = await createDataSDK();
const response = await sdk.graphql?.(GET_ACCOUNTS);

if (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) ?? [];
```

For detailed patterns (external .graphql files, codegen, error handling strategies, quality checklists), see [Webapp Integration](references/webapp-integration.md).

---

## REST API Patterns
Expand Down Expand Up @@ -320,7 +344,7 @@ const response = await sdk.graphql?.(GET_CURRENT_USER);
|---------|----------|-----|
| `npm run graphql:schema` | webapp dir | Script in webapp's package.json |
| `npx eslint <file>` | webapp dir | Reads eslint.config.js |
| `bash .a4drules/skills/using-salesforce-data/graphql-search.sh <Entity>` | project root | Schema lookup |
| `bash scripts/graphql-search.sh <Entity>` | skill root | Schema lookup |
| `sf api request rest` | project root | Needs sfdx-project.json |

---
Expand All @@ -332,7 +356,7 @@ const response = await sdk.graphql?.(GET_CURRENT_USER);
Run the search script to get all relevant schema info in one step:

```bash
bash .a4drules/skills/using-salesforce-data/graphql-search.sh <EntityName>
bash scripts/graphql-search.sh <EntityName>
```

| Script Output Section | Used For |
Expand All @@ -358,6 +382,9 @@ bash .a4drules/skills/using-salesforce-data/graphql-search.sh <EntityName>
### Checklist

- [ ] All field names verified via search script (Step 2)
- [ ] `@optional` applied to record fields (reads)
- [ ] `@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 <file>`
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Mutation Query Generation
Copy link
Copy Markdown
Contributor

@jodarove jodarove Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should the folder name for these be "references" or maybe "assets" instead of "docs"? https://agentskills.io/specification#directory-structure

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch, will update to references (I see that in other skills as well)


## Mutation Types

The GraphQL engine supports three mutation operations:

- **Create** — Insert a new record
- **Update** — Modify an existing record (Id-based)
- **Delete** — Remove an existing record (Id-based)

Mutations are GA in API v66+. They live under `mutation { uiapi { ... } }` and only support UI API-available objects.

## Generation Rules

1. **Input fields validation** — Validate that input fields satisfy the constraints for the operation type
2. **Output fields validation** — Validate that output fields satisfy the constraints for the operation type
3. **Type consistency** — Variables used as query arguments and their related fields must share the same GraphQL type. Verify types via the schema search script — do NOT assume types
4. **Input arguments** — `input` is the default argument name unless otherwise specified
5. **Output field** — For `Create` and `Update`, the output field is always named `Record` (type: EntityName)
6. **Field name validation** — Every field name in the generated mutation **MUST** match a field confirmed via the schema search script. Do NOT guess or assume field names exist
7. **Raw input values** — Numeric values must be raw numbers without commas, currency symbols, or locale formatting (e.g., `80000` not `"80,000"` or `"$80,000"`). Compound fields (like addresses) require constituent fields (e.g., `BillingCity`, `BillingStreet`) — do not attempt to set the compound wrapper itself.

## Transactional Semantics: `allOrNone`

The `uiapi` mutation input accepts an `allOrNone` argument that controls rollback behavior:

- **`allOrNone: true` (default)** — If any operation fails, all operations in the request are rolled back. Use when operations must succeed or fail together.
- **`allOrNone: false`** — Independent operations can succeed individually. However, dependent operations (those using `@{alias}` references) still roll back together with their dependencies.

Always set `allOrNone` explicitly to make transactional intent clear.

## Mutation Schema Patterns

Replace `EntityName` with the actual entity name (e.g., Account, Case). `Delete` operations use generic `Record` types.

```graphql
input EntityNameCreateRepresentation {
# Subset of EntityName fields
}
input EntityNameCreateInput { EntityName: EntityNameCreateRepresentation! }
type EntityNameCreatePayload { Record: EntityName! }

input EntityNameUpdateRepresentation {
# Subset of EntityName fields
}
input EntityNameUpdateInput { Id: IdOrRef! EntityName: EntityNameUpdateRepresentation! }
type EntityNameUpdatePayload { Record: EntityName! }

input RecordDeleteInput { Id: IdOrRef! }
type RecordDeletePayload { Id: ID }

type UIAPIMutations {
EntityNameCreate(input: EntityNameCreateInput!): EntityNameCreatePayload
EntityNameDelete(input: RecordDeleteInput!): RecordDeletePayload
EntityNameUpdate(input: EntityNameUpdateInput!): EntityNameUpdatePayload
}
```

## Input Field Constraints

### Create

- **Must** include all required fields (unless `defaultedOnCreate` is `true` and not explicitly requested)
- **Must** only include `createable` fields
- Child relationships cannot be set — exclude them
- Reference fields (`REFERENCE` type) can only be assigned IDs through their `ApiName` name
- **No nested child creates** — Creating a record with child relationships in a single create operation is not supported. To create a parent and child together, use separate operations with `IdOrRef` chaining (see [Mutation Chaining](#mutation-chaining)).

### Update

- **Must** include the `Id` of the entity to update
- **Must** only include `updateable` fields
- Child relationships cannot be set — exclude them
- Reference fields (`REFERENCE` type) can only be assigned IDs through their `ApiName` name

### Delete

- **Must** include the `Id` of the entity to delete

## Output Field Constraints

### Create and Update

- **Must** exclude all child relationships (child relationships cannot be queried in mutations)
- **Must** exclude all `REFERENCE` fields unless accessed through their `ApiName` member (no navigation to referenced entity, no sub fields)
- Inaccessible fields are reported in the `errors` attribute of the returned payload

### Delete

- **Must** only include the `Id` field

## Mutation Chaining

Chain related mutations in a single request using references to `Id` values from previous mutations. This is the required approach for creating parent-child records together, since nested child creates are not supported.

1. **Ordering** — Mutation `B` can reference mutation `A` only if `A` comes first in the query
2. **Notation** — Use `SomeId: "@{A}"` in mutation `B` to set a field to the `Id` produced by mutation `A`
3. **IDs only** — `@{A}` is always interpreted as the `Id` from mutation `A`
4. **Restrictions** — `A` must be a `Create` or `Delete` mutation (chaining from `Update` will fail)

### Chaining Example

```graphql
mutation CreateAccountAndContact {
uiapi(input: { allOrNone: true }) {
AccountCreate(input: { Account: { Name: "Acme" } }) {
Record { Id }
}
ContactCreate(input: { Contact: { LastName: "Smith", AccountId: "@{AccountCreate}" } }) {
Record { Id }
}
}
}
```

## Mutation Query Template

```graphql
mutation mutateEntityName(
# arguments
) {
uiapi(input: { allOrNone: true }) {
EntityNameOperation(input: {
# For Create and Update only:
EntityName: {
# Input fields — use raw values, no formatting
}
# For Update and Delete only:
Id: ... # id here
}) {
# For Create and Update only:
Record {
# Output fields
}
# For Delete only:
Id
}
}
}
```
78 changes: 78 additions & 0 deletions skills/using-webapp-salesforce-data/references/query-testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Query Testing

## Testing Method

Use `sf api request rest` to POST the query to the GraphQL endpoint. Run from the **SFDX project root** (where `sfdx-project.json` lives).

```bash
sf api request rest /services/data/v66.0/graphql \
--method POST \
--body '{"query":"query GetData { uiapi { query { EntityName { edges { node { Id } } } } } }"}'
```

- Use the API version of the target org (v66.0+ for mutation support, v65.0+ for `@optional`)
- Replace the `query` value with the generated query string
- If the query uses variables, include them in the JSON body as a `variables` key

## Critical: HTTP 200 Does Not Mean Success

Salesforce returns HTTP 200 even when the GraphQL operation has errors (e.g., invalid fields, permission failures, invalid IDs). **Always parse the `errors` array in the response body regardless of HTTP status code.** Do not treat HTTP 200 as confirmation that the query succeeded.

## Testing Workflow

This workflow applies to both read and mutation queries:

1. **Report method** — State the exact method: `sf api request rest` POST to `/services/data/vXX.0/graphql` from the project root
2. **Ask user** — Ask the user whether they want to test the query. For mutations, also ask for input argument values — mutations modify real data, so explicit consent is essential. Wait for the user's answer before proceeding. Do not fabricate test data.
3. **Execute test** — Only if the user explicitly agrees. Run `sf api request rest` with the query, variables, and correct API version
4. **Report result** — Classify the result using the status definitions below. Always check the `errors` array in the response, even on HTTP 200.

## Result Status Definitions

| Status | Condition | Meaning |
| --------- | ----------------------------------------------- | --------------------------------------------- |
| `SUCCESS` | `errors` is absent or empty | Query is valid (even if no data is returned) |
| `FAILED` | `data` is empty or null | Query is invalid |
| `PARTIAL` | `data` is present **and** `errors` is not empty | Some fields are inaccessible (mutations only) |

## FAILED Status Handling

The query is invalid. Follow this sequence:

### 1. Error Analysis

Parse the `errors` array and check `errors[].extensions.ErrorType` for Salesforce-specific error classification. Categorize into:

| Category | ErrorType / Message Contains | Resolution |
| --------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| **Syntax** | `InvalidSyntax` | Fix syntax errors using the error message details |
| **Validation** | `ValidationError` | Field name is likely invalid — re-run the schema search script, ask user if still unclear |
| **Type** | `VariableTypeMismatch` or `UnknownType` | Use error details and schema to correct the argument type; adjust variables |
| **Execution** | `DataFetchingException`, `invalid cross reference id` | Entity is unknown/deleted — create entity first if possible, or ask for a valid Id |
| **Navigation** | `is not currently available in mutation results` | Field cannot be in mutation output — apply PARTIAL status handling |
| **Unsupported** | `OperationNotSupported` | The operation is not supported — check object availability and API version |
| **API Version** | `Cannot invoke JsonElement.isJsonObject()` (on update mutations) | `Record` selection requires API version 64+ — report and retry with version 64 |

### 2. Targeted Resolution

Apply the resolution from the table above based on the error category. Update the query accordingly.

### 3. Test Again

Re-run the testing workflow with the updated query. Increment and track the attempt counter.

## PARTIAL Status Handling

The query executed but some fields are inaccessible (mutations only):

1. Report the fields listed in the `errors` attribute
2. Explain that these fields cannot be queried as part of a mutation
3. Explain that the query will report errors if these fields remain
4. Offer to remove the offending fields
5. **STOP and WAIT** for the user's answer. Do NOT remove fields without explicit consent.
6. If the user agrees, restart the mutation generation workflow with the updated field list

## Retry and Escalation

- **Maximum 2 test attempts** per generated query
- If targeted resolution fails after 2 attempts, ask the user for additional details and **restart the entire workflow from Step 1 (Acquire Schema)** to re-validate entity and field information
Loading
Loading