@salesforce/afv-skills 1.24.0 → 1.26.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 (84) 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/dx-devops-test-failures-analyze/SKILL.md +89 -0
  5. package/skills/dx-devops-test-failures-analyze/references/code-analyzer-violations.md +26 -0
  6. package/skills/dx-devops-test-failures-analyze/references/failure-categories.md +85 -0
  7. package/skills/{checking-devops-prerequisites/SKILL.md → dx-devops-test-failures-analyze/references/prerequisite-checks.md} +8 -37
  8. package/skills/{creating-fix-work-item/SKILL.md → dx-devops-test-failures-analyze/references/work-item-creation.md} +8 -12
  9. package/skills/dx-devops-test-pipeline-configure/SKILL.md +72 -0
  10. package/skills/dx-devops-test-pipeline-configure/references/configuring-quality-gate.md +133 -0
  11. package/skills/dx-devops-test-pipeline-configure/references/configuring-test-provider.md +80 -0
  12. package/skills/dx-devops-test-pipeline-configure/references/error-handling.md +39 -0
  13. package/skills/dx-devops-test-pipeline-configure/references/gotchas.md +37 -0
  14. package/skills/dx-devops-test-pipeline-configure/references/prerequisite-checks.md +112 -0
  15. package/skills/dx-devops-test-pipeline-configure/references/syncing-test-providers.md +69 -0
  16. package/skills/dx-devops-test-suite-assignments-configure/SKILL.md +74 -0
  17. package/skills/dx-devops-test-suite-assignments-configure/references/api-endpoint.md +30 -0
  18. package/skills/dx-devops-test-suite-assignments-configure/references/error-handling.md +14 -0
  19. package/skills/dx-devops-test-suite-assignments-configure/references/prerequisite-checks.md +112 -0
  20. package/skills/{recommending-devops-tests/SKILL.md → dx-devops-test-suite-assignments-configure/references/recommendation-logic.md} +10 -26
  21. package/skills/dx-devops-test-suite-assignments-configure/references/suite-assignment-modes.md +99 -0
  22. package/skills/dx-devops-test-suite-run/SKILL.md +111 -0
  23. package/skills/dx-devops-test-suite-run/references/error-handling.md +31 -0
  24. package/skills/dx-devops-test-suite-run/references/polling-configuration.md +78 -0
  25. package/skills/dx-devops-test-suite-run/references/prerequisite-checks.md +112 -0
  26. package/skills/dx-devops-test-suite-run/references/retrigger-mode.md +51 -0
  27. package/skills/dx-org-manage/SKILL.md +192 -0
  28. package/skills/dx-org-manage/examples/README.md +45 -0
  29. package/skills/dx-org-manage/examples/scratch-orgs/error_no_devhub.json +9 -0
  30. package/skills/dx-org-manage/examples/scratch-orgs/error_timeout.json +13 -0
  31. package/skills/dx-org-manage/examples/scratch-orgs/success_definition_file.json +28 -0
  32. package/skills/dx-org-manage/examples/scratch-orgs/success_edition.json +26 -0
  33. package/skills/dx-org-manage/examples/scratch-orgs/success_snapshot.json +27 -0
  34. package/skills/dx-org-manage/examples/snapshots/error_output.json +9 -0
  35. package/skills/dx-org-manage/examples/snapshots/success_output.json +15 -0
  36. package/skills/dx-org-manage/references/cli_flags.md +67 -0
  37. package/skills/dx-org-manage/references/creating-scratch-org.md +164 -0
  38. package/skills/dx-org-manage/references/creating-snapshot.md +103 -0
  39. package/skills/dx-org-manage/references/definition_file_options.md +224 -0
  40. package/skills/dx-org-manage/references/edition_types.md +78 -0
  41. package/skills/dx-org-manage/references/opening-org.md +160 -0
  42. package/skills/dx-org-manage/references/snapshot_usage.md +74 -0
  43. package/skills/dx-org-permission-set-assign/SKILL.md +98 -0
  44. package/skills/dx-org-permission-set-assign/examples/error_output.json +19 -0
  45. package/skills/dx-org-permission-set-assign/examples/success_output.json +16 -0
  46. package/skills/dx-org-permission-set-assign/references/cli_flags.md +68 -0
  47. package/skills/experience-cms-brand-apply/SKILL.md +1 -1
  48. package/skills/experience-ui-bundle-app-coordinate/SKILL.md +31 -19
  49. package/skills/experience-ui-bundle-file-upload-generate/SKILL.md +1 -1
  50. package/skills/experience-ui-bundle-frontend-generate/implementation/header-footer.md +1 -1
  51. package/skills/experience-ui-bundle-salesforce-data-access/SKILL.md +336 -581
  52. package/skills/experience-ui-bundle-salesforce-data-access/references/caching.md +172 -0
  53. package/skills/experience-ui-bundle-salesforce-data-access/references/graphiti-cli.md +373 -0
  54. package/skills/experience-ui-bundle-salesforce-data-access/references/graphql-hand-authoring.md +376 -0
  55. package/skills/experience-ui-bundle-salesforce-data-access/references/migration.md +119 -0
  56. package/skills/experience-ui-bundle-salesforce-data-access/references/rest-and-integration.md +152 -0
  57. package/skills/experience-ui-bundle-salesforce-data-access/references/sdk-api.md +217 -0
  58. package/skills/experience-ui-bundle-salesforce-data-access/scripts/graphql-search.sh +36 -9
  59. package/skills/platform-agentsetup-categories-fetch/SKILL.md +109 -0
  60. package/skills/platform-agentsetup-categories-fetch/references/api-response-schema.md +121 -0
  61. package/skills/platform-custom-object-generate/SKILL.md +62 -7
  62. package/skills/platform-custom-object-generate/references/description-enrichment.md +125 -0
  63. package/skills/platform-metadata-retrieve/SKILL.md +121 -0
  64. package/skills/platform-metadata-retrieve/examples/error_output.json +10 -0
  65. package/skills/platform-metadata-retrieve/examples/success_output.json +27 -0
  66. package/skills/platform-metadata-retrieve/references/cli_flags.md +138 -0
  67. package/skills/platform-metadata-retrieve/references/retrieval_modes.md +181 -0
  68. package/skills/platform-sharing-rules-generate/SKILL.md +165 -0
  69. package/skills/platform-sharing-rules-generate/references/rule-types.md +199 -0
  70. package/skills/platform-tracing-agentforce-configure/SKILL.md +118 -0
  71. package/skills/platform-tracing-agentforce-configure/assets/AgentforcePlatformTracing-template.xml +4 -0
  72. package/skills/platform-tracing-configure/SKILL.md +118 -0
  73. package/skills/platform-tracing-configure/assets/EventSettings-template.xml +4 -0
  74. package/skills/platform-trust-archive-manage/SKILL.md +25 -11
  75. package/skills/platform-trust-archive-manage/examples/monitor-failed-jobs.md +2 -2
  76. package/skills/platform-trust-archive-manage/references/archive-activity-entity.md +1 -1
  77. package/skills/platform-trust-archive-manage/references/connect-api-operations.md +51 -12
  78. package/skills/analyzing-test-failures/SKILL.md +0 -159
  79. package/skills/configuring-quality-gate/SKILL.md +0 -120
  80. package/skills/configuring-test-provider/SKILL.md +0 -113
  81. package/skills/managing-suite-assignments/SKILL.md +0 -161
  82. package/skills/polling-test-results/SKILL.md +0 -72
  83. package/skills/running-devops-test-suite/SKILL.md +0 -144
  84. package/skills/syncing-test-providers/SKILL.md +0 -108
@@ -0,0 +1,172 @@
1
+ # Caching & the reactive query result
2
+
3
+ Deep reference for the WebApp resource cache and the two refresh tools. The
4
+ [SKILL.md](../SKILL.md#freshness--caching) Freshness & caching section is the summary; this is
5
+ the full behavior.
6
+
7
+ > **Surface caveat up front:** everything about *caching* below is the **WebApp surface**. On
8
+ > uncached surfaces (Mosaic, OpenAI) there is no cache — see
9
+ > [Uncached surfaces](#uncached-surfaces-mosaic-openai) at the end. The `query`/`mutate`
10
+ > namespace shape is identical on every surface, so call sites stay portable.
11
+
12
+ ---
13
+
14
+ ## 1. Caching is ON by default (WebApp) — do NOT reinvent it
15
+
16
+ Every `sdk.graphql!.query()` on WebApp is cached automatically. There is **no opt-in flag, no
17
+ `createCachedClient` factory, and no `/cache` import subpath** — if you have heard of those,
18
+ they do not exist in `@salesforce/platform-sdk`. There is also **no need for React Query, SWR,
19
+ `localStorage`, or any hand-rolled memoization**. If you find yourself writing a cache, **stop**
20
+ — it already exists.
21
+
22
+ Default policy: **`max-age` with a 300-second TTL** (`DEFAULT_MAX_AGE_SECONDS = 300`).
23
+
24
+ | Situation | Behavior |
25
+ |-----------|----------|
26
+ | Cache hit (entry < 300s old) | Return cached `data` immediately — **no network call** |
27
+ | Stale (entry > 300s old) | Treated as a miss → fetch from network → write back (300s TTL) |
28
+ | Miss (no entry) | Fetch → write to cache (300s TTL) → return |
29
+
30
+ `mutate()` is **never** cached — it is a pass-through to the network.
31
+
32
+ ### What gets cached
33
+
34
+ Only **successful responses with a non-empty `data` object** are written:
35
+
36
+ - `data` is `null`, missing, or `{}` → **NOT cached** (an empty `data` usually means a
37
+ transient server condition; caching it would poison the cache for the full TTL).
38
+ - Response carries a non-empty `errors` array → surfaced as an error and **NOT cached**.
39
+
40
+ ### Cache key
41
+
42
+ The key is `stableJSONStringify({ query, variables, operationName })`.
43
+
44
+ - **`cacheControl` does NOT affect the key.** The same query + variables share **one** cache
45
+ entry no matter what policy each call passes. A `"no-cache"` call and a default call
46
+ read/write the *same* slot.
47
+ - The query is keyed by its **raw string** — two semantically identical queries with different
48
+ whitespace produce **different** entries. Reuse the same `gql`-tagged constant; do not
49
+ re-template the same query inline per call.
50
+ - Variables are deep-cloned at call time, so mutating your variables object afterward does not
51
+ desync the key from the request body. Variables must be JSON-serializable (circular refs /
52
+ BigInt throw a typed error).
53
+
54
+ ### Shared across SDK instances by `baseUrl`
55
+
56
+ Cache bundles are deduped in a module-level registry keyed by the resolved `baseUrl`. **A query
57
+ run through one `createDataSDK()` instance is a cache hit on another instance targeting the same
58
+ host.** Independently-built features that each create their own SDK do not issue redundant
59
+ network requests for the same data.
60
+
61
+ The per-instance fetch pipeline (CSRF, `onStatus`) stays **isolated** — a cache *miss* routes
62
+ through the calling SDK's own fetch, so per-instance request behavior is preserved.
63
+
64
+ **Practical implication:** you do **not** need to hoist `createDataSDK()` into a singleton purely
65
+ to share cache. Calling it per-feature is fine; the cache is shared by host underneath. (A
66
+ singleton is still reasonable for other reasons.)
67
+
68
+ ---
69
+
70
+ ## 2. The two refresh tools (keep them distinct)
71
+
72
+ There are **two unrelated mechanisms** for getting fresh data. They have different shapes and
73
+ different mental models. Do not conflate `result.refresh()` (a method on a live handle) with
74
+ `cacheControl: "no-cache"` (a per-call option).
75
+
76
+ | | **Reactive refresh** | **Call-site cache control** |
77
+ |---|---|---|
78
+ | **API** | `result.subscribe(cb)` + `result.refresh()` | `cacheControl` on the query options bag |
79
+ | **Lifetime** | Long-lived handle; subscription persists until you unsubscribe | One-shot, fire-and-forget per call |
80
+ | **Pushes updates?** | Yes — `subscribe` fires on every subsequent snapshot | No — you read the returned value once |
81
+ | **PR** | #502 | #537 (W-22514759) |
82
+ | **Use when** | A mounted component should react to cache updates or re-fetch on demand | "This specific read must bypass / only-use / re-TTL the cache" |
83
+
84
+ ### 2a. Reactive refresh — `subscribe` + `refresh`
85
+
86
+ `query()` resolves a **`QueryResult<T>`** — a snapshot (`data`/`errors`) plus `subscribe(cb)`
87
+ and `refresh()`. The contract (type shape, the independent-subscription and fire-on-subsequent
88
+ semantics) lives in [sdk-api.md](sdk-api.md#queryresultt--the-reactive-query-handle); this
89
+ section is about *when* and *how* to use it.
90
+
91
+ The lifecycle is: read the initial snapshot, register a subscriber for later snapshots, and
92
+ **always unsubscribe when the consumer goes away** (component unmount, effect re-run, view
93
+ teardown) so the subscription doesn't leak:
94
+
95
+ ```typescript
96
+ const result = await sdk.graphql!.query<GetAccountsQuery>({ query: GET_ACCOUNTS, variables });
97
+ render(result.data, result.errors); // initial snapshot
98
+
99
+ const unsub = result.subscribe(({ data, errors }) => render(data, errors)); // live updates
100
+ await result.refresh(); // force re-fetch → pushes to subscribers
101
+ // later, when the consumer tears down:
102
+ unsub();
103
+ ```
104
+
105
+ **Managing the subscription in a framework.** Whatever reactive/lifecycle primitive your UI
106
+ layer uses — a React effect, a Vue/Svelte lifecycle hook, a web-component
107
+ `connected`/`disconnectedCallback`, a store teardown — the same three obligations hold:
108
+
109
+ - Kick off `query()` on mount/setup and store the resolved `result` so you can call
110
+ `refresh()` on it later (e.g. behind a "Refresh" button).
111
+ - Push each `subscribe` snapshot into your reactive state so the view re-renders.
112
+ - Run `unsub()` in the teardown path, and guard against a late-resolving `query()` writing state
113
+ after teardown (track a `cancelled` flag). The subscription does not fire on registration, so
114
+ set initial state from the awaited snapshot, not from the subscriber.
115
+
116
+ **Refresh after a mutation** — mutations have no `subscribe`/`refresh`. To make a list reflect a
117
+ write, hold the query `result` and call `result.refresh()` after `mutate()` resolves:
118
+
119
+ ```typescript
120
+ await sdk.graphql!.mutate({ mutation: CREATE_ACCOUNT, variables: { input } });
121
+ await accountsResult.refresh(); // re-fetches, bypasses cache, pushes to subscribers
122
+ ```
123
+
124
+ ### 2b. Call-site cache control — the `cacheControl` option
125
+
126
+ `cacheControl` is a one-shot policy override on the query options bag. The type and the precise
127
+ per-value behavior (including how an `only-if-cached` miss surfaces as `DataNotFoundError`) are
128
+ the SDK contract — see
129
+ [sdk-api.md](sdk-api.md#cachecontrol--the-per-call-cache-policy). In short:
130
+
131
+ - `"no-cache"` — skip the cache read, always hit the network, still write back.
132
+ - `"only-if-cached"` — cache-only; a miss surfaces a `DataNotFoundError` on `result.errors` (no
133
+ network, no throw). Handle it by rendering an empty state — do **not** fall back to the network.
134
+ - `{ type: "max-age", maxAge: <seconds> }` — custom TTL instead of 300s.
135
+
136
+ It does **not** affect the cache key — the same query+variables share one slot regardless of the
137
+ policy each call passes. Which policy to reach for is the strategy table below.
138
+
139
+ ---
140
+
141
+ ## 3. Choosing a strategy
142
+
143
+ | Goal | Reach for |
144
+ |------|-----------|
145
+ | Default reads, freshness within ~5 min is fine | Nothing — default 300s cache |
146
+ | Component stays mounted, should reflect cache updates / on-demand re-fetch | `subscribe` + `refresh` (2a) |
147
+ | Re-fetch a held query after a mutation | `result.refresh()` (2a) |
148
+ | One-off "this read must be fresh" (button, post-mutation one-shot) | `cacheControl: "no-cache"` (2b) |
149
+ | Offline-first — render only cached data, tolerate a miss as an empty state (no network fallback) | `cacheControl: "only-if-cached"` (2b) |
150
+ | Data changes faster than 5 min | `cacheControl: { type: "max-age", maxAge: N }` (2b) |
151
+
152
+ `no-cache` (2b) and `refresh()` (2a) both bypass the cache and write back; the difference is
153
+ **`refresh()` pushes to existing subscribers** and is a method on a live handle, while
154
+ `no-cache` is a fresh one-shot call with no subscribers.
155
+
156
+ ---
157
+
158
+ ## Uncached surfaces (Mosaic, OpenAI)
159
+
160
+ - **No cache exists.** Every `query()` is a network request.
161
+ - **`cacheControl` is silently ignored** — `"no-cache"`, `"only-if-cached"`, and `max-age` have
162
+ no effect (notably, `only-if-cached` will **not** raise `DataNotFoundError` because there is no
163
+ cache layer to miss).
164
+ - **`subscribe` is real but only emits in response to `refresh()`** — there is no background
165
+ cache to push updates, so `refresh()` is the sole source of new snapshots, and each
166
+ `refresh()` costs one network request.
167
+ - PR #502 flags the uncached `subscribe`/`refresh` edge semantics (re-emit-to-all, error
168
+ fan-out, ordering vs concurrent refresh) as a **known soft spot / follow-up**. Document
169
+ conservatively; do not over-promise behavior there.
170
+
171
+ The namespace shape (`query`/`mutate`, options bag, `QueryResult`) is identical across surfaces,
172
+ so the same call site runs on WebApp and uncached surfaces alike.
@@ -0,0 +1,373 @@
1
+ # Building queries with the graphiti CLI
2
+
3
+ The base UI-bundle template ships **`@salesforce/graphiti`** as a devDependency, exposing a
4
+ `graphiti` CLI. Its `sf-gql-*` subcommands turn a small JSON spec into a **schema-correct
5
+ GraphQL document** — the query string, its typed variables, and a TypeScript shape — with
6
+ every platform guardrail (`@optional`, `value`/`displayValue` wrappers, `edges/node`,
7
+ pagination, the mutation `Record` envelope) already applied. This is the **preferred way to
8
+ author the GraphQL in [Read workflow](../SKILL.md#read-workflow) /
9
+ [Write workflow](../SKILL.md#write-workflow) step 2** — you get a query that's already
10
+ grounded against the org's live schema instead of hand-writing one and discovering field
11
+ errors at runtime.
12
+
13
+ > **The CLI is a query *compiler*, not a data fetcher.** Every `sf-gql-*` command returns a
14
+ > `{ query, variables, types, warnings }` envelope. It **never calls Salesforce for records
15
+ > and never returns rows** — execution still happens at runtime through
16
+ > `sdk.graphql!.query()` / `.mutate()` exactly as the Read/Write workflows describe. Think of
17
+ > it as "codegen for the query string itself": dev-time you compile the operation, persist it,
18
+ > generate types; runtime the SDK runs it.
19
+
20
+ > **Fallback only when the CLI genuinely *can't run*.** Fall back to the schema-grep path
21
+ > (`bash <skill-dir>/scripts/graphql-search.sh <Entity>`) and hand-author per
22
+ > [graphql-hand-authoring.md](graphql-hand-authoring.md) **only** when `@salesforce/graphiti` isn't
23
+ > installed or the org can't be primed (`SCHEMA_PRIME_FAILED`). The guardrails there are the same
24
+ > ones the CLI automates — you're just applying them by hand.
25
+ >
26
+ > **A primed CLI returning empty / "not found" / `Cannot query field` is NOT a fallback trigger.**
27
+ > That's a fact about the org — a wrong API name, or metadata that isn't deployed/refreshed — not a
28
+ > CLI failure. Re-`discover` (list before describe), `sf-gql-connect --forceRefresh` if you just
29
+ > deployed, or deploy the metadata; **do not switch to the script or hand-author around it**, and
30
+ > **never edit `schema.graphql`** to make the name resolve (it grants no org access — see
31
+ > [graphql-hand-authoring.md](graphql-hand-authoring.md)).
32
+
33
+ > **MCP, later.** These `sf-gql-*` commands mirror, one-for-one, the `sf_gql_*` tools of the
34
+ > graphiti MCP server (same args, same output, different transport). If graphiti is later
35
+ > approved as an MCP server, an agent calls the `sf_gql_*` tools directly and everything below
36
+ > about *shapes and behavior* still holds — only the invocation changes.
37
+
38
+ ---
39
+
40
+ ## How to invoke it
41
+
42
+ Run from the **UI bundle dir** (where `package.json` with the `@salesforce/graphiti` dep
43
+ lives). Each command takes one JSON argument (positional, or piped on stdin) and emits exactly
44
+ one JSON line on stdout:
45
+
46
+ ```bash
47
+ npx graphiti sf-gql-list '{"org":"myOrgAlias","object":"Account","fields":["Name","Industry"],"first":10}'
48
+
49
+ # stdin form (handy for large specs):
50
+ echo '{"org":"myOrgAlias","object":"Account","fields":["Name"]}' | npx graphiti sf-gql-list
51
+ ```
52
+
53
+ **Always single-quote the JSON argument.** It contains `"` and may contain `$varName` tokens
54
+ (see [Variables](#variables--parameterising-a-query)); an unquoted `$foo` is expanded by the
55
+ shell to an empty string before graphiti ever sees it.
56
+
57
+ `org` is an **org alias from the local Salesforce CLI auth** (`~/.sf` / `~/.sfdx`) — the same
58
+ aliases `sf org list` shows. It is **required on every command**.
59
+
60
+ Exit code is `0` on success, `1` on any error (the error is also in the JSON envelope, so you
61
+ can parse stdout rather than branch on the code).
62
+
63
+ ---
64
+
65
+ ## The three phases
66
+
67
+ ### 1. Prepare — make sure the org's schema is available
68
+
69
+ The build commands **auto-prime**: the first `sf-gql-*` call for an org downloads and caches
70
+ its schema, so you usually don't need a separate step. You only call `sf-gql-connect`
71
+ explicitly to **refresh after a deploy** (new objects/fields/picklist values won't appear until
72
+ you do):
73
+
74
+ ```bash
75
+ npx graphiti sf-gql-connect '{"org":"myOrgAlias","forceRefresh":true}'
76
+ # → {"org":"myOrgAlias","instanceUrl":"https://…","refreshed":true,"cached":false,"durationMs":…}
77
+ ```
78
+
79
+ If priming fails, the command returns a `SCHEMA_PRIME_FAILED` error envelope (see
80
+ [Errors](#error-envelope)) — the org is unreachable, unauthed, or hitting a server-side
81
+ introspection issue. You can't build verified queries against an org you can't prime; surface
82
+ that to the user rather than guessing field names.
83
+
84
+ ### 2. Discover — never guess an object or field
85
+
86
+ `sf-gql-discover` is how you ground intent against the org *before* building. Three modes:
87
+
88
+ ```bash
89
+ # What objects exist (optional substring filter)?
90
+ npx graphiti sf-gql-discover '{"org":"myOrgAlias","mode":"list_objects","search":"Account"}'
91
+ # → {"mode":"list_objects","objects":[{"name":"Account"}, …]}
92
+
93
+ # What fields does an object have (+ type, filterable, sortable, picklist values)?
94
+ npx graphiti sf-gql-discover '{"org":"myOrgAlias","mode":"describe_object","object":"Hero__c"}'
95
+ # → {"mode":"describe_object","object":{"name":"Hero__c","fields":[
96
+ # {"name":"Class__c","label":"Class","type":"PICKLIST","filterable":true,"sortable":true,
97
+ # "picklistValues":["Warrior","Mage","Rogue","Cleric"], …}, …]}}
98
+
99
+ # Drill into one field
100
+ npx graphiti sf-gql-discover '{"org":"myOrgAlias","mode":"describe_field","object":"Hero__c","field":"Class__c"}'
101
+ ```
102
+
103
+ `mode` is **required**; `object` is required for `describe_object`/`describe_field`; `field`
104
+ is required for `describe_field`. Use this output to pick exact API names, valid picklist
105
+ values, and which fields are filterable/sortable — the same facts that otherwise cause silent
106
+ runtime failures.
107
+
108
+ ### 3. Build — compile the operation
109
+
110
+ Pick the command for the task, pass the spec, read the `query` + `variables` + `types` out of
111
+ the envelope. The next section is the catalogue.
112
+
113
+ ---
114
+
115
+ ## The commands
116
+
117
+ | Command | Builds | Key spec fields |
118
+ |---|---|---|
119
+ | `sf-gql-list` | List query (`uiapi.query`) | `object`, `fields[]`, `first?`, `filter?`, `orderBy?`, `parentFields?`, `childRelationships?`, `scope?` |
120
+ | `sf-gql-detail` | Single-record-by-Id query | `object`, `fields[]`, `idVariable?` (default `id`) |
121
+ | `sf-gql-aggregate` | Aggregate query (`uiapi.aggregate`) | `object`, `groupBy?[]`, `aggregations?[]`, `filter?`, `first?` |
122
+ | `sf-gql-create` | Create mutation | `object`, `returnFields?` (default `["Id"]`), `inputVariable?` (default `input`) |
123
+ | `sf-gql-update` | Update mutation | `object`, `returnFields?`, `inputVariable?` |
124
+ | `sf-gql-delete` | Delete mutation | `object`, `inputVariable?` |
125
+ | `sf-gql-raw` | Arbitrary query from CLI-style `select`/`set`/`var` commands | `commands[]`, `operation?` (`query`\|`mutation`\|`aggregate`) |
126
+ | `sf-gql-discover` | Schema metadata (no GraphQL) | `mode`, `object?`, `field?`, `search?` |
127
+ | `sf-gql-connect` | Primes/refreshes the schema cache (no GraphQL) | `forceRefresh?` |
128
+
129
+ `operationName` (most commands) overrides the generated operation name — **set it to something
130
+ meaningful** (e.g. `"GetActiveHeroes"`) so the persisted `.graphql` file and the types codegen
131
+ generates off it read well. Defaults are derived (`<Object>List`, `<Object>Detail`, …).
132
+
133
+ ### Output envelope
134
+
135
+ Every build command emits the same four-key envelope:
136
+
137
+ ```jsonc
138
+ {
139
+ "query": "query …{ … }", // the GraphQL document — paste verbatim
140
+ "variables": [{ "name": "after", "type": "String", "required": false }],
141
+ "types": "export interface …", // TS shape of variables + result (a preview)
142
+ "warnings": [] // schema/semantic warnings — READ THESE
143
+ }
144
+ ```
145
+
146
+ - **`query`** — paste it verbatim into your `.graphql` file or inline `gql`. The guardrails are
147
+ baked in; don't "tidy" them out (that's the load-bearing part — see
148
+ [Primed vs degraded](#primed-vs-degraded--why-the-guardrails-sometimes-vanish)).
149
+ - **`variables`** — the GraphQL variables the operation declares, with types and nullability.
150
+ These map straight to the `variables` object you pass to `sdk.graphql!.query({ query,
151
+ variables })`.
152
+ - **`types`** — a TypeScript preview of the variables + result shape, with anonymized interface
153
+ names (`S37eaResult`). Useful to *see* the shape, but the canonical typed path is still the
154
+ bundle's `npm run graphql:codegen` over your saved `.graphql` (it produces **named** types
155
+ you import) — see [Wiring into runtime](#wiring-the-output-into-runtime).
156
+ - **`warnings`** — non-fatal, but **always read them**. An empty `[]` means the operation
157
+ validated cleanly against the live schema. A non-empty entry usually means a field/object
158
+ isn't in the primed schema or a selection is semantically off (examples below).
159
+
160
+ ### Ground-truth examples
161
+
162
+ These are real outputs captured from the CLI (query strings pretty-printed for readability;
163
+ the CLI emits them as a single JSON line).
164
+
165
+ **`sf-gql-list`** — `{"org":"…","object":"Hero__c","fields":["Name","Level__c","Class__c"],"first":5,"orderBy":{"Level__c":{"order":"DESC"}}}`
166
+
167
+ ```graphql
168
+ query Hero__cList($after: String) {
169
+ uiapi {
170
+ query {
171
+ Hero__c(first: 5, after: $after, orderBy: { Level__c: { order: DESC } }) {
172
+ edges {
173
+ node {
174
+ Name @optional { value displayValue }
175
+ Level__c @optional { value displayValue }
176
+ Class__c @optional { value displayValue }
177
+ }
178
+ }
179
+ pageInfo { hasNextPage endCursor }
180
+ }
181
+ }
182
+ }
183
+ }
184
+ ```
185
+ `variables: [{ "name": "after", "type": "String", "required": false }]` — note the CLI adds
186
+ forward-pagination (`$after` + `pageInfo`) for you. `orderBy` is a **singleton object**
187
+ (`{Field:{order:DESC}}`), not an array or `{field,direction}`.
188
+
189
+ **`sf-gql-detail`** — `{"org":"…","object":"Hero__c","fields":["Name","Level__c"]}`
190
+
191
+ ```graphql
192
+ query Hero__cDetail($id: ID!) {
193
+ uiapi { query { Hero__c(where: { Id: { eq: $id } }, first: 1) {
194
+ edges { node {
195
+ Name @optional { value displayValue }
196
+ Level__c @optional { value displayValue }
197
+ } }
198
+ } } }
199
+ }
200
+ ```
201
+ Injects `$id: ID!` and the `where: { Id: { eq: $id } }, first: 1` binding. Pass
202
+ `idVariable` to rename `$id`.
203
+
204
+ **`sf-gql-aggregate`** — `{"org":"…","object":"Hero__c","groupBy":["Class__c"],"aggregations":[{"function":"count","field":"Id","alias":"total"},{"function":"avg","field":"Level__c","alias":"avgLevel"}]}`
205
+
206
+ ```graphql
207
+ query Hero__cAggregate($after: String) {
208
+ uiapi { aggregate { Hero__c(groupBy: { Class__c: { group: true } }, after: $after) {
209
+ edges { node { aggregate {
210
+ Class__c { value }
211
+ total: Id { count { value } }
212
+ avgLevel: Level__c { avg { value } }
213
+ } } }
214
+ pageInfo { hasNextPage endCursor }
215
+ } } }
216
+ }
217
+ ```
218
+ `groupBy` is `["Field"]` (or `[{field,function}]` for date bucketing); aggregations carry a
219
+ `function` (`count`/`countDistinct`/`sum`/`avg`/`min`/`max`), an optional `field` (defaults to
220
+ `Id` for counts), and an `alias`. *Observed quirk:* this exact spec returns
221
+ `warnings: ["Validation: Field \"avg\" must not have a selection since type \"Double\" has no
222
+ subfields."]` — `avg` over a Double should be selected bare, not `avg { value }`. Treat such a
223
+ warning as a prompt to adjust the selection, not as a failed build.
224
+
225
+ **`sf-gql-create`** — `{"org":"…","object":"Hero__c","returnFields":["Id","Name"]}`
226
+
227
+ ```graphql
228
+ mutation CreateHero__c($input: Hero__cCreateInput!) {
229
+ uiapi { Hero__cCreate(input: $input) { Record {
230
+ Id
231
+ Name @optional { value displayValue }
232
+ } } }
233
+ }
234
+ ```
235
+ The `types` field describes the input you must supply at runtime — note the **entity-keyed
236
+ wrapper**, the single most common mutation-variables mistake:
237
+ ```typescript
238
+ interface Hero__cCreateInput { Hero__c: Hero__cCreateRepresentation; }
239
+ interface Hero__cCreateRepresentation { Name?: string; Class__c?: string; Level__c?: number; … }
240
+ // → variables: { input: { Hero__c: { Name: "Aria", Level__c: 5 } } }
241
+ ```
242
+ `sf-gql-update` is identical but the input type adds a sibling `Id: string`
243
+ (`{ input: { Hero__c: {…}, Id: "a0X…" } }`). `sf-gql-delete` uses the generic
244
+ `RecordDeleteInput { Id: string }` (`{ input: { Id: "a0X…" } }`) and selects only `Id` back —
245
+ there is no per-entity delete type. **Mutation inputs are raw values — never `{value}`-wrapped**
246
+ (that wrapper is a *read*-shape thing; mirroring it into a write is the classic failure).
247
+
248
+ > **One mutation guardrail the builder does NOT add: `allOrNone`.** The emitted document wraps the
249
+ > operation in a bare `uiapi { … }`, *not* `uiapi(input: { allOrNone: … })`. Before you ship, add
250
+ > the `allOrNone` wrapper yourself and set it explicitly — `uiapi(input: { allOrNone: true }) {
251
+ > Hero__cCreate(input: $input) { Record { … } } }` — per guardrail 4 in the spine and the
252
+ > [hand-authored templates](graphql-hand-authoring.md#mutations). graphiti gives you the `Record`
253
+ > output envelope and the entity-keyed input shape; the transaction policy is still on you.
254
+
255
+ > **You do not pass record values to the builder.** A create/update spec takes **only**
256
+ > `object`, `returnFields?`, `inputVariable?`, and `operationName?` — there is **no `fields`
257
+ > key** carrying the values to write. The builder emits the mutation *shape* (with the input
258
+ > declared as a `$variable`); the actual values become the **runtime variable** you pass to
259
+ > `sdk.graphql!.mutate({ mutation, variables })`. `returnFields` only controls what you read
260
+ > *back* after the write.
261
+
262
+ **`sf-gql-raw`** — for shapes the declarative commands don't cover, drive `select`/`set`/`var`:
263
+ ```bash
264
+ npx graphiti sf-gql-raw '{"org":"…","commands":["select uiapi/query/Hero__c/edges/node/Name/value","set uiapi/query/Hero__c first=5"]}'
265
+ ```
266
+ Same `@optional`/wrapper guardrails are still applied automatically.
267
+
268
+ ---
269
+
270
+ ## Primed vs degraded — why the guardrails sometimes vanish
271
+
272
+ This is the one behavior that surprises people, and it changes how much you can trust a single
273
+ build. The guardrail automation (`@optional`, `value`/`displayValue`, `edges/node`, the typed
274
+ result shape) is **conditional on the object being in the primed schema**. When it is, you get
275
+ the clean output above. When it **isn't** — wrong API name, not deployed, or schema not yet
276
+ refreshed after a deploy — the CLI still emits *a* query, but a **degraded** one, and flags it:
277
+
278
+ `{"org":"…","object":"Account","fields":["Name","Industry"],"first":3}` against an org whose
279
+ cache doesn't contain `Account`:
280
+
281
+ ```graphql
282
+ query AccountList($after: String) {
283
+ uiapi { query { Account(first: 3, after: $after) {
284
+ edges { node { Name Industry } } # ← bare fields: no @optional, no value/displayValue
285
+ pageInfo { hasNextPage endCursor }
286
+ } } }
287
+ }
288
+ ```
289
+ …with `types` collapsing the result to `Account: unknown` and:
290
+ ```json
291
+ "warnings": ["Validation: Cannot query field \"Account\" on type \"RecordQuery\"."]
292
+ ```
293
+
294
+ **So: a non-empty `warnings` array — especially `Cannot query field …` — means the build is
295
+ NOT trustworthy. Do not ship a degraded query.** Re-`discover` the correct API name, or
296
+ `sf-gql-connect` with `forceRefresh: true` if you just deployed, then rebuild until
297
+ `warnings` is `[]`. A clean build is your signal that the guardrails actually fired.
298
+ **Never edit `schema.graphql` to make a degraded build pass** — the mirror is a generated,
299
+ read-only reflection of the org; adding the missing field/type there silences the warning but
300
+ grants no org access, so the operation still fails at runtime. Fix the name or deploy + refresh.
301
+
302
+ ---
303
+
304
+ ## Variables — parameterising a query
305
+
306
+ A `$varName` string **leaf inside `filter`** is promoted to a typed, nullable GraphQL variable,
307
+ with the type inferred from the schema:
308
+
309
+ `{"org":"…","object":"Hero__c","fields":["Name"],"filter":{"Class__c":{"eq":"$heroClass"}},"first":5}`
310
+ ```graphql
311
+ query Hero__cList($after: String, $heroClass: Picklist) {
312
+ uiapi { query { Hero__c(first: 5, after: $after, where: { Class__c: { eq: $heroClass } }) { … } } }
313
+ }
314
+ # variables: [ {after, String, false}, {heroClass, Picklist, false} ]
315
+ ```
316
+
317
+ What does **not** work on the declarative tools: passing a whole-arg `$var` for `filter` or
318
+ `orderBy` (e.g. `"filter":"$where"`). Those args are typed as objects and reject a bare string
319
+ with an `INVALID_ARGS` error — promote at the **leaf**, not the whole argument. `first` is a
320
+ number and likewise can't take a `$var` string.
321
+
322
+ ---
323
+
324
+ ## Wiring the output into runtime
325
+
326
+ The CLI produces the operation; the rest is the existing
327
+ [Read workflow](../SKILL.md#read-workflow) / [Write workflow](../SKILL.md#write-workflow),
328
+ unchanged:
329
+
330
+ 1. **Persist** the `query` string — inline `gql` for simple ops, or a `.graphql` file
331
+ (one operation per file, imported with `?raw`) for complex ones. Use a meaningful
332
+ `operationName` so the file/type names read well.
333
+ 2. **Codegen** — `npm run graphql:codegen` (from the UI bundle dir) generates **named** types
334
+ into `src/api/graphql-operations-types.ts`. (The CLI's `types` field is a preview of the
335
+ same shape; codegen is the canonical import source.)
336
+ 3. **Call** — `sdk.graphql!.query({ query, variables })` for reads,
337
+ `sdk.graphql!.mutate({ mutation, variables })` for writes, using the codegen'd types and the
338
+ `variables` the CLI listed. Surface decision (`!` vs guard), error handling, and caching are
339
+ exactly as the SKILL spine and [sdk-api.md](sdk-api.md) describe.
340
+
341
+ The CLI never executes anything — it has no part in step 3. It just makes step 1 produce a
342
+ query you can trust.
343
+
344
+ ---
345
+
346
+ ## Error envelope
347
+
348
+ Failures come back as a single JSON line and set exit code `1`:
349
+
350
+ ```json
351
+ {"error":{"code":"INVALID_ARGS","message":"Input failed schema validation.","details":[{"path":["mode"],"message":"Required"}]}}
352
+ ```
353
+
354
+ | `code` | Means | Do |
355
+ |---|---|---|
356
+ | `INVALID_ARGS` | The spec failed Zod validation (missing/mis-typed field). `details[]` gives the JSON path. | Fix the spec per `details` — e.g. add the required `mode`, or stop passing a `$var` where an object is expected. |
357
+ | `AUTH_FAILED` | Org alias not authed / not found in local CLI auth. | Check `sf org list`; have the user authenticate the alias. |
358
+ | `SCHEMA_PRIME_FAILED` | Schema couldn't be downloaded/introspected (unreachable, or a server-side introspection error). | Retry; if it persists the org has an introspection problem — fall back to `graphql-search.sh` + hand-authoring, and tell the user. |
359
+ | `INTERNAL` | Anything else; verbatim message preserved. | Read the message. Set `GRAPHITI_DEBUG=1` for a stack trace. |
360
+
361
+ ---
362
+
363
+ ## Common pitfalls (failure-first)
364
+
365
+ | Symptom | Cause | Fix |
366
+ |---|---|---|
367
+ | Built query has bare fields, no `@optional`/wrappers; `types` shows `unknown` | Object not in the primed schema (typo, not deployed, stale cache) | Check `warnings` for `Cannot query field`; re-`discover` the name or `connect --forceRefresh`, rebuild until `warnings: []` |
368
+ | `$heroClass` came through as an empty string | Shell expanded `$…` in an unquoted arg | **Single-quote** the whole JSON argument |
369
+ | `INVALID_ARGS` on `"filter":"$where"` | Whole-arg `$var` not allowed on declarative tools | Promote at a leaf: `"filter":{"Field":{"eq":"$where"}}` |
370
+ | Mutation rejected at runtime / wrong shape | `{value}`-wrapped a mutation **input**, or dropped the entity-key wrapper | Inputs are raw values under the entity key: `{ input: { <Object>: { Field: v } } }`; read the `types` field |
371
+ | `Field "avg" must not have a selection…` warning | Selected `avg { value }` on a Double | Select the aggregate bare per the warning |
372
+ | New field/object missing from discover or build | Schema cached before the deploy | `sf-gql-connect` with `forceRefresh: true`, then rebuild |
373
+ | Used `graphiti new`/`cd`/`select`/`run` and it executed against the org | That's the **legacy** interactive/session flow, not the `sf-gql-*` mirror | Use the stateless `sf-gql-*` commands documented here — they compile, they don't execute |