@salesforce/afv-skills 1.23.0 → 1.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/package.json +1 -1
  2. package/skills/commerce-b2b-open-code-components-replace/SKILL.md +244 -0
  3. package/skills/commerce-b2b-open-code-components-replace/assets/ootb-to-open-code-mapping.json +66 -0
  4. package/skills/{developing-datacloud-code-extension → data360-code-extension-generate}/SKILL.md +7 -7
  5. package/skills/{developing-datacloud-code-extension → data360-code-extension-generate}/references/README.md +7 -7
  6. package/skills/{developing-datacloud-code-extension → data360-code-extension-generate}/references/quick-reference.md +2 -2
  7. package/skills/{getting-datacloud-schema → data360-schema-get}/SKILL.md +26 -26
  8. package/skills/{getting-datacloud-schema → data360-schema-get}/references/README.md +9 -9
  9. package/skills/dx-org-manage/SKILL.md +192 -0
  10. package/skills/dx-org-manage/examples/README.md +45 -0
  11. package/skills/dx-org-manage/examples/scratch-orgs/error_no_devhub.json +9 -0
  12. package/skills/dx-org-manage/examples/scratch-orgs/error_timeout.json +13 -0
  13. package/skills/dx-org-manage/examples/scratch-orgs/success_definition_file.json +28 -0
  14. package/skills/dx-org-manage/examples/scratch-orgs/success_edition.json +26 -0
  15. package/skills/dx-org-manage/examples/scratch-orgs/success_snapshot.json +27 -0
  16. package/skills/dx-org-manage/examples/snapshots/error_output.json +9 -0
  17. package/skills/dx-org-manage/examples/snapshots/success_output.json +15 -0
  18. package/skills/dx-org-manage/references/cli_flags.md +67 -0
  19. package/skills/dx-org-manage/references/creating-scratch-org.md +164 -0
  20. package/skills/dx-org-manage/references/creating-snapshot.md +103 -0
  21. package/skills/dx-org-manage/references/definition_file_options.md +224 -0
  22. package/skills/dx-org-manage/references/edition_types.md +78 -0
  23. package/skills/dx-org-manage/references/opening-org.md +160 -0
  24. package/skills/dx-org-manage/references/snapshot_usage.md +74 -0
  25. package/skills/dx-org-permission-set-assign/SKILL.md +98 -0
  26. package/skills/dx-org-permission-set-assign/examples/error_output.json +19 -0
  27. package/skills/dx-org-permission-set-assign/examples/success_output.json +16 -0
  28. package/skills/dx-org-permission-set-assign/references/cli_flags.md +68 -0
  29. package/skills/experience-cms-brand-apply/SKILL.md +1 -1
  30. package/skills/experience-ui-bundle-app-coordinate/SKILL.md +31 -19
  31. package/skills/experience-ui-bundle-file-upload-generate/SKILL.md +1 -1
  32. package/skills/experience-ui-bundle-frontend-generate/implementation/header-footer.md +1 -1
  33. package/skills/experience-ui-bundle-salesforce-data-access/SKILL.md +336 -581
  34. package/skills/experience-ui-bundle-salesforce-data-access/references/caching.md +172 -0
  35. package/skills/experience-ui-bundle-salesforce-data-access/references/graphiti-cli.md +373 -0
  36. package/skills/experience-ui-bundle-salesforce-data-access/references/graphql-hand-authoring.md +376 -0
  37. package/skills/experience-ui-bundle-salesforce-data-access/references/migration.md +119 -0
  38. package/skills/experience-ui-bundle-salesforce-data-access/references/rest-and-integration.md +152 -0
  39. package/skills/experience-ui-bundle-salesforce-data-access/references/sdk-api.md +217 -0
  40. package/skills/experience-ui-bundle-salesforce-data-access/scripts/graphql-search.sh +36 -9
  41. package/skills/platform-agentsetup-categories-fetch/SKILL.md +109 -0
  42. package/skills/platform-agentsetup-categories-fetch/references/api-response-schema.md +121 -0
  43. package/skills/platform-custom-object-generate/SKILL.md +62 -7
  44. package/skills/platform-custom-object-generate/references/description-enrichment.md +125 -0
  45. package/skills/platform-metadata-retrieve/SKILL.md +121 -0
  46. package/skills/platform-metadata-retrieve/examples/error_output.json +10 -0
  47. package/skills/platform-metadata-retrieve/examples/success_output.json +27 -0
  48. package/skills/platform-metadata-retrieve/references/cli_flags.md +138 -0
  49. package/skills/platform-metadata-retrieve/references/retrieval_modes.md +181 -0
  50. package/skills/platform-sharing-rules-generate/SKILL.md +165 -0
  51. package/skills/platform-sharing-rules-generate/references/rule-types.md +199 -0
  52. package/skills/platform-tracing-agentforce-configure/SKILL.md +118 -0
  53. package/skills/platform-tracing-agentforce-configure/assets/AgentforcePlatformTracing-template.xml +4 -0
  54. package/skills/platform-tracing-configure/SKILL.md +118 -0
  55. package/skills/platform-tracing-configure/assets/EventSettings-template.xml +4 -0
  56. package/skills/platform-trust-archive-manage/SKILL.md +25 -11
  57. package/skills/platform-trust-archive-manage/examples/monitor-failed-jobs.md +2 -2
  58. package/skills/platform-trust-archive-manage/references/archive-activity-entity.md +1 -1
  59. package/skills/platform-trust-archive-manage/references/connect-api-operations.md +51 -12
  60. /package/skills/{getting-datacloud-schema → data360-schema-get}/scripts/get_dlo_schema.py +0 -0
  61. /package/skills/{getting-datacloud-schema → data360-schema-get}/scripts/get_dmo_schema.py +0 -0
@@ -1,638 +1,393 @@
1
1
  ---
2
2
  name: experience-ui-bundle-salesforce-data-access
3
- description: "MUST activate when the project contains a uiBundles/*/src/ directory and the task involves ANY Salesforce record operation — reading, creating, updating, or deleting. Use this skill when building forms that submit to Salesforce, pages that display Salesforce records, or any code that touches Salesforce objects or custom objects. Activate when files under uiBundles/*/src/ import from @salesforce/sdk-data, or when *.graphql files or codegen.yml exist. This skill owns all Salesforce data access patterns in UI bundles. Does not apply to authentication/OAuth setup, schema changes, Bulk/Tooling/Metadata API, or declarative automation."
3
+ description: "MUST activate when a uiBundles/*/src/ project does ANY Salesforce record operation — reading, creating, updating, deleting, or caching/refreshing query results. Triggers: code importing @salesforce/platform-sdk, calls to sdk.graphql.query / sdk.graphql.mutate / sdk.fetch, *.graphql files, stale data needing a force-refresh, or wiring up a UI bundle's data layer to read, write, or refresh Salesforce records. The default for new read/write work is the Read/Write workflow with the current @salesforce/platform-sdk API; only follow the migration path when EXISTING code already uses the old @salesforce/sdk-data callable form. Not for building app shell/UI, styling, file upload, or auth/search scaffolding — use the other ui-bundle-* skills. DO NOT TRIGGER when: OAuth setup, schema changes, Bulk/Tooling/Metadata API, or declarative automation."
4
4
  metadata:
5
- version: "1.0"
5
+ version: "2.1"
6
6
  ---
7
7
 
8
- # Salesforce Data Access
9
-
10
- ## Data SDK Requirement
11
-
12
- > **All Salesforce data access MUST use the Data SDK** (`@salesforce/sdk-data`). The SDK handles authentication, CSRF, and base URL resolution.
8
+ # Salesforce Data Access (UI bundles)
9
+
10
+ All Salesforce data access in a UI bundle goes through the **`@salesforce/platform-sdk`**
11
+ data SDK. The SDK handles auth, CSRF, and base-URL resolution, and — on the WebApp
12
+ surface caches every GraphQL query by default.
13
+
14
+ This file is the **workflow + guardrail spine**. Depth lives in linked docs:
15
+
16
+ - **[references/graphiti-cli.md](references/graphiti-cli.md)** — the **`graphiti` CLI** (`sf-gql-*`
17
+ commands) that compiles a small JSON spec into a schema-correct, guardrail-applied query +
18
+ variables + types. The preferred way to author the GraphQL in steps below; falls back to the
19
+ schema-grep script when unavailable.
20
+ - **[references/sdk-api.md](references/sdk-api.md)** — the new call API: `query`/`mutate`,
21
+ `QueryResult`, typing, error-handling stances.
22
+ - **[references/caching.md](references/caching.md)** — on-by-default cache + the **two refresh
23
+ modes** (`result.refresh`/`subscribe` vs per-call `cacheControl`).
24
+ - **[references/graphql-hand-authoring.md](references/graphql-hand-authoring.md)** — schema lookup, read /
25
+ mutation templates, every platform guardrail (`@optional`, pagination, limits,
26
+ semi-join, wrappers, error table…).
27
+ - **[references/rest-and-integration.md](references/rest-and-integration.md)** — `sdk.fetch`,
28
+ the supported-API allowlist, and the reactive/lifecycle integration patterns.
29
+ - **[references/migration.md](references/migration.md)** — old `@salesforce/sdk-data` callable code
30
+ → new namespace. The **only** place the dead API appears as usable code.
31
+
32
+ ## The one-paragraph mental model
33
+
34
+ `const sdk = await createDataSDK()`. Then `sdk.graphql` is a **namespace**, not a
35
+ function: **`sdk.graphql!.query({...})`** for reads, **`sdk.graphql!.mutate({...})`**
36
+ for writes. On WebApp, **every `query()` is cached by default** (300s). HTTP 200 never
37
+ means success — always check `result.errors`. Verify every entity and field against the
38
+ schema before you query it: one unverified field fails the *whole* query at runtime, and
39
+ `schema.graphql` is too large to eyeball — look it up.
13
40
 
14
41
  ```typescript
15
- import { createDataSDK, gql } from "@salesforce/sdk-data";
16
- import type { ResponseTypeQuery } from "../graphql-operations-types";
42
+ import { createDataSDK, gql } from "@salesforce/platform-sdk"; // gql tags the query string so codegen + eslint validate it
17
43
 
18
44
  const sdk = await createDataSDK();
19
-
20
- // GraphQL for record queries/mutations (PREFERRED)
21
- const response = await sdk.graphql?.<ResponseTypeQuery>(query, variables);
22
-
23
- // REST for Connect REST, Apex REST, UI API (when GraphQL insufficient)
24
- const res = await sdk.fetch?.("/services/apexrest/my-resource");
45
+ const result = await sdk.graphql!.query({ query: GET_ACCOUNTS, variables });
46
+ if (result.errors?.length) throw new Error(result.errors.map((e) => e.message).join("; "));
47
+ const rows = result.data?.uiapi?.query?.Account?.edges?.map((e) => e.node) ?? []; // unwrap edges/node; read field values via .value
25
48
  ```
26
49
 
27
- **Always use optional chaining** (`sdk.graphql?.()`, `sdk.fetch?.()`) — these methods may be undefined in some surfaces.
28
-
29
- ## Preconditions — verify before starting
30
-
31
- | # | Requirement | How to verify | If missing |
32
- |---|-------------|---------------|------------|
33
- | 1 | `@salesforce/sdk-data` installed | Check `package.json` in the UI bundle dir | Cannot proceed — tell user to install it |
34
- | 2 | `schema.graphql` at project root | Check if file exists | Run `npm run graphql:schema` from UI bundle dir |
35
- | 3 | Custom objects/fields deployed | Run `graphql-search.sh <Entity>` — no output means not deployed | Ask user to deploy metadata and assign permission sets |
36
-
37
- **If preconditions are not met**, you may scaffold components, routes, layout, and UI logic, but use empty arrays / `null` for data and mark query locations with `// TODO: add query after schema verification` and include in the plan to go back, resolve requirements and write the GraphQL. Do not write GraphQL query strings until the schema workflow is complete.
38
-
39
- ## Supported APIs
50
+ Typed call params (`query<GetAccountsQuery, GetAccountsQueryVariables>`), the `CacheControl`
51
+ type, and `NodeOfConnection<T>` (extracts a node type from a Connection for clean typing) all
52
+ live in [references/sdk-api.md](references/sdk-api.md).
40
53
 
41
- **Only the following APIs are permitted.** Any endpoint not listed here must not be used.
42
-
43
- | API | Method | Endpoints / Use Case |
44
- |-----|--------|----------------------|
45
- | GraphQL | `sdk.graphql` | All record queries and mutations via `uiapi { }` namespace |
46
- | UI API REST | `sdk.fetch` | `/services/data/v{ver}/ui-api/records/{id}` record metadata when GraphQL is insufficient |
47
- | Apex REST | `sdk.fetch` | `/services/apexrest/{resource}` custom server-side logic, aggregates, multi-step transactions |
48
- | Connect REST | `sdk.fetch` | `/services/data/v{ver}/connect/file/upload/config` file upload config |
49
- | Einstein LLM | `sdk.fetch` | `/services/data/v{ver}/einstein/llm/prompt/generations` — AI text generation |
50
-
51
- **Not supported:**
52
-
53
- - **Enterprise REST query endpoint** (`/services/data/v*/query` with SOQL) — blocked at the proxy level. Use GraphQL for record reads; use Apex REST if server-side SOQL aggregates are required.
54
- - **Aura-enabled Apex** (`@AuraEnabled`) — an LWC/Aura pattern with no invocation path from React UI bundles.
55
- - **Chatter API** (`/chatter/users/me`) — use `uiapi { currentUser { ... } }` in a GraphQL query instead.
56
- - **Any other Salesforce REST endpoint** not listed in the supported table above.
57
-
58
- ## Decision: GraphQL vs REST
59
-
60
- | Need | Method | Example |
61
- |------|--------|---------|
62
- | Query/mutate records | `sdk.graphql` | Account, Contact, custom objects |
63
- | Current user info | `sdk.graphql` | `uiapi { currentUser { Id Name { value } } }` |
64
- | UI API record metadata | `sdk.fetch` | `/ui-api/records/{id}` |
65
- | Connect REST | `sdk.fetch` | `/connect/file/upload/config` |
66
- | Apex REST | `sdk.fetch` | `/services/apexrest/auth/login` |
67
- | Einstein LLM | `sdk.fetch` | `/einstein/llm/prompt/generations` |
68
-
69
- **GraphQL is preferred** for record operations. Use REST only when GraphQL doesn't cover the use case.
54
+ > **This changed (breaking PR #502).** The previous callable `sdk.graphql(...)` form and the
55
+ > previous package name are **dead** — the code above is the only correct form. If you encounter
56
+ > the old API in existing code (or a stale `dist/` artifact), don't copy it; convert it per
57
+ > [Working on existing code](#working-on-existing-code-migration).
58
+ >
59
+ > **`sdk.graphql!` is WebApp-only.** The non-null assertion above is correct *only* if the
60
+ > bundle runs solely on WebApp. On other surfaces it can crash decide before you write it.
61
+ > See **[Surfaces`!` vs guard](#surfaces--sdkgraphql-vs-guard)** below.
70
62
 
71
63
  ---
72
64
 
73
- ## GraphQL Non-Negotiable Rules
74
-
75
- These rules exist because Salesforce GraphQL has platform-specific behaviors that differ from standard GraphQL. Violations cause silent runtime failures.
65
+ ## Surfaces `sdk.graphql!` vs guard
76
66
 
77
- 1. **HTTP 200 does not mean success** — Salesforce returns HTTP 200 even when operations fail. **Always parse the `errors` array in the response body.**
67
+ `createDataSDK()` runs on multiple surfaces, and **`sdk.graphql` / `sdk.fetch` are genuinely
68
+ optional** (typed `graphql?: …`). Whether you may assert them with `!` depends entirely on
69
+ where the bundle runs — this is the one surface decision that turns into a *runtime crash* if
70
+ you get it wrong, so make it explicitly before writing any `query`/`mutate` call:
78
71
 
79
- 2. **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`). Objects added to UI API in v60+ may use a `_Record` suffix (e.g., `FeedItem_Record` instead of `FeedItem`).
72
+ | Surface(s) | `sdk.graphql` | Write |
73
+ |---|---|---|
74
+ | **WebApp only** | always present | `sdk.graphql!.query({...})` — `!` is safe; every shipped WebApp consumer uses it |
75
+ | **Mosaic / OpenAI / MCPApps** (or any bundle that *might* run off-WebApp) | can be `undefined` | **guard first** (`if (!sdk.graphql) return …`), then call |
80
76
 
81
- 3. **`@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 (`??`).
82
-
83
- 4. **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.
84
-
85
- 5. **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. Forward-only (`first`/`after`) — `last`/`before` are unsupported.
86
-
87
- 6. **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.
88
-
89
- 7. **Only requested fields** — Only generate fields the user explicitly asked for. Do NOT add extra fields.
90
-
91
- 8. **Compound fields** — When filtering or ordering, use constituent fields (e.g., `BillingCity`, `BillingCountry`), not the compound wrapper (`BillingAddress`). The compound wrapper is only for selection.
77
+ Rule of thumb: **if you cannot prove the bundle is WebApp-only, guard.** A bare `sdk.graphql!`
78
+ that later ships to another surface throws `Cannot read properties of undefined` at runtime —
79
+ TypeScript won't catch it because `!` silences exactly that check (same applies to `sdk.fetch!`).
80
+ The portable guard snippet lives in [references/sdk-api.md](references/sdk-api.md#sdkgraphql-vs-guard).
92
81
 
93
82
  ---
94
83
 
95
- ## GraphQL Workflow
96
-
97
- | Step | Action | Key output |
98
- |------|--------|------------|
99
- | 1 | Acquire schema | `schema.graphql` exists |
100
- | 2 | Look up entities | Field names, types, relationships confirmed |
101
- | 3 | Generate query | `.graphql` file or inline `gql` tag |
102
- | 4 | Generate types | `graphql-operations-types.ts` |
103
- | 5 | Validate | Lint + codegen pass |
104
-
105
- ### Step 1: Acquire Schema
84
+ ## Step 0 — Route the task
106
85
 
107
- The `schema.graphql` file (265K+ lines) is the source of truth. **Never open or parse it directly** — no cat, less, head, tail, editors, or programmatic parsers.
108
-
109
- Verify preconditions 1–3 (see [Preconditions](#preconditions--verify-before-starting)), then proceed to Step 2.
110
-
111
- ### Step 2: Look Up Entity Schema
112
-
113
- Map user intent to PascalCase names ("accounts" → `Account`), then **run the search script from the `sfdx-project` folder (project root)**:
114
-
115
- ```bash
116
- bash scripts/graphql-search.sh Account
117
- # Multiple entities:
118
- bash scripts/graphql-search.sh Account Contact Opportunity
119
- ```
120
-
121
- The script outputs seven sections per entity:
122
- 1. **Type definition** — all queryable fields and relationships
123
- 2. **Filter options** — available fields for `where:` conditions
124
- 3. **Sort options** — available fields for `orderBy:`
125
- 4. **Create mutation wrapper** — `<Entity>CreateInput`
126
- 5. **Create mutation fields** — `<Entity>CreateRepresentation` (fields accepted by create mutations)
127
- 6. **Update mutation wrapper** — `<Entity>UpdateInput`
128
- 7. **Update mutation fields** — `<Entity>UpdateRepresentation` (fields accepted by update mutations)
129
-
130
- **Maximum 2 script runs.** If the entity still can't be found, ask the user — the object may not be deployed.
131
-
132
- #### Entity Identification
133
-
134
- If a candidate does not match:
135
- - Try `__c` suffix for custom objects, `__e` for platform events
136
- - Try `_Record` suffix — objects added in v60+ may use `<EntityName>_Record`
137
- - If still unresolved, **ask the user** — do not guess
138
-
139
- #### Iterative Introspection (max 3 cycles)
140
-
141
- 1. **Introspect** — Run the script for each unresolved entity
142
- 2. **Fields** — Extract requested field names and types from the type definition
143
- 3. **References** — Identify reference fields. If polymorphic (multiple types), use inline fragments. Add newly discovered entity types to the working list.
144
- 4. **Child relationships** — Identify Connection types. Add child entity types to the working list.
145
- 5. **Repeat** if unresolved entities remain (max 3 cycles)
146
-
147
- **Hard stops:** If no data returned for an entity, stop — it may not be deployed. If unknown entities remain after 3 cycles, ask the user. Do not generate queries with unconfirmed entities or fields.
148
-
149
- ### Step 3: Generate Query
150
-
151
- Every field name **must** be verified from the script output in Step 2.
152
-
153
- #### Read Query Template
154
-
155
- ```graphql
156
- query QueryName($after: String) {
157
- uiapi {
158
- query {
159
- EntityName(
160
- first: 10
161
- after: $after
162
- where: { ... }
163
- orderBy: { ... }
164
- ) {
165
- edges {
166
- node {
167
- Id
168
- FieldName @optional { value }
169
- # Parent relationship (non-polymorphic)
170
- Owner @optional { Name { value } }
171
- # Parent relationship (polymorphic — use fragments)
172
- What @optional {
173
- ...WhatAccount
174
- ...WhatOpportunity
175
- }
176
- # Child relationship — max 1 level, no grandchildren
177
- Contacts @optional(first: 10) {
178
- edges { node { Name @optional { value } } }
179
- }
180
- }
181
- }
182
- pageInfo { hasNextPage endCursor }
183
- }
184
- }
185
- }
186
- }
187
-
188
- fragment WhatAccount on Account {
189
- Id
190
- Name @optional { value }
191
- }
192
- fragment WhatOpportunity on Opportunity {
193
- Id
194
- Name @optional { value }
195
- }
196
- ```
86
+ | The task is | Go to |
87
+ |---|---|
88
+ | Read records | **[Read workflow](#read-workflow)** below |
89
+ | Create / update / delete records | **[Write workflow](#write-workflow)** below |
90
+ | Object/field metadata, picklist values, related-list metadata, aggregations | **[Beyond record CRUD](#beyond-record-crud)** below |
91
+ | Data is stale / "add a refresh button" / "cache it longer" | **[Freshness & caching](#freshness--caching)** below |
92
+ | Something GraphQL can't express (Apex REST, file upload, Einstein) | [references/rest-and-integration.md](references/rest-and-integration.md) |
93
+ | Migrating old `sdk.graphql?.(query, vars)` code | **[Working on existing code](#working-on-existing-code-migration)** below |
197
94
 
198
- **Consuming code must defend against missing fields:**
95
+ GraphQL covers far more than record reads and writes — prefer it for **anything the `uiapi`
96
+ namespace exposes** (see [Beyond record CRUD](#beyond-record-crud)). Reach for REST only when
97
+ the data genuinely lives outside `uiapi` (Apex REST, file upload, Einstein) — see
98
+ [references/rest-and-integration.md](references/rest-and-integration.md).
199
99
 
200
- ```typescript
201
- const name = node.Name?.value ?? "";
202
- const relatedName = node.Owner?.Name?.value ?? "N/A";
203
- ```
204
-
205
- #### Filtering
206
-
207
- ```graphql
208
- # Implicit AND
209
- Account(where: { Industry: { eq: "Technology" }, AnnualRevenue: { gt: 1000000 } })
210
-
211
- # Explicit OR
212
- Account(where: { OR: [{ Industry: { eq: "Technology" } }, { Industry: { eq: "Finance" } }] })
213
-
214
- # NOT
215
- Account(where: { NOT: { Industry: { eq: "Technology" } } })
216
-
217
- # Date literal
218
- Opportunity(where: { CloseDate: { eq: { value: "2024-12-31" } } })
219
-
220
- # Relative date
221
- Opportunity(where: { CloseDate: { gte: { literal: TODAY } } })
222
-
223
- # Relationship filter (nested objects, NOT dot notation)
224
- Contact(where: { Account: { Name: { like: "Acme%" } } })
225
-
226
- # Polymorphic relationship filter
227
- Account(where: { Owner: { User: { Username: { like: "admin%" } } } })
228
- ```
229
-
230
- String equality (`eq`) is case-insensitive. Both 15-char and 18-char record IDs are accepted.
231
-
232
- #### Ordering
233
-
234
- ```graphql
235
- Account(
236
- first: 10,
237
- orderBy: { Name: { order: ASC }, CreatedDate: { order: DESC } }
238
- ) { ... }
239
- ```
240
-
241
- Unsupported for ordering: multi-select picklist, rich text, long text area, encrypted fields. Add `Id` as tie-breaker for deterministic ordering.
242
-
243
- #### UpperBound Pagination (v59+)
244
-
245
- For >200 records per page or >4,000 total records, use `upperBound`. `first` must be 200–2000 when set.
246
-
247
- ```graphql
248
- Account(first: 2000, after: $cursor, upperBound: 10000) {
249
- edges { node { Id Name @optional { value } } }
250
- pageInfo { hasNextPage endCursor }
251
- }
252
- ```
253
-
254
- #### Semi-Join and Anti-Join
255
-
256
- 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 }`.
257
-
258
- ```graphql
259
- query SemiJoinExample {
260
- uiapi {
261
- query {
262
- Account(where: {
263
- Id: {
264
- inq: {
265
- Contact: { LastName: { like: "Smith%" } }
266
- ApiName: "AccountId"
267
- }
268
- }
269
- }, first: 10) {
270
- edges { node { Id Name @optional { value } } }
271
- }
272
- }
273
- }
274
- }
275
- ```
276
-
277
- Replace `inq` with `ninq` for anti-join. Restrictions: no `OR` in subquery, no `orderBy` in subquery, no nesting joins within each other.
278
-
279
- #### Current User
280
-
281
- Use `uiapi.currentUser` (no arguments) instead of the standard query pattern:
282
-
283
- ```graphql
284
- query CurrentUser {
285
- uiapi { currentUser { Id Name { value } } }
286
- }
287
- ```
100
+ ---
288
101
 
289
- #### Field Value Wrappers
102
+ ## Preconditions verify before writing any query
290
103
 
291
- Schema fields use typed wrappers access via `.value`:
104
+ `<skill-dir>` below is wherever this skill is installed (the directory this
105
+ `SKILL.md` loaded from). The schema-lookup script ships inside it. The script does
106
+ **not** hunt for `schema.graphql` by walking up the tree — an ancestor schema can
107
+ belong to a different org and would validate fields against the wrong one. Resolve
108
+ the schema explicitly: run from the SFDX project root (where `schema.graphql` lives),
109
+ or pass `--schema <path>` / set `GRAPHQL_SCHEMA=<path>`. The script echoes the schema
110
+ it resolved (`[graphql-search] using schema: …` on stderr) — glance at it to confirm
111
+ you grounded against the right file.
292
112
 
293
- | Wrapper Type | Underlying | Wrapper Type | Underlying |
113
+ | # | Requirement | Verify | If missing |
294
114
  |---|---|---|---|
295
- | `StringValue` | `String` | `BooleanValue` | `Boolean` |
296
- | `IntValue` | `Int` | `DoubleValue` | `Double` |
297
- | `CurrencyValue` | `Currency` | `PercentValue` | `Percent` |
298
- | `DateTimeValue` | `DateTime` | `DateValue` | `Date` |
299
- | `PicklistValue` | `Picklist` | `LongValue` | `Long` |
300
- | `IDValue` | `ID` | `TextAreaValue` | `TextArea` |
301
- | `EmailValue` | `Email` | `PhoneNumberValue` | `PhoneNumber` |
302
- | `UrlValue` | `Url` | | |
303
-
304
- All wrappers also expose `displayValue: String` (server-rendered via `toLabel()`/`format()`) — use for UI display instead of formatting client-side.
305
-
306
- #### Mutation Template
307
-
308
- Mutations are GA in API v66+. Three operations: **Create**, **Update**, **Delete**.
309
-
310
- ```graphql
311
- # Create
312
- mutation CreateAccount($input: AccountCreateInput!) {
313
- uiapi(input: { allOrNone: true }) {
314
- AccountCreate(input: $input) {
315
- Record { Id Name { value } }
316
- }
317
- }
318
- }
319
-
320
- # Update — must include Id
321
- mutation UpdateAccount {
322
- uiapi(input: { allOrNone: true }) {
323
- AccountUpdate(input: { Id: "001xx000003GYkZAAW", Account: { Name: "New Name" } }) {
324
- Record { Id Name { value } }
325
- }
326
- }
327
- }
328
- ```
329
-
330
- **Input constraints:**
331
- - **Create**: Required fields (unless `defaultedOnCreate`), only `createable` fields, no child relationships. Reference fields set by `ApiName` (e.g., `AccountId`).
332
- - **Update**: Must include `Id`, only `updateable` fields, no child relationships.
333
- - **Delete**: `Id` only.
334
- - **`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.
335
- - **Raw values**: No commas, currency symbols, or locale formatting (e.g., `80000` not `"$80,000"`).
336
-
337
- **Output constraints:**
338
- - Create/Update: Exclude child relationships, exclude navigated reference fields (only `ApiName` member allowed). Output field is always named `Record`.
339
- - Delete: `Id` only.
340
-
341
- **`allOrNone` semantics:**
342
- - `true` (default) — All operations succeed or all roll back.
343
- - `false` — Independent operations succeed individually, but dependent operations (using `@{alias}`) still roll back together.
344
-
345
- #### Mutation Chaining
346
-
347
- Chain related mutations using `@{alias}` references to `Id` from earlier mutations. Required for parent-child creation (nested child creates are not supported).
348
-
349
- ```graphql
350
- mutation CreateAccountAndContact {
351
- uiapi(input: { allOrNone: true }) {
352
- AccountCreate(input: { Account: { Name: "Acme" } }) {
353
- Record { Id }
354
- }
355
- ContactCreate(input: { Contact: { LastName: "Smith", AccountId: "@{AccountCreate}" } }) {
356
- Record { Id }
357
- }
358
- }
359
- }
360
- ```
361
-
362
- 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`).
363
-
364
- #### Delete Mutation
365
-
366
- Delete uses generic `RecordDeleteInput` (not entity-specific). Output is `Id` only — no `Record` field.
367
-
368
- ```graphql
369
- mutation DeleteAccount($id: ID!) {
370
- uiapi(input: { allOrNone: true }) {
371
- AccountDelete(input: { Id: $id }) {
372
- Id
373
- }
374
- }
375
- }
376
- ```
377
-
378
- #### Object Metadata & Picklist Values
379
-
380
- Use `uiapi { objectInfos(...) }` to fetch field metadata or picklist values. Pass **either** `apiNames` or `objectInfoInputs` — never both.
381
-
382
- ```typescript
383
- // Object metadata
384
- const GET_OBJECT_INFO = gql`
385
- query GetObjectInfo($apiNames: [String!]!) {
386
- uiapi {
387
- objectInfos(apiNames: $apiNames) {
388
- ApiName
389
- label
390
- labelPlural
391
- fields { ApiName label dataType updateable createable }
392
- }
393
- }
394
- }
395
- `;
396
-
397
- // Picklist values (use objectInfoInputs + inline fragment)
398
- const GET_PICKLIST_VALUES = gql`
399
- query GetPicklistValues($objectInfoInputs: [ObjectInfoInput!]!) {
400
- uiapi {
401
- objectInfos(objectInfoInputs: $objectInfoInputs) {
402
- ApiName
403
- fields {
404
- ApiName
405
- ... on PicklistField {
406
- picklistValuesByRecordTypeIDs {
407
- recordTypeID
408
- picklistValues { label value }
409
- }
410
- }
411
- }
412
- }
413
- }
414
- }
415
- `;
416
- ```
417
-
418
- ### Step 4: Generate Types (codegen)
419
-
420
- After writing the query (whether in a `.graphql` file or inline with `gql`), generate TypeScript types:
421
-
422
- ```bash
423
- # Run from UI bundle dir
424
- npm run graphql:codegen
425
- ```
426
-
427
- Output: `src/api/graphql-operations-types.ts`
428
-
429
- Generated type naming conventions:
430
- - `<OperationName>Query` / `<OperationName>Mutation` — response types
431
- - `<OperationName>QueryVariables` / `<OperationName>MutationVariables` — variable types
432
-
433
- **Always import and use the generated types** when calling `sdk.graphql`:
434
-
435
- ```typescript
436
- import type { GetAccountsQuery, GetAccountsQueryVariables } from "../graphql-operations-types";
437
-
438
- const response = await sdk.graphql?.<GetAccountsQuery, GetAccountsQueryVariables>(GET_ACCOUNTS, variables);
439
- ```
440
-
441
- Use `NodeOfConnection<T>` to extract the node type from a Connection for cleaner typing:
442
-
443
- ```typescript
444
- import { type NodeOfConnection } from "@salesforce/sdk-data";
445
-
446
- type AccountNode = NodeOfConnection<GetAccountsQuery["uiapi"]["query"]["Account"]>;
447
- ```
115
+ | 1 | `@salesforce/platform-sdk` installed | `package.json` in the UI bundle dir | Tell user to install it; cannot proceed |
116
+ | 2 | A grounding tool resolves | **Preferred:** `npx graphiti sf-gql-discover '{"org":"<alias>","mode":"list_objects"}'` from the UI bundle dir returns objects. **Fallback:** `bash <skill-dir>/scripts/graphql-search.sh <Entity>` from the project root prints a lookup, not "schema.graphql not found" | No graphiti dep / org won't prime → use the script. Script can't find `schema.graphql` pass `--schema <path>`, or `npm run graphql:schema` from the UI bundle dir. ([references/graphiti-cli.md](references/graphiti-cli.md) covers CLI setup) |
117
+ | 3 | Target objects/fields deployed | The object appears in `sf-gql-discover` (or `graphql-search.sh <Entity>` returns output) | Entity absent usually means it isn't deployed (or the cache/schema is stale). Refresh: `npx graphiti sf-gql-connect '{"org":"<alias>","forceRefresh":true}'` (CLI) or `npm run graphql:schema` (script). If still absent, deploy the metadata (the **platform-metadata-deploy** skill handles this) and assign the permission sets, then re-check |
448
118
 
449
- ### Step 5: Validate & Test
450
-
451
- 1. **Lint**: `npx eslint <file>` from UI bundle dir
452
- 2. **codegen**: `npm run graphql:codegen` from UI bundle dir
453
-
454
- #### Common Error patterns
455
-
456
- | Error Contains | Resolution |
457
- |----------------|------------|
458
- | `Cannot query field` / `ValidationError` | Field name wrong — re-run `graphql-search.sh <Entity>` |
459
- | `Unknown type` | Type name wrong — verify PascalCase entity name via script |
460
- | `Unknown argument` | Argument wrong — check Filter/OrderBy sections in script output |
461
- | `invalid syntax` / `InvalidSyntax` | Fix syntax per error message |
462
- | `VariableTypeMismatch` / `UnknownType` | Correct argument type from schema |
463
- | `invalid cross reference id` | Entity deleted — ask for valid Id |
464
- | `OperationNotSupported` | Check object availability and API version |
465
- | `is not currently available in mutation results` | Remove field from mutation output |
466
- | `Cannot invoke JsonElement.isJsonObject()` | Use API version 64+ for update mutation `Record` selection |
467
-
468
- **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.
119
+ If preconditions aren't met you may still scaffold components, routes, and layout — but
120
+ use empty arrays / `null` for data, mark query sites with
121
+ `// TODO: add query after schema verification`, and add a plan item to return. Do **not**
122
+ write GraphQL strings until the schema workflow is complete.
469
123
 
470
124
  ---
471
125
 
472
- ## UI Bundle Integration (React)
473
-
474
- Two integration patterns:
475
-
476
- ### Pattern 1 — External `.graphql` file (complex queries)
477
-
478
- **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.
126
+ ## Read workflow
127
+
128
+ 1. **Look up the schema first — never guess a name.** **Preferred (graphiti):** when the exact
129
+ API name is at all uncertain, **list before you describe** —
130
+ `npx graphiti sf-gql-discover '{"org":"<alias>","mode":"list_objects","search":"<intent>"}'`
131
+ to find the real name, then
132
+ `npx graphiti sf-gql-discover '{"org":"<alias>","mode":"describe_object","object":"<Entity>"}'`
133
+ for exact field/type names, picklist values, filterable/sortable. An empty list or missing object
134
+ is a **fact about the org** (wrong name or not deployed), **not a tool failure** — re-list or
135
+ `forceRefresh`; **do not fall back to the script for this** (see guardrail 2). **Fallback** is
136
+ only for a CLI that genuinely can't run (no graphiti dep / org won't prime):
137
+ `bash <skill-dir>/scripts/graphql-search.sh <Entity>` from the SFDX project root.
138
+ (Full rules: [references/graphql-hand-authoring.md](references/graphql-hand-authoring.md).)
139
+ 2. **Write the query.** **Preferred — compile it with graphiti:**
140
+ `npx graphiti sf-gql-list '{"org":"<alias>","object":"<Entity>","fields":[…],"first":N}'`
141
+ returns a `{ query, variables, types, warnings }` envelope with `@optional`, `value`/`displayValue`,
142
+ `edges/node`, and `first:`/`pageInfo` **already applied**. Confirm `warnings: []` (a non-empty
143
+ array means the object wasn't in the primed schema — the query is degraded; don't ship it), then
144
+ paste the `query` verbatim into inline `gql` (simple) or an external `.graphql` file (one operation
145
+ per file, imported with the bundler's `?raw` suffix — `import Q from "./q.graphql?raw"` brings the
146
+ file in as a plain string). **Fallback — hand-author:** apply `@optional` to every **selectable
147
+ FLS-gated field** — scalar leaf fields (`Name @optional { value }`) and parent/child
148
+ relationships *and* the fields inside them — but **NOT** on `Id`, on connection plumbing
149
+ (`edges`, `node`, the connection field itself), or on `pageInfo`; the graphiti output leaves
150
+ those bare and is the canonical placement. Always set `first:`, include `pageInfo` if it may
151
+ page. Either way, full mechanics and the primed-vs-degraded behavior:
152
+ [references/graphiti-cli.md](references/graphiti-cli.md).
153
+ 3. **Generate types** — `npm run graphql:codegen` (from the UI bundle dir) →
154
+ `src/api/graphql-operations-types.ts`.
155
+ 4. **Call `query()`** with the generated types:
156
+
157
+ ```typescript
158
+ import type { GetAccountsQuery, GetAccountsQueryVariables } from "../graphql-operations-types";
159
+
160
+ const result = await sdk.graphql!.query<GetAccountsQuery, GetAccountsQueryVariables>({
161
+ query: GET_ACCOUNTS,
162
+ variables: { first: 20 },
163
+ // cacheControl, // optional — see Freshness & caching
164
+ });
165
+ ```
166
+ 5. **Handle the result.** `result.data` + `result.errors` are the initial snapshot;
167
+ `result.subscribe` / `result.refresh` are the reactive handles. Always check
168
+ `errors` before reading `data`:
169
+
170
+ ```typescript
171
+ if (result.errors?.length) throw new Error(result.errors.map((e) => e.message).join("; "));
172
+ const rows = result.data?.uiapi?.query?.Account?.edges?.map((e) => e.node) ?? [];
173
+ ```
174
+
175
+ Defend consuming code with `?.`/`??` (because `@optional` can omit fields). Error-handling
176
+ stances (strict / tolerant / discriminated) and `NodeOfConnection` typing: [references/sdk-api.md](references/sdk-api.md).
479
177
 
480
- ```typescript
481
- import { createDataSDK, type NodeOfConnection } from "@salesforce/sdk-data";
482
- import MY_QUERY from "./query/myQuery.graphql?raw"; // ?raw suffix required
483
- import type { GetMyDataQuery, GetMyDataQueryVariables } from "../graphql-operations-types";
484
-
485
- const sdk = await createDataSDK();
486
- const response = await sdk.graphql?.<GetMyDataQuery, GetMyDataQueryVariables>(MY_QUERY, variables);
487
- ```
178
+ ---
488
179
 
489
- After creating/changing `.graphql` files, run `npm run graphql:codegen` to generate types into `src/api/graphql-operations-types.ts`.
180
+ ## Write workflow
181
+
182
+ 1–3 as above (schema lookup → write the **mutation** → codegen). To compile the mutation with
183
+ graphiti, use `sf-gql-create` / `sf-gql-update` / `sf-gql-delete` — they emit the
184
+ `uiapi { <Object>Create(input: $input) { Record {…} } }` shape; the `types` field tells you
185
+ the input shape. Details: [references/graphiti-cli.md](references/graphiti-cli.md).
186
+ 4. **Call `mutate()`** — note the option key is **`mutation`**, not `query`, and that
187
+ mutations are **never cached**. The runtime `variables` shape differs per operation —
188
+ values are **raw** (never `{value}`-wrapped; that wrapper is a read-shape thing and breaks
189
+ writes) and nest under the **entity key**:
190
+
191
+ ```typescript
192
+ // create — input.<Entity> holds the new field values
193
+ variables: { input: { Account: { Name: "Acme", Industry: "Technology" } } }
194
+ // update — sibling Id alongside the entity key
195
+ variables: { input: { Id: "001…", Account: { Industry: "Finance" } } }
196
+ // delete — Id only, no entity key (generic RecordDeleteInput)
197
+ variables: { input: { Id: "001…" } }
198
+
199
+ const { data, errors } = await sdk.graphql!.mutate<CreateAccountMutation, CreateAccountMutationVariables>({
200
+ mutation: CREATE_ACCOUNT,
201
+ variables: { input: { Account: { Name: "Acme" } } },
202
+ });
203
+ if (errors?.length) throw new Error(errors.map((e) => e.message).join("; "));
204
+ ```
205
+
206
+ This is the **`variables` shape** the spine owns; the CLI `types`-field interpretation is in
207
+ [references/graphiti-cli.md](references/graphiti-cli.md) and the GraphQL-document field constraints
208
+ (`createable`/`updateable`, `ApiName` references, `@{alias}` chaining) in
209
+ [references/graphql-hand-authoring.md](references/graphql-hand-authoring.md).
210
+ 5. **Re-freshen affected reads.** `mutate()` has no `refresh`. To update a live list
211
+ after a write, hold the `QueryResult` from your earlier `query()` call (e.g.
212
+ `accountsResult`) and call `await accountsResult.refresh()` (forced re-fetch, pushes
213
+ to subscribers) — note this is the read's handle, not anything `mutate()` returns. See
214
+ **[Freshness & caching](#freshness--caching)**.
215
+
216
+ Mutation syntax is exacting: wrap under `uiapi(input: { allOrNone: ... })`, only
217
+ `createable`/`updateable` fields, Create/Update output is always `Record` but **Delete has no
218
+ `Record` field — select `Id` only**. Full template + chaining + constraints:
219
+ [references/graphql-hand-authoring.md](references/graphql-hand-authoring.md).
490
220
 
491
- ### Pattern 2 — Inline `gql` tag (simple queries)
221
+ ---
492
222
 
493
- **Must use `gql`** — plain template strings bypass ESLint schema validation.
223
+ ## Beyond record CRUD
494
224
 
495
- ```typescript
496
- import { createDataSDK, gql } from "@salesforce/sdk-data";
497
- import type { GetAccountsQuery } from "../graphql-operations-types";
498
-
499
- const GET_ACCOUNTS = gql`
500
- query GetAccounts {
501
- uiapi {
502
- query {
503
- Account(first: 10) {
504
- edges { node { Id Name @optional { value } } }
505
- }
506
- }
507
- }
508
- }
509
- `;
225
+ The `uiapi` namespace is not just record reads/writes. Before reaching for REST, check
226
+ whether GraphQL already covers it the same `sdk.graphql!.query()` call, different
227
+ sub-selection. The top-level `uiapi` fields:
510
228
 
511
- const sdk = await createDataSDK();
512
- const response = await sdk.graphql?.<GetAccountsQuery>(GET_ACCOUNTS);
513
- ```
229
+ | Need | Use | Returns |
230
+ |---|---|---|
231
+ | Query records | `uiapi { query { <Entity>(...) } }` | records (the [Read workflow](#read-workflow)) |
232
+ | Counts / sums / grouped rollups without pulling rows | `uiapi { aggregate { <Entity>(groupBy: …) } }` | aggregated buckets |
233
+ | Object/field metadata — labels, data types, `createable`/`updateable`, record types | `uiapi { objectInfos(apiNames: […]) }` | `ObjectInfo[]` |
234
+ | Picklist values (per record type) | `uiapi { objectInfos(objectInfoInputs: […]) { fields … on PicklistField { … } } }` | picklist values |
235
+ | Related-list metadata — display columns, ordering for a parent's related list | `uiapi { relatedListByName(parentApiName, relatedListName) }` | `RelatedListInfo` |
514
236
 
515
- ### Error Handling
237
+ Same rules as record reads: verify every type/field first, `@optional` where FLS applies, check
238
+ `result.errors`. Aggregations can be compiled with `npx graphiti sf-gql-aggregate` (pass
239
+ `groupBy` + `aggregations`); object metadata / picklists / related lists are hand-authored —
240
+ templates: [references/graphql-hand-authoring.md](references/graphql-hand-authoring.md).
516
241
 
517
- ```typescript
518
- // Strict (default) any errors = failure
519
- if (response?.errors?.length) {
520
- throw new Error(response.errors.map(e => e.message).join("; "));
521
- }
522
-
523
- // Tolerant — log errors, use available data
524
- if (response?.errors?.length) {
525
- console.warn("GraphQL partial errors:", response.errors);
526
- }
527
-
528
- // Discriminated — fail only when no data returned
529
- if (!response?.data && response?.errors?.length) {
530
- throw new Error(response.errors.map(e => e.message).join("; "));
531
- }
532
-
533
- const accounts = response?.data?.uiapi?.query?.Account?.edges?.map(e => e.node) ?? [];
534
- ```
242
+ > Two related capabilities (the **current-user** record and **layout** delivery) need
243
+ > confirmation against a current org schema before this skill documents a query shape —
244
+ > tracked as a follow-up, not yet covered here.
535
245
 
536
246
  ---
537
247
 
538
- ## REST API Patterns
248
+ ## Freshness & caching
249
+
250
+ **Caching is ON by default on WebApp.** Every `sdk.graphql!.query()` is cached with a
251
+ **300-second `max-age`** TTL — no opt-in flag, no factory, no import subpath. **Do not
252
+ build your own cache** (no React Query, SWR, `localStorage`, or hand-rolled Map). The
253
+ cache is **shared across SDK instances by `baseUrl`**: the same query+variables from a
254
+ different `createDataSDK()` targeting the same host is a cache hit. Only non-empty,
255
+ error-free `data` is cached. `mutate()` is never cached.
256
+
257
+ There are **two distinct freshness tools** — keep them separate:
258
+
259
+ 1. **Per-call `cacheControl`** — a one-shot policy override on the query options bag
260
+ (`"no-cache"` / `"only-if-cached"` / `{ type: "max-age", maxAge: <seconds> }`). The type and
261
+ exact per-value behavior live in [references/sdk-api.md](references/sdk-api.md#cachecontrol--the-per-call-cache-policy).
262
+ Take `cacheControl` as an optional param on the read function and expose each distinct policy as
263
+ a **thin named export in the same data-layer file** — a "call site" is a named export, not a new
264
+ React component. For `getAccounts(first, after?, cacheControl?)`: `export const refreshAccounts =
265
+ () => getAccounts(20, undefined, "no-cache")` (and likewise `offlineAccounts` → `"only-if-cached"`,
266
+ `shortLivedAccounts` → `{ type: "max-age", maxAge: 10 }`). Keep the policy in the data layer.
267
+ 2. **Reactive `subscribe` / `refresh`** — a stateful handle on a live `QueryResult`:
268
+ `result.subscribe(cb)` fires on every later snapshot, `result.refresh()` re-fetches bypassing
269
+ the cache and pushes to subscribers. Shape in [references/sdk-api.md](references/sdk-api.md#queryresultt--the-reactive-query-handle);
270
+ subscription lifecycle (always unsubscribe on teardown) in [references/caching.md](references/caching.md).
271
+
272
+ | Want | Reach for |
273
+ |---|---|
274
+ | Freshness within ~5 min is fine | nothing (default cache) |
275
+ | This one read must bypass the cache (refresh button) | `cacheControl: "no-cache"` |
276
+ | Read only cached data, tolerate misses (offline-first) | `cacheControl: "only-if-cached"` — a miss is **expected, not an error**: it surfaces a `DataNotFoundError` on `result.errors` (no network, no throw). Check `result.errors`, render empty state, **do not throw and do not fall back to the network** — that defeats offline-first. |
277
+ | Tighter/looser TTL for this query | `cacheControl: { type: "max-age", maxAge: 60 }` (`maxAge` is in **seconds**) |
278
+ | Mounted component reflects updates over time | `result.subscribe(cb)` |
279
+ | Re-fetch now + notify all subscribers (e.g. after a mutation) | `result.refresh()` |
280
+
281
+ `cacheControl` is fire-and-forget at call time; `subscribe`/`refresh` is a live handle.
282
+ Different mechanisms, different jobs — don't conflate "refresh" with "no-cache". Full
283
+ behavior, the reactive-subscription lifecycle, and uncached-surface caveats: [references/caching.md](references/caching.md).
539
284
 
540
- Use `sdk.fetch` when GraphQL is insufficient. See the [Supported APIs](#supported-apis) table for the full allowlist.
285
+ ---
541
286
 
542
- ```typescript
543
- declare const __SF_API_VERSION__: string;
544
- const API_VERSION = typeof __SF_API_VERSION__ !== "undefined" ? __SF_API_VERSION__ : "65.0";
545
-
546
- // Connect — file upload config
547
- const res = await sdk.fetch?.(`/services/data/v${API_VERSION}/connect/file/upload/config`);
548
-
549
- // Apex REST (no version in path)
550
- const res = await sdk.fetch?.("/services/apexrest/auth/login", {
551
- method: "POST",
552
- body: JSON.stringify({ email, password }),
553
- headers: { "Content-Type": "application/json" },
554
- });
555
-
556
- // UI API — record with metadata (prefer GraphQL for simple reads)
557
- const res = await sdk.fetch?.(`/services/data/v${API_VERSION}/ui-api/records/${recordId}`);
558
-
559
- // Einstein LLM
560
- const res = await sdk.fetch?.(`/services/data/v${API_VERSION}/einstein/llm/prompt/generations`, {
561
- method: "POST",
562
- body: JSON.stringify({ promptTextorId: prompt }),
563
- });
564
- ```
287
+ ## Working on existing code (migration)
565
288
 
566
- **Current user**: Do not use Chatter (`/chatter/users/me`). Use GraphQL instead:
289
+ **Only enter this path if the existing code actually uses the old API** — i.e. it imports
290
+ `@salesforce/sdk-data` or calls the callable `sdk.graphql(query, vars)` form. For any new
291
+ read/write, ignore migration entirely and use the [Read workflow](#read-workflow) /
292
+ [Write workflow](#write-workflow) — those already show the **only** correct API.
567
293
 
568
- ```typescript
569
- const GET_CURRENT_USER = gql`
570
- query CurrentUser {
571
- uiapi { currentUser { Id Name { value } } }
572
- }
573
- `;
574
- const response = await sdk.graphql?.(GET_CURRENT_USER);
575
- ```
294
+ When you do have old code to convert, see **[references/migration.md](references/migration.md)** for the
295
+ before→after diff (imports, query/mutate calls, optional-chaining → non-null assertion, codegen
296
+ type placement) and a checklist. The target API is exactly what the Read/Write workflows above
297
+ prescribe migrating is just swapping the old form for that.
576
298
 
577
299
  ---
578
300
 
579
- ## Directory Structure
580
-
581
- ```
582
- <project-root>/ ← SFDX project root
583
- ├── schema.graphql ← grep target (lives here)
584
- ├── sfdx-project.json
585
- ├── scripts/graphql-search.sh ← schema lookup script
586
- └── force-app/main/default/uiBundles/<app-name>/ ← UI bundle dir
587
- ├── package.json ← npm scripts
588
- └── src/
589
- ```
590
-
591
- | Command | Run From | Why |
592
- |---------|----------|-----|
593
- | `npm run graphql:schema` | UI bundle dir | Script in UI bundle's package.json |
594
- | `npm run graphql:codegen` | UI bundle dir | Generate GraphQL types |
595
- | `npx eslint <file>` | UI bundle dir | Reads eslint.config.js |
596
- | `bash scripts/graphql-search.sh <Entity>` | project root | Schema lookup |
301
+ ## Platform guardrails — never regress these
302
+
303
+ These are Salesforce GraphQL platform behaviors, independent of the SDK. Violations cause
304
+ silent runtime failures. (Details + templates: [references/graphql-hand-authoring.md](references/graphql-hand-authoring.md).)
305
+
306
+ 1. **HTTP 200 ≠ success** — always parse `result.errors`; the Promise resolves even on failure.
307
+ 2. **Schema is the only source of truth — verify, never invent.** Verify every
308
+ entity/field/type via graphiti `sf-gql-discover` (preferred) or
309
+ `bash <skill-dir>/scripts/graphql-search.sh <Entity>` before use. Case-sensitive;
310
+ `__c`/`__e`; `_Record` entity suffix (v60+). When graphiti is primed, a
311
+ "not found"/empty/`Cannot query field` answer (including from
312
+ `graphql-codegen`/`@graphql-eslint`, even when the message points at `schema.graphql`)
313
+ is a **fact about the org** wrong name or undeployed/inaccessible metadata, not a tool
314
+ failure: fix the operation, or deploy the metadata (the **platform-metadata-deploy** skill)
315
+ + assign perms + refresh (`sf-gql-connect --forceRefresh` / `npm run graphql:schema`). Do
316
+ not fall back to the script, hand-author around it, or **guess a name** — a guessed entity or
317
+ field silently fails the whole query at runtime; if lookups aren't converging, **ask the user
318
+ rather than keep spiraling**. **`schema.graphql` and the codegen output
319
+ (`src/api/graphql-operations-types.ts`) are read-only generated mirrors — never open or edit
320
+ them** (honor any `# DO NOT EDIT` marker). Hand-adding a missing type satisfies codegen/lint
321
+ but grants no org access; it just hides the failure until runtime. Fall back to the script
322
+ *only* when the CLI can't run at all (no dep / `SCHEMA_PRIME_FAILED`).
323
+ 3. **`@optional` on every FLS-gated field at each nesting level** — scalar leaf fields plus each
324
+ parent/child relationship *and* the fields inside it (FLS fails the whole query otherwise, v65+).
325
+ **Do NOT** decorate `Id`, the connection plumbing (`edges`, `node`, the connection field), or
326
+ `pageInfo` — those are not FLS-gated and the graphiti output leaves them bare. Consume with
327
+ `?.`/`??`. Placement rules: [references/graphql-hand-authoring.md](references/graphql-hand-authoring.md).
328
+ 4. **Mutations** wrap under `uiapi(input: { allOrNone: ... })`; set `allOrNone` explicitly;
329
+ output excludes child/navigated-reference fields; the output field is literally named
330
+ `Record` (unrelated to the `_Record` entity suffix in rule 2) — Delete → `Id` only. GA v66+.
331
+ 5. **Explicit pagination** — always set `first:`, because the server silently caps at 10 and
332
+ you'll drop rows with no error; forward-only (`first`/`after`, no `last`/`before`);
333
+ `upperBound` (v59+) raises the per-request ceiling for large sets (when set, `first` must be 200–2000).
334
+ 6. **SOQL governor limits apply** — `uiapi` queries compile to SOQL, so the same governor
335
+ limits are inherited: ≤10 subqueries, ≤5 child→parent levels, ≤1 parent→child level,
336
+ ≤2,000 records/subquery. Split into multiple requests if you'd exceed them.
337
+ 7. **Field value wrappers** — read the raw value via `.value`; `displayValue` is the
338
+ server-formatted string for UI. When a field is both shown *and* operated on (currency,
339
+ dates, picklists), select **both** `value` and `displayValue` so you don't reformat on the
340
+ client. Display-only fields can take just `displayValue`.
341
+ 8. **Compound fields** — filter/order on constituents (`BillingCity`), not the wrapper (`BillingAddress`).
342
+ 9. **Supported APIs only** — GraphQL (`uiapi`), UI API REST, Apex REST, Connect REST,
343
+ Einstein LLM via `sdk.fetch`. NOT: Enterprise SOQL `/query`, Aura-enabled Apex, Chatter
344
+ (use `uiapi.currentUser`). See [references/rest-and-integration.md](references/rest-and-integration.md).
345
+
346
+ > One SDK convention lives in the workflows, not this list (it's not a platform behavior):
347
+ > always run `npm run graphql:codegen` and use the generated types after writing an operation
348
+ > ([Read workflow](#read-workflow) step 3). Also in the [Pre-flight checklist](#pre-flight-checklist).
349
+ >
350
+ > **graphiti applies most of these for you.** When you compile a query with `sf-gql-*` against an
351
+ > object that's in the primed schema, rules 3 (`@optional`), 4 (mutation `Record` *output*
352
+ > envelope and entity-keyed input — **not** `allOrNone`, which you still add yourself),
353
+ > 5 (`first:`/`pageInfo`), and 7 (`value`/`displayValue` wrappers) come out already satisfied —
354
+ > which is exactly why you **paste the `query` verbatim** rather than re-deriving it. Rules 1
355
+ > (check `result.errors`), 6 (governor limits), 8 (compound fields), and 9 (supported APIs) are
356
+ > still on you. And the automation only fires when the object is primed: a non-empty `warnings`
357
+ > array means it isn't, and the emitted query is **degraded** (bare fields, no guardrails) —
358
+ > see [references/graphiti-cli.md](references/graphiti-cli.md#primed-vs-degraded--why-the-guardrails-sometimes-vanish).
597
359
 
598
360
  ---
599
361
 
600
- ## Quick Reference
601
-
602
- ### Schema Lookup (from project root)
362
+ ## Commands & layout
603
363
 
604
- Run the search script to get all relevant schema info in one step:
364
+ ```text
365
+ <skill-dir>/ ← wherever this skill is installed
366
+ └── scripts/graphql-search.sh ← schema lookup (ships with the skill)
605
367
 
606
- ```bash
607
- bash scripts/graphql-search.sh <EntityName>
368
+ <project-root>/ ← SFDX project root; run the script from here
369
+ ├── schema.graphql ← generated mirror; grep target (never open or edit; script reads ./schema.graphql)
370
+ └── force-app/main/default/uiBundles/<app>/ ← UI bundle dir
371
+ ├── package.json ← npm scripts
372
+ └── src/api/ ← queries, generated types, SDK calls
608
373
  ```
609
374
 
610
- | Script Output Section | Used For |
611
- |-----------------------|----------|
612
- | Type definition | Field names, parent/child relationships |
613
- | Filter options | `where:` conditions |
614
- | Sort options | `orderBy:` |
615
- | CreateRepresentation | Create mutation field list |
616
- | UpdateRepresentation | Update mutation field list |
617
-
618
- ### Error Categories
619
-
620
- | Error Contains | Resolution |
621
- |----------------|------------|
622
- | `Cannot query field` | Field name is wrong — run `graphql-search.sh <Entity>` and use the exact name from the Type definition section |
623
- | `Unknown type` | Type name is wrong run `graphql-search.sh <Entity>` to confirm the correct PascalCase entity name |
624
- | `Unknown argument` | Argument name is wrong run `graphql-search.sh <Entity>` and check Filter or OrderBy sections |
625
- | `invalid syntax` | Fix syntax per error message |
626
- | `validation error` | Field name is wrong — run `graphql-search.sh <Entity>` to verify |
627
- | `VariableTypeMismatch` | Correct argument type from schema |
628
- | `invalid cross reference id` | Entity deleted ask for valid Id |
629
-
630
- ### Checklist
631
-
632
- - [ ] All field names verified via search script (Step 2)
633
- - [ ] `@optional` applied to all record fields (reads)
634
- - [ ] Mutations use `uiapi(input: { allOrNone: ... })` wrapper
635
- - [ ] `first:` specified in every query
636
- - [ ] Optional chaining in consuming code
637
- - [ ] `errors` array checked in response handling
638
- - [ ] Lint passes: `npx eslint <file>`
375
+ | Command | Run from | Purpose |
376
+ |---|---|---|
377
+ | `npx graphiti sf-gql-discover '{…}'` | UI bundle dir | Discover objects/fields against the live org (preferred grounding) |
378
+ | `npx graphiti sf-gql-<list\|detail\|aggregate\|create\|update\|delete\|raw> '{…}'` | UI bundle dir | Compile a guardrail-applied query/mutation ([references/graphiti-cli.md](references/graphiti-cli.md)) |
379
+ | `npx graphiti sf-gql-connect '{"org":"<alias>","forceRefresh":true}'` | UI bundle dir | Refresh graphiti's schema cache after a deploy |
380
+ | `bash <skill-dir>/scripts/graphql-search.sh <Entity>` | project root (or pass `--schema <path>`; no tree walk-up) | Schema lookup fallback (grep over local `schema.graphql`) |
381
+ | `npm run graphql:schema` | UI bundle dir | Fetch/refresh `schema.graphql` (for the fallback script) |
382
+ | `npm run graphql:codegen` | UI bundle dir | Generate operation types |
383
+ | `npx eslint <file>` | UI bundle dir | Lint (catches `gql` schema violations) |
384
+
385
+ ## Pre-flight checklist
386
+
387
+ - [ ] Surface decided: `sdk.graphql!` only if WebApp-only; otherwise guard with `if (!sdk.graphql) …` ([Surfaces](#surfaces--sdkgraphql-vs-guard))
388
+ - [ ] Every field/entity verified `sf-gql-discover` (preferred) or `graphql-search.sh` (fallback, against the right schema)
389
+ - [ ] If compiled with graphiti: `warnings: []` confirmed (non-empty = degraded query, don't ship); `query` pasted verbatim
390
+ - [ ] `@optional` on FLS-gated fields + relationships (NOT `Id`/`edges`/`node`/`pageInfo`); `?.`/`??` in consuming code
391
+ - [ ] `result.errors` checked before reading `result.data`
392
+ - [ ] Caching considered: default 300s OK, or `cacheControl` / `refresh` chosen deliberately
393
+ - [ ] `npm run graphql:codegen` run; generated types used; `npx eslint` passes