@salesforce/afv-skills 1.45.0 → 1.47.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 (171) hide show
  1. package/package.json +1 -1
  2. package/skills/agentforce-observe/SKILL.md +32 -4
  3. package/skills/agentforce-observe/references/ahm-alerts.md +719 -0
  4. package/skills/automation-flow-generate/SKILL.md +11 -5
  5. package/skills/consumer-goods-promotion-bo-api-deploy/SKILL.md +275 -0
  6. package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/README.md +32 -0
  7. package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/SetCommentValue.cls +75 -0
  8. package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/SetCommentValue.cls-meta.xml +5 -0
  9. package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/interview-answers.json +13 -0
  10. package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/payloads/copy.json +10 -0
  11. package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/payloads/create.json +20 -0
  12. package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/payloads/update.json +16 -0
  13. package/skills/consumer-goods-promotion-bo-api-deploy/references/conventions-and-payload-rules.md +273 -0
  14. package/skills/consumer-goods-promotion-bo-api-deploy/references/generate-and-wire.md +236 -0
  15. package/skills/consumer-goods-promotion-bo-api-deploy/references/reference-example-set-comment-value.md +132 -0
  16. package/skills/consumer-goods-promotion-bo-api-deploy/references/smoke-and-verify.md +211 -0
  17. package/skills/dx-code-analyzer-configure/scripts/validate-config.sh +14 -10
  18. package/skills/dx-code-analyzer-run/scripts/apply-fixes.js +45 -4
  19. package/skills/dx-code-analyzer-run/scripts/describe-rule.js +52 -32
  20. package/skills/dx-devops-project-manage/SKILL.md +197 -0
  21. package/skills/dx-devops-project-manage/examples/common-workflows.md +197 -0
  22. package/skills/dx-devops-project-manage/references/cli-commands.md +295 -0
  23. package/skills/dx-devops-project-manage/scripts/create-project.sh +48 -0
  24. package/skills/dx-devops-project-manage/scripts/list-projects.sh +51 -0
  25. package/skills/dx-devops-project-manage/scripts/update-project.sh +96 -0
  26. package/skills/education-cloud-academic-calendar-generate/SKILL.md +225 -0
  27. package/skills/education-cloud-academic-calendar-generate/examples/quarter-calendar.json +47 -0
  28. package/skills/education-cloud-academic-calendar-generate/examples/sample-output.md +57 -0
  29. package/skills/education-cloud-academic-calendar-generate/examples/semester-calendar.json +54 -0
  30. package/skills/education-cloud-academic-calendar-generate/references/calendar-systems.md +127 -0
  31. package/skills/education-cloud-academic-calendar-generate/references/date-validation.md +222 -0
  32. package/skills/education-cloud-academic-calendar-generate/references/foundation_prerequisites.md +40 -0
  33. package/skills/education-cloud-academic-calendar-generate/scripts/validate_calendar_dates.py +143 -0
  34. package/skills/education-cloud-course-catalog-migrate/SKILL.md +321 -0
  35. package/skills/education-cloud-course-catalog-migrate/references/gotchas-detail.md +16 -0
  36. package/skills/education-cloud-course-catalog-migrate/references/gotchas.md +16 -0
  37. package/skills/education-cloud-course-catalog-migrate/references/large-catalog-handling.md +42 -0
  38. package/skills/education-cloud-course-catalog-migrate/scripts/batch_courses.py +36 -0
  39. package/skills/education-cloud-course-catalog-migrate/scripts/detect_linked_courses.py +51 -0
  40. package/skills/education-cloud-course-catalog-migrate/scripts/detect_modality_variants.py +48 -0
  41. package/skills/education-cloud-course-catalog-migrate/scripts/resolve_api_version.py +43 -0
  42. package/skills/education-cloud-course-catalog-migrate/scripts/split_course_code.py +39 -0
  43. package/skills/education-cloud-course-catalog-migrate/scripts/validate_completeness.py +54 -0
  44. package/skills/education-cloud-multi-campus-configure/references/foundation_prerequisites.md +3 -5
  45. package/skills/education-cloud-student-recruitment-agent-configure/SKILL.md +177 -0
  46. package/skills/education-cloud-student-recruitment-agent-configure/references/agent-and-subagents.md +151 -0
  47. package/skills/education-cloud-student-recruitment-agent-configure/references/customer-narration.md +34 -0
  48. package/skills/education-cloud-student-recruitment-agent-configure/references/execution-model.md +54 -0
  49. package/skills/education-cloud-student-recruitment-agent-configure/references/flows.md +82 -0
  50. package/skills/education-cloud-student-recruitment-agent-configure/references/grounding.md +199 -0
  51. package/skills/education-cloud-student-recruitment-agent-configure/references/permissions.md +183 -0
  52. package/skills/education-cloud-student-recruitment-agent-configure/references/platform-enablement.md +82 -0
  53. package/skills/education-cloud-student-recruitment-agent-configure/references/prerequisites.md +158 -0
  54. package/skills/education-cloud-student-recruitment-agent-configure/references/routing.md +141 -0
  55. package/skills/experience-cms-brand-apply/SKILL.md +5 -5
  56. package/skills/experience-cms-brand-create/SKILL.md +2 -2
  57. package/skills/experience-cms-content-generate/SKILL.md +1 -0
  58. package/skills/experience-cms-content-render/SKILL.md +173 -0
  59. package/skills/experience-cms-content-render/assets/angular/DetailPage.component.ts +25 -0
  60. package/skills/experience-cms-content-render/assets/angular/MediaRenderer.component.ts +133 -0
  61. package/skills/experience-cms-content-render/assets/angular/TypeList.component.ts +38 -0
  62. package/skills/experience-cms-content-render/assets/angular/TypeRenderer.component.ts +90 -0
  63. package/skills/experience-cms-content-render/assets/angular/cms-content.component.ts +248 -0
  64. package/skills/experience-cms-content-render/assets/angular/cms-item.service.ts +100 -0
  65. package/skills/experience-cms-content-render/assets/react/DetailPage.tsx +20 -0
  66. package/skills/experience-cms-content-render/assets/react/MediaRenderer.tsx +129 -0
  67. package/skills/experience-cms-content-render/assets/react/TypeList.tsx +40 -0
  68. package/skills/experience-cms-content-render/assets/react/TypeRenderer.tsx +64 -0
  69. package/skills/experience-cms-content-render/assets/react/heuristicRenderer.tsx +310 -0
  70. package/skills/experience-cms-content-render/assets/react/useCmsItem.ts +129 -0
  71. package/skills/experience-cms-content-render/assets/shared/cmsContentType.ts +49 -0
  72. package/skills/experience-cms-content-render/assets/shared/cmsCore.types.ts +96 -0
  73. package/skills/experience-cms-content-render/assets/shared/externalRefs.ts +55 -0
  74. package/skills/experience-cms-content-render/references/bulk-loading.md +60 -0
  75. package/skills/experience-cms-content-render/references/codegen-guardrails.md +111 -0
  76. package/skills/experience-cms-content-render/references/detail-pages.md +87 -0
  77. package/skills/experience-cms-content-render/references/embed-recipes.md +127 -0
  78. package/skills/experience-cms-content-render/references/failure-modes.md +96 -0
  79. package/skills/experience-cms-content-render/references/heuristic-render-rules.md +131 -0
  80. package/skills/experience-cms-content-render/references/init-scaffold.md +122 -0
  81. package/skills/experience-cms-content-render/references/interaction-model.md +173 -0
  82. package/skills/experience-cms-content-render/references/package-api.md +106 -0
  83. package/skills/experience-cms-content-render/references/schema-sync.md +114 -0
  84. package/skills/experience-cms-content-render/references/styling-scopes.md +65 -0
  85. package/skills/experience-cms-content-render/references/verify.md +49 -0
  86. package/skills/experience-cms-content-type-generate/SKILL.md +2 -2
  87. package/skills/experience-content-media-stock-image-search/SKILL.md +5 -4
  88. package/skills/experience-search-coordinate/SKILL.md +198 -0
  89. package/skills/experience-search-coordinate/assets/search-payload-template.json +25 -0
  90. package/skills/experience-search-coordinate/references/content-route.md +313 -0
  91. package/skills/experience-search-coordinate/references/content-type-discovery.md +57 -0
  92. package/skills/experience-search-coordinate/references/media-route.md +172 -0
  93. package/skills/experience-search-coordinate/references/scope-resolution.md +14 -0
  94. package/skills/experience-ui-bundle-localize/SKILL.md +1 -1
  95. package/skills/experience-ui-bundle-localize/references/i18n-setup.md +5 -3
  96. package/skills/experience-ui-bundle-project-generate/SKILL.md +18 -14
  97. package/skills/experience-ui-bundle-project-generate/references/angular-project-generate.md +22 -0
  98. package/skills/experience-ui-bundle-project-generate/references/react-project-generate.md +20 -0
  99. package/skills/experience-ui-bundle-salesforce-data-access/SKILL.md +58 -54
  100. package/skills/experience-ui-bundle-salesforce-data-access/references/caching.md +6 -0
  101. package/skills/experience-ui-bundle-salesforce-data-access/references/graphiti-cli.md +2 -2
  102. package/skills/experience-ui-bundle-salesforce-data-access/references/migration.md +6 -0
  103. package/skills/experience-ui-bundle-salesforce-data-access/references/rest-and-integration.md +2 -1
  104. package/skills/experience-ui-bundle-salesforce-data-access/references/sdk-api.md +6 -0
  105. package/skills/experience-ui-bundle-site-generate/SKILL.md +59 -8
  106. package/skills/experience-ui-bundle-site-generate/references/configure-metadata-digital-experience.md +8 -3
  107. package/skills/experience-ui-bundle-site-generate/references/configure-metadata-language-settings.md +120 -0
  108. package/skills/life-sciences-fieldsalesrep-coordinate/SKILL.md +336 -0
  109. package/skills/life-sciences-fieldsalesrep-coordinate/references/orchestration-flow.md +143 -0
  110. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-application-flexipage-mapping.md +127 -0
  111. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-deploy-commands.md +116 -0
  112. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-lifesci-metadata-deploy.md +111 -0
  113. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-overview.md +312 -0
  114. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-profile-layout-assignments.md +171 -0
  115. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-state-tracking.md +64 -0
  116. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-trigger-handlers.md +122 -0
  117. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-4-user-provisioning-overview.md +335 -0
  118. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-4-user-provisioning-user-provisioning-details.md +140 -0
  119. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-execution-state-and-recovery.md +196 -0
  120. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-metadata-cache-generation.md +155 -0
  121. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-overview.md +307 -0
  122. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-visit-creation-data.md +211 -0
  123. package/skills/life-sciences-fieldsalesrep-coordinate/references/state-machine-and-changes.md +108 -0
  124. package/skills/life-sciences-kam-coordinate/SKILL.md +241 -0
  125. package/skills/life-sciences-kam-coordinate/references/orchestration-flow.md +152 -0
  126. package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-application-flexipage-mapping.md +79 -0
  127. package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-deploy-commands.md +131 -0
  128. package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-kam-config-records.md +85 -0
  129. package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-lifesci-metadata-deploy.md +112 -0
  130. package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-overview.md +202 -0
  131. package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-profile-layout-assignments.md +67 -0
  132. package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-state-tracking.md +65 -0
  133. package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-trigger-handlers.md +123 -0
  134. package/skills/life-sciences-kam-coordinate/references/stage-4-participant-role-and-sprint.md +89 -0
  135. package/skills/life-sciences-kam-coordinate/references/stage-5-data-and-plan-templates-overview.md +337 -0
  136. package/skills/life-sciences-kam-coordinate/references/stage-5-data-creation-data.md +248 -0
  137. package/skills/life-sciences-kam-coordinate/references/stage-6-ipad-validation-script.md +35 -0
  138. package/skills/life-sciences-kam-coordinate/references/stage-6-metadata-cache-generation.md +155 -0
  139. package/skills/life-sciences-kam-coordinate/references/stage-6-user-provisioning-details.md +146 -0
  140. package/skills/life-sciences-kam-coordinate/references/stage-6-user-provisioning-overview.md +89 -0
  141. package/skills/life-sciences-kam-coordinate/references/state-machine-and-changes.md +114 -0
  142. package/skills/life-sciences-prerequisites-validate/SKILL.md +138 -0
  143. package/skills/life-sciences-prerequisites-validate/references/checks-org-settings.md +190 -0
  144. package/skills/life-sciences-prerequisites-validate/references/checks-user-and-package.md +211 -0
  145. package/skills/life-sciences-territory-configure/SKILL.md +217 -0
  146. package/skills/life-sciences-territory-configure/references/territory-metadata.md +262 -0
  147. package/skills/platform-apex-logs-debug/SKILL.md +7 -7
  148. package/skills/platform-custom-application-generate/SKILL.md +4 -4
  149. package/skills/platform-custom-object-generate/SKILL.md +7 -7
  150. package/skills/platform-custom-tab-generate/SKILL.md +1 -1
  151. package/skills/platform-dsar-policy-manage/SKILL.md +272 -0
  152. package/skills/platform-dsar-policy-manage/references/configure.md +106 -0
  153. package/skills/platform-dsar-policy-manage/references/export-and-history.md +123 -0
  154. package/skills/platform-dsar-policy-manage/references/gap-analysis-guide.md +150 -0
  155. package/skills/platform-dsar-policy-manage/references/gap-scan.md +129 -0
  156. package/skills/platform-dsar-policy-manage/references/headless-sor.md +59 -0
  157. package/skills/platform-dsar-policy-manage/references/report-format.md +59 -0
  158. package/skills/platform-dsar-policy-manage/scripts/tests/__init__.py +0 -0
  159. package/skills/platform-dsar-policy-manage/scripts/tests/test_validate_policy_tree.py +76 -0
  160. package/skills/platform-dsar-policy-manage/scripts/validate-policy-tree.py +130 -0
  161. package/skills/platform-flexipage-generate/SKILL.md +4 -0
  162. package/skills/platform-list-view-generate/SKILL.md +1 -0
  163. package/skills/platform-salesforce-connect-adapter-generate/SKILL.md +359 -0
  164. package/skills/platform-salesforce-connect-adapter-generate/references/official-examples.md +69 -0
  165. package/skills/platform-salesforce-connect-adapter-generate/references/scenarios.md +187 -0
  166. package/skills/platform-soql-query/SKILL.md +8 -8
  167. package/skills/platform-value-set-generate/SKILL.md +2 -2
  168. package/skills/service-itsm-agentic-setup-cmdb-coordinate/SKILL.md +20 -27
  169. package/skills/service-native-voice-recording-transcription-configure/SKILL.md +47 -27
  170. package/skills/service-native-voice-recording-transcription-configure/references/thunderbird-voice-settings.md +13 -9
  171. package/skills/service-native-voice-recording-transcription-configure/scripts/enable-recording-transcription.sh +104 -45
@@ -0,0 +1,57 @@
1
+ # Content Type Discovery — Fallback Chains
2
+
3
+ Reached from `SKILL.md` Step 2 point 2 whenever `search_metadata` doesn't produce a usable candidate list.
4
+
5
+ ## Workspace content-type fallback
6
+
7
+ Only reached when `search_metadata` never produced a usable candidate list — the server was unavailable, or it returned zero candidates even after one retry with a clarified query.
8
+
9
+ - Call `get_content_types_for_workspace` passing `baseType: "content"` **instead of** `spaceId` — omitting `spaceId` and passing `baseType` returns every applicable content type across the org rather than one workspace's subset. Also pass `typeClassFullyQualifiedNames: ["sfdc_cms__structured"]` and `mixinFullyQualifiedNames: ["sfdc_cms:deliverySearch"]` — this pre-filters the response to the same "supported for search" set the primary path gates on in Step 2 point 4, so the fallback doesn't have to hand back unsupported types for you to filter after the fact:
10
+
11
+ ```javascript
12
+ get_content_types_for_workspace({
13
+ inputs: [{
14
+ baseType: "content",
15
+ typeClassFullyQualifiedNames: ["sfdc_cms__structured"],
16
+ mixinFullyQualifiedNames: ["sfdc_cms:deliverySearch"]
17
+ }]
18
+ })
19
+ ```
20
+
21
+ - Each returned entry has `developerName`, `namespace`, and `description` — the type-class/mixin filtering above is applied server-side to the candidate set, but individual entries still don't carry `typeClasses`/`mixins` fields themselves, so **media-vs-content routing** is still judgment-based (reading the description), not a field check like `query_metadata`'s.
22
+ - **Read each candidate's `developerName`/`description` against the search intent** and pick at most the 5 most relevant. For each: if it's clearly an image, audio, video, or document type (by name/description) → media candidate, go to Step 4 (Media Route); otherwise → content candidate, go to Step 3 (Content Route). Construct the FQN as `<namespace>__<developerName>` (e.g. `c__BookReview`, `sfdc_cms__image`) — the same convention `experience-cms-content-generate` uses for FQN construction.
23
+ - **Can't confidently classify, or can't narrow to relevant candidates** — ask the user one clarifying question (`ask_followup_question`) listing the candidates (label + description) as options, and route on their reply. This is a separate, one-shot ask from Step 2 point 2's clarifying question (that one narrows the search query before this fallback ever runs; this one disambiguates among this fallback's own candidates) — don't skip it just because point 2 already asked once.
24
+ - **Zero candidates returned here too** — apply the wording fallback below; there's nothing left to ground the route in.
25
+
26
+ ## Wording fallback
27
+
28
+ Last resort — only reached when both `search_metadata` and the workspace content-type fallback failed to produce a usable candidate list.
29
+
30
+ - **Media wording** (e.g. image, photo, logo, icon, banner, graphic, picture, audio, clip, sound, recording, video, document, PDF, file, asset, media) — go to Step 4 (Media Route) with an empty `contentTypeFqn` (`""`).
31
+ - **Content wording** (e.g. article, blog, post, news, FAQ, event, product, page, press release) — go to Step 3 (Content Route) with an empty `contentTypeFqn` (`""`).
32
+ - **Still unclear either way** — fall back to the Content Route with an empty `contentTypeFqn` (`""`), since structured content is the more common case.
33
+ - In every one of these cases, search silently across all types for that route — do not block or ask the user again just because `search_metadata` and the workspace content-type fallback both came up empty.
34
+
35
+ ## Filter-result messages (Step 2 point 4)
36
+
37
+ **Zero candidates survive the supported-type filters:**
38
+
39
+ ```text
40
+ I found these content types for "<intent>", but none of them are supported for search: <label1> (<fqn1>), <label2> (<fqn2>).
41
+ ```
42
+
43
+ **Some, but not all, candidates survive** (plain-text statement, not a question — don't wait for a reply):
44
+
45
+ ```text
46
+ <label1> (<fqn1>) isn't supported for search, so I'll continue with: <label2> (<fqn2>).
47
+ ```
48
+
49
+ ## "Intent itself is ambiguous" question
50
+
51
+ Reached from `SKILL.md` Step 2 when the request gives no hint of type or subject (e.g. "find me some content") — asked before ever calling `search_metadata`.
52
+
53
+ `ask_followup_question` with the question `"What are you looking for?"` and one **option** per choice in the tool's `options` array — never as numbered lines inside the question text:
54
+
55
+ `Articles / Blog posts`, `News`, `FAQs`, `Events`, `Products`, `Images / logos / photos`, `Audio clips`, `Videos`, `Documents`, `Other (describe what you need)`.
56
+
57
+ Wait for the reply before proceeding. Do not guess.
@@ -0,0 +1,172 @@
1
+ # Media Route — Full Reference
2
+
3
+ Worked payload examples for the media route (Step 4 of SKILL.md).
4
+
5
+ **Media is not limited to images.** Step 2's `search_metadata` + `query_metadata` calls discover the actual FQN for the intent — it may be `sfdc_cms__image`, `sfdc_cms__audio`, `sfdc_cms__video`, `sfdc_cms__document`, or another `sfdc_cms__media`-typed FQN. Use whatever FQN was discovered; never default to `sfdc_cms__image`.
6
+
7
+ ## Presenting available search sources
8
+
9
+ Reached at Step 4 point 1. Check your own tool list (introspection, not a tool call) for `search_media_cms_channels` and `search_electronic_media`. Include only the sources whose tools you actually have, plus "Other" as the last option, each as its own **option** in `ask_followup_question`'s `options` array — never as numbered lines inside the question text:
10
+
11
+ ```text
12
+ Question: "Where would you like to search for that?"
13
+ Options:
14
+ - Search using keywords — Search Salesforce CMS by keywords and taxonomies
15
+ - Search using Data 360 hybrid search — Semantic search across Salesforce CMS and connected DAMs
16
+ - Other — Provide your own URL or path
17
+ ```
18
+
19
+ Wait for the user's selection. Do not call any tool before this point. **Other** — ask for a direct URL, asset library path, or specific system to check (no confirmation step needed, since no tool is called).
20
+
21
+ ## Executing the search call(s)
22
+
23
+ Per the scope from Step 1. `contentAccessScope` and `channelIds` are **mutually exclusive fields — never include both in the same call**: no `channelId` in scope → one call with `contentAccessScope: "Public"` and no `channelIds`; a `channelId` in scope → two calls — one with `contentAccessScope: "Public"` and no `channelIds`, one with `channelIds` set to `"<channelId>"` and no `contentAccessScope`. Each result item already carries its own list of channel deliveries in `managedContentChannelDeliveryDetails` — same shape, and same "can hold many entries for one item" reality, as the Content Route (see `content-route.md` "Executing the search call(s)" for a real six-entry API response). Each entry's `managedContentChannelDetails.type` is `PUBLIC_UNAUTHENTICATED`, `COMMUNITY`, or `WEB_APP` (the channel-scoped call's uiBundle channel). **If the same item (by its unique identifier) appears in both responses, merge their `managedContentChannelDeliveryDetails` arrays** rather than picking one response and discarding the other — de-duplicate only exact repeats of the same channel id. Presentation still shows the item once; the channel choice is made after selection — see "Disambiguating channel at selection" below. (Data 360 hybrid search ignores channel scope entirely — no `channelIds`/`contentAccessScope` field, no channel to label.)
24
+
25
+ **The Unauthenticated URL is public-channel-only** — see "Handing off the selected media item" below: only a `PUBLIC_UNAUTHENTICATED` or `COMMUNITY` entry's `contentUrl` is ever surfaced; a `WEB_APP` entry's `contentUrl` never is, even though the field is present in the response.
26
+
27
+ ## Search using keywords (`search_media_cms_channels`)
28
+
29
+ Keyword/taxonomy extraction rule: see `SKILL.md` Step 4 point 2. Extract taxonomy whenever the query contains a genuine descriptive/style/mood/category term — do not default to an empty taxonomy just because keywords were already found. Additional worked examples for media:
30
+ - "modern minimalist company logo" → keywords: logo, emblem, corporate logo; taxonomies: Modern, Minimalist, Clean
31
+ - "customer testimonial clips" → keywords: testimonial, customer, feedback, review; taxonomies: _(empty — no descriptive terms)_
32
+ - "bright spacious room" → keywords: _(empty — no concrete nouns)_; taxonomies: Bright, Spacious, Open, Airy, Light
33
+
34
+ **Process:**
35
+
36
+ 1. **Analyze the query** — understand what the user is searching for (subject, attributes, domain)
37
+ 2. **Determine locale** — format `en_US`, `es_MX`, `fr_FR` (default: `en_US`)
38
+ 3. **Build the JSON payload:**
39
+
40
+ ```json
41
+ {
42
+ "inputs": [{
43
+ "searchKeyword": "keyword1 OR keyword2 OR keyword3",
44
+ "taxonomyExpression": "{\"OR\": [\"Taxonomy1\", \"Taxonomy2\"]}",
45
+ "searchLanguage": "en_US",
46
+ "contentAccessScope": "Public",
47
+ "contentTypeFqn": "<fqn discovered in Step 2, e.g. sfdc_cms__image, sfdc_cms__audio, sfdc_cms__video, sfdc_cms__document>",
48
+ "pageOffset": 0,
49
+ "searchLimit": 5
50
+ }]
51
+ }
52
+ ```
53
+
54
+ **Field rules:**
55
+ - `searchKeyword`: join keywords with ` OR `. Empty string if no keywords.
56
+ - `taxonomyExpression`: stringified JSON `{"OR": ["term1", "term2"]}`. `"{}"` if no taxonomies.
57
+ - `contentAccessScope` / `channelIds`: **mutually exclusive — never send both in the same call.** The public-channels call sets `contentAccessScope: "Public"` and omits `channelIds` entirely. If Step 1 also found one or more `channelId`s, issue a **second** call with `channelIds` set to those values (comma-separated if more than one, e.g. `"0apXX0000001001,0apXX0000001002"`) and omit `contentAccessScope` entirely — merge/de-duplicate the two calls' results.
58
+ - `contentTypeFqn`: the FQN(s) discovered in Step 2 — never hardcode `sfdc_cms__image`.
59
+ - `pageOffset`: start at `0`, increment by `searchLimit` for pagination.
60
+ - `searchLimit`: default `5`, adjust if user requests more.
61
+
62
+ **Worked example — "luxury apartment with river view" (discovered FQN: `sfdc_cms__image`):**
63
+
64
+ ```json
65
+ {
66
+ "inputs": [{
67
+ "searchKeyword": "apartment OR villa OR penthouse OR residence",
68
+ "taxonomyExpression": "{\"OR\": [\"Luxury\", \"Premium\", \"Waterfront\", \"Riverside\"]}",
69
+ "searchLanguage": "en_US",
70
+ "contentAccessScope": "Public",
71
+ "contentTypeFqn": "sfdc_cms__image",
72
+ "pageOffset": 0,
73
+ "searchLimit": 5
74
+ }]
75
+ }
76
+ ```
77
+
78
+ **Worked example — "car images" (no descriptive terms, discovered FQN: `sfdc_cms__image`):**
79
+
80
+ ```json
81
+ {
82
+ "inputs": [{
83
+ "searchKeyword": "car OR automobile OR vehicle OR auto",
84
+ "taxonomyExpression": "{}",
85
+ "searchLanguage": "en_US",
86
+ "contentAccessScope": "Public",
87
+ "contentTypeFqn": "sfdc_cms__image",
88
+ "pageOffset": 0,
89
+ "searchLimit": 5
90
+ }]
91
+ }
92
+ ```
93
+
94
+ **Worked example — "customer testimonial audio clips" (discovered FQN: `sfdc_cms__audio`):**
95
+
96
+ ```json
97
+ {
98
+ "inputs": [{
99
+ "searchKeyword": "testimonial OR customer success OR product satisfaction OR feedback OR review",
100
+ "taxonomyExpression": "{}",
101
+ "searchLanguage": "en_US",
102
+ "contentAccessScope": "Public",
103
+ "contentTypeFqn": "sfdc_cms__audio",
104
+ "pageOffset": 0,
105
+ "searchLimit": 5
106
+ }]
107
+ }
108
+ ```
109
+
110
+ ## Search using Data 360 hybrid search (`search_electronic_media`)
111
+
112
+ 1. Use the user's query **as-is** — no keyword extraction or transformation needed
113
+ 2. Call `search_electronic_media(searchQuery="<user's query>")`
114
+
115
+ Example: user query "modern luxury apartment with natural lighting" → `search_electronic_media(searchQuery="modern luxury apartment with natural lighting")`
116
+
117
+ ## Confirming the query before searching
118
+
119
+ Regardless of method, confirm the built query with the user before calling the search tool — this is the same required two-message pattern used by the Content Route (see `SKILL.md` Step 3, point 2). Never put the parameter list inside `ask_followup_question`'s `question` text; it strips line breaks and collapses everything into one paragraph.
120
+
121
+ **Message 1 (plain chat text):**
122
+
123
+ ```text
124
+ I'll search for media with these parameters:
125
+
126
+ Content Type: Image (sfdc_cms__image)
127
+ Keywords: apartment OR villa OR penthouse OR residence
128
+ Taxonomies: Luxury, Premium, Waterfront, Riverside
129
+ Language: en_US
130
+ Channels: Public
131
+ ```
132
+
133
+ **Message 2 (immediately after):** `ask_followup_question` with options `Yes - search now` / `Edit search` / `Cancel`.
134
+
135
+ ## Presenting Search Results
136
+
137
+ 1. **Parse each call's response** — Extract all results (title and content type) and each item's full `managedContentChannelDeliveryDetails` array, merged across calls if the item appears in more than one response (per "Executing the search call(s)" above — nothing is dropped as a duplicate).
138
+ 2. **Use `ask_followup_question`** to present ALL results as selectable **unique** options (one option per item, by identifier), grouped by content type — same grouping key as the Content Route (see `content-route.md` Step 6), never by channel. Show the title, content type, and channel on each option — do not display the URL. If an item's `managedContentChannelDeliveryDetails` array has more than one entry, show the **first** entry's channel name here as a lightweight hint only; the actual choice between channels is made after selection (see "Disambiguating channel at selection" below), not here. If more than one content type FQN is in scope, order the options by content type (the tool has no native grouping — approximate it by ordering and, where the UI allows a label, prefixing the option text with the content type). Regardless of grouping, always include the channel as a per-item suffix, e.g. `Product Launch Hero Banner (sfdc_cms__image) — Public`.
139
+ 3. **Receive the user's selection** from the tool response
140
+
141
+ ## Disambiguating channel at selection
142
+
143
+ Reached at Step 4 point 5, only when the selected item's (merged) `managedContentChannelDeliveryDetails` array has more than one entry. Build one option per entry from `managedContentChannelDetails.name`, in the order the array returns them — not a fixed two-option choice. **Call `ask_followup_question` with these as its actual `question` and `options` parameters — never concatenate the channel list into the question string itself**, the same rule every other question in this skill follows:
144
+
145
+ ```text
146
+ question: "Which channel would you like to use for 'Modern Minimalist Logo'?"
147
+ options: ["Public", "landing-page"]
148
+ ```
149
+
150
+ Wait for the reply before displaying the item's details (below) — use the record from the **chosen** entry, never assume entry `[0]` is the right one. If the array has zero entries, skip this question and display with the Unauthenticated URL line omitted; if it has exactly one entry, skip the question and use that entry directly. See `content-route.md` "Disambiguating channel at selection" for a real six-channel example — the array can hold far more than two entries.
151
+
152
+ **A `WEB_APP` entry is still a valid, selectable option here** — it's excluded only from the Unauthenticated URL line at display time (see "Handing off the selected media item" below), not from the channel choice itself.
153
+
154
+ ## Handing off the selected media item
155
+
156
+ 1. **Confirm the selection**, displaying:
157
+ - **Content Name** — `title`
158
+ - **Content Type** — `contentType` (e.g. `sfdc_cms__image`, `sfdc_cms__audio`, `sfdc_cms__video`, `sfdc_cms__document`)
159
+ - **Channel** — the channel of the entry that was used (the item's only entry, or the one chosen in "Disambiguating channel at selection" above)
160
+ - **Unauthenticated URL** — shown **only when that entry's `managedContentChannelDetails.type` is `PUBLIC_UNAUTHENTICATED` or `COMMUNITY` AND it has a `contentUrl`** — the complete URL including all query parameters (CMS and DAM URLs rely on them for authentication, resizing, and CDN routing; dropping them breaks the media item). E.g. `https://cms.example.com/media/img.jpg?oid=00D&refid=0EM&v=2` must be used in full. **Never shown for a `WEB_APP` (uiBundle) channel entry, even when `contentUrl` is present** — that URL isn't meant for public consumption. Omit the line entirely whenever either condition fails (absent `contentUrl`, `WEB_APP` type, or no entries at all) — do not show it blank or with a placeholder.
161
+ 2. **Offer the render hand-off — always, no uiBundle gating** (unlike the Content Route's offer, which only appears when a uiBundle channel was in scope; see `content-route.md` "Offering the render hand-off"). Same required two-message pattern:
162
+
163
+ **Message 1 (plain text):**
164
+
165
+ ```text
166
+ I can render "<title>" (<content type label>) using the experience-cms-content-render skill.
167
+ ```
168
+
169
+ **Message 2 (immediately after):** `ask_followup_question` with options `Yes` / `No`. Never restate the item's details inside the question text — the question stays generic (`"Invoke the render skill for this item?"`).
170
+
171
+ 3. **On `Yes`** — dispatch via `mcp__skill_bridge__load_skill("experience-cms-content-render")` — never the built-in Skill tool (separate registry, returns `Unknown skill`). Pass the item's `title`, content type FQN, and the chosen entry's `managedContentChannelDetails.id` (channel ID) **only when the item has a delivery entry** — this route explicitly supports zero-entry items (see "Disambiguating channel at selection" above), and the render hand-off is always offered regardless, so a zero-entry item reaches this step with no entry to read `.id` from. **Omit the channel ID for it entirely — never a placeholder or `null`** — `experience-cms-content-render`'s own documented channel-resolution flow asks the user for the channel (or HALTs) when none is supplied. Also pass either its Unauthenticated URL (if present) or, if absent, its `managedContentKey` renamed to `contentKey` — `experience-cms-content-render`'s own `SKILL.md` names the field `contentKey`, not `managedContentKey`. If the load call fails, tell the user the hand-off failed and stop.
172
+ 4. **On `No`** — stop; the search is complete. Do not apply the URL yourself as a fallback.
@@ -0,0 +1,14 @@
1
+ # Channel Scope Resolution — Full Reference
2
+
3
+ ## Step 1 points 1-3 — discovering channel scope
4
+
5
+ 1. **Explicit `channelId` given** (e.g. "search within channel 0apXX0000001001", or passed as an input to this skill) — use it directly as the scoped channel and skip straight to the scope rule in `SKILL.md` Step 1. Do not run uiBundle discovery in this case.
6
+ 2. **Named uiBundle/app, no `channelId`** (e.g. "search in this uiBundle" or "search in the Homepage app") — locate that named uiBundle (e.g. `<sfdx-source>/uiBundles/<named-uiBundle>/public/content-metadata.json`) and read its `channelId` if present:
7
+ - **Found, with a `channelId`** — use it as the scoped channel, skip to the scope rule.
8
+ - **Not found, or found but no `channelId` present** — fall through to point 3 and scan every uiBundle as if the user hadn't named one.
9
+ 3. **Otherwise, discover from uiBundles by scanning the local project — never gate this on server-side deployment status:**
10
+ - **Scan the project directly** for every `uiBundles/*/public/content-metadata.json` file using whichever directory-listing/glob capability your environment exposes. Do NOT run `sf org list metadata --metadata-type UIBundle` first and use its result to gate which bundles you check — a uiBundle's CMS channel can exist even when the uiBundle itself is not (yet, or ever) deployed to the currently-targeted org, so a server-side deployed-bundle list would wrongly exclude it.
11
+ - **For each `content-metadata.json` found**, read it and collect the `channelId` if present — no server-side field or API links a UIBundle to a CMS channel, so this local file is the only source for `channelId`.
12
+ - **Zero uiBundles with a `channelId`** — scope is public channels only.
13
+ - **Exactly one** — scope is public channels **plus** that `channelId`.
14
+ - **More than one** — call `ask_followup_question` with the question `"Multiple components have their own CMS channel. Which one should I search within (in addition to public channels)?"` and one **option** per uiBundle in the tool's `options` array — never as numbered lines inside the question text. Scope is public channels plus the selected `channelId`.
@@ -190,7 +190,7 @@ Branch on the exit code: `0`, every key is registered (or there are no `t()` cal
190
190
 
191
191
  **Goal:** Ensure the i18next init exists; scaffold it if the app has no i18n yet.
192
192
 
193
- Configure `SalesforceBackend` by bundle type: preserve the shipped `BASE_VALUE` default for B2E; for B2C only, set `labelFallback: "USER_DEFAULT"`. See [references/i18n-setup.md](references/i18n-setup.md) for both configurations and B2C language context.
193
+ Configure `SalesforceBackend` by bundle type: preserve the shipped `BASE_VALUE` default for B2E; for B2C only, set `labelFallback: "USER_DEFAULT"` and pass the route-selected `SFDC_ENV.language`, with `ctx.lang` fallback, as `lng` to `i18next.init`. See [references/i18n-setup.md](references/i18n-setup.md) for both configurations and B2C language context.
194
194
 
195
195
  **Check:**
196
196
  Run `check-i18n-wired.sh` from the UI bundle dir (it scans `src/` relative to the current directory) and report what it returns. The script owns the whole deterministic inspection: it looks for an init file defining `initI18n()` and a boot-time call to it, and when those exist it also reports whether the label manifest is imported and actually passed into the backend config. Do not re-derive any of this by reading files yourself.
@@ -74,7 +74,7 @@ backendOptions: [
74
74
 
75
75
  `USER_DEFAULT` is required for B2C so fallback follows the guest/site language context. Do not copy this override into B2E wiring.
76
76
 
77
- For B2C, also set the document language and direction from the route-selected display language, not `ctx.dir`. The GraphQL i18n context direction reflects the guest session profile and can stay `ltr` after the site switches to an RTL language. Reuse the resolved language from the site's language-switcher integration:
77
+ For B2C, explicitly pass the route-selected display language to i18next and use it for the document language and direction, rather than relying on the detector or `ctx.dir`. The GraphQL i18n context direction reflects the guest session profile and can stay `ltr` after the site switches to an RTL language. Reuse the resolved language from the site's language-switcher integration:
78
78
 
79
79
  ```typescript
80
80
  const resolvedLang =
@@ -83,6 +83,8 @@ document.documentElement.dir = i18next.dir(resolvedLang);
83
83
  document.documentElement.lang = resolvedLang.replace(/_/g, "-");
84
84
  ```
85
85
 
86
+ Then add `lng: resolvedLang` to the B2C `i18next.init({ ... })` options. The B2E example above remains unchanged.
87
+
86
88
  Before using this configuration, have an org admin confirm that `GraphQLApiOrgPrefForGuestUsers` is already enabled. This workflow must never enable it. Without the preference, unauthenticated GraphQL label requests return HTTP 403; see dependency W-23854208.
87
89
 
88
90
  **Call it once at boot**, before mounting your app:
@@ -120,7 +122,7 @@ The manifest is how i18next knows what to fetch. An **unregistered key fails sil
120
122
 
121
123
  ## B2C language context
122
124
 
123
- A B2C site's configured languages and language-specific URLs are the source of truth. At boot, the site route supplies `SFDC_ENV.language`; the SDK detector uses that value to resolve labels. A language switcher must navigate to the target language URL and perform a full page reload. An in-place i18next language change is insufficient because the SDK context and localStorage-backed labels are established at boot.
125
+ A B2C site's configured languages and language-specific URLs are the source of truth. At boot, the site route supplies `SFDC_ENV.language`; pass that value explicitly as i18next's `lng`, with `ctx.lang` as fallback. The SDK detector does not read `SFDC_ENV.language` itself. A language switcher must navigate to the target language URL and perform a full page reload. An in-place i18next language change is insufficient because the SDK context and localStorage-backed labels are established at boot.
124
126
 
125
127
  For local preview, use the site entry of the Vite plugin and pass the site's supported language codes, with the default first:
126
128
 
@@ -169,7 +171,7 @@ If you see an older example that vendors `salesforce-detector.ts` or `salesforce
169
171
 
170
172
  1. Bundle loads, `initI18n()` runs
171
173
  2. `createDataSDK()` initializes the SDK
172
- 3. `fetchI18nContext()` queries the org for language/locale/direction (B2C uses the site route's `SFDC_ENV.language`)
174
+ 3. `fetchI18nContext()` queries the org for language/locale/direction; B2C separately passes the site route's `SFDC_ENV.language` to i18next as `lng`
173
175
  4. `SalesforceBackend` reads the manifest and issues a GraphQL query per namespace:
174
176
  ```graphql
175
177
  query LoadLabels {
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: experience-ui-bundle-project-generate
3
- description: "Generates a minimal, ready-to-develop SFDX starter project from template instead of hand-scaffolding files. Use this skill when starting a brand-new Salesforce React UI bundle app and the initial project must be scaffolded — trigger phrases include create, start, or scaffold a new React UI bundle app, generate a starter project, or use a prebuilt/starter template. DO NOT TRIGGER when: editing, styling, or adding pages or components to an EXISTING app (use experience-ui-bundle-frontend-generate); configuring ui-bundle.json or metadata files (use experience-ui-bundle-metadata-generate); deploying to an org (use experience-ui-bundle-deploy); or when the user explicitly says they want to hand-scaffold from scratch."
3
+ description: "Generates a minimal, ready-to-develop SFDX starter project from template instead of hand-scaffolding files. Use this skill when starting a brand-new Salesforce UI bundle app (React or Angular) and the initial project must be scaffolded — trigger phrases include create, start, or scaffold a new UI bundle app, generate a starter project, or use a prebuilt/starter template. DO NOT TRIGGER when: editing, styling, or adding pages or components to an EXISTING app (use experience-ui-bundle-frontend-generate); configuring ui-bundle.json or metadata files (use experience-ui-bundle-metadata-generate); deploying to an org (use experience-ui-bundle-deploy); or when the user explicitly says they want to hand-scaffold from scratch."
4
4
  metadata:
5
- version: "1.1"
5
+ version: "1.2"
6
6
  domains: ["Experience"]
7
7
  relatedSkills:
8
8
  - "experience-ui-bundle-app-coordinate"
@@ -21,20 +21,24 @@ metadata:
21
21
 
22
22
  # Using a UI Bundle Template
23
23
 
24
- Before building a Salesforce React UI bundle app from scratch, offer the user a **prebuilt starter template**. The Salesforce CLI generates these — a complete, deployable SFDX project (React UI bundle + toolchain + an `npm run setup` automation) — in one command. Starting from a starter is faster and less error-prone than hand-scaffolding.
24
+ Before building a Salesforce UI bundle app from scratch, offer the user a **prebuilt starter template**. The Salesforce CLI generates these — a complete, deployable SFDX project (UI bundle + toolchain + an `npm run setup` automation) — in one command. Starting from a starter is faster and less error-prone than hand-scaffolding.
25
25
 
26
26
  The CLI command is `sf template generate project`.
27
27
 
28
28
  ## Step 1: Offer the choice
29
29
 
30
- The following are the templates. Ask the user which one fits, or whether they want to start from scratch.
30
+ **Determine the framework.** It is normally already decided by the calling context — either passed down by the root/coordinator skill that invoked this one, or stated in the user's request. Use that.
31
31
 
32
- | Template | `--template` flag | Best for |
33
- |----------|-------------------|----------|
34
- | Internal starter | `reactinternalapp` | Starter for internal, employee-facing Salesforce apps (e.g. support consoles, ops dashboards, internal admin apps) — users are already-authenticated employees. Includes Agentforce chat. No login flow or public. |
35
- | External starter | `reactexternalapp` | Starter for customer/partner-facing Salesforce apps/sites (e.g. portals, communities, storefront, public sites). Full auth support (login, registration, reset, profile) -- external users sign in with their own accounts. |
32
+ The frameworks this skill supports are exactly the reference files under `<SKILL_DIR>/references/`, each named `<framework>-project-generate.md` (so `react` → `<SKILL_DIR>/references/react-project-generate.md`). This is the single source of truth — adding a framework means adding a reference file, nothing here changes.
36
33
 
37
- **If the user prefers to start from scratch** (or neither fits), stop here and let `experience-ui-bundle-app-coordinate` scaffold a new project. This skill is opt-in — do not force a template.
34
+ - **If the framework is known** — open `<SKILL_DIR>/references/<framework>-project-generate.md`.
35
+ - **If it is unknown** (a standalone run where nobody said which) — list `<SKILL_DIR>/references/`, derive the supported set by stripping the `-project-generate.md` suffix from each filename, and ask the user to pick one of those. If the user names a framework with no matching reference file, it is not supported here — hand off to `experience-ui-bundle-app-coordinate` to scaffold from scratch.
36
+
37
+ Each reference lists that framework's `--template` flags and what each starter contains. Pick the one that fits the user's audience (internal vs. external).
38
+
39
+ **If the user prefers to start from scratch** (or neither template fits), stop here and let `experience-ui-bundle-app-coordinate` scaffold a new project. This skill is opt-in — do not force a template.
40
+
41
+ Once the user picks, carry the chosen `--template` flag into Step 2.
38
42
 
39
43
  ## Step 2: Generate the project into the target root
40
44
 
@@ -48,8 +52,8 @@ The project contents must land **directly at the target root `$DEST`** — so `s
48
52
  NAME=MyApp # project name the user chose; also names the UI bundle
49
53
  DEST=. # target root directory (the contents land directly here, no NAME/ wrapper)
50
54
 
51
- # choose ONE template flag based on the user's pick from Step 1
52
- TEMPLATE=reactinternalapp # or reactexternalapp
55
+ # the --template flag from the framework reference chosen in Step 1
56
+ TEMPLATE=reactinternalapp # example placeholder — replace with the flag from your Step-1 reference
53
57
 
54
58
  mkdir -p "$DEST"
55
59
  sf template generate project --name "$NAME" --template "$TEMPLATE" --output-dir "$DEST"
@@ -70,7 +74,7 @@ After generation, confirm the contents landed at the root (not in a `$NAME/` sub
70
74
  test -f "$DEST/sfdx-project.json" && echo "OK: project root landed" || echo "FAILED"
71
75
  ```
72
76
 
73
- `sfdx-project.json` must sit at `$DEST/sfdx-project.json`. The project also contains `package.json`, `force-app/main/default/uiBundles/$NAME/` (the React/Vite bundle), `scripts/`, `config/`, and `README.md`. If `sfdx-project.json` is missing or is one level down in `$DEST/$NAME/`, the flatten did not run — re-check before continuing.
77
+ `sfdx-project.json` must sit at `$DEST/sfdx-project.json`. The project also contains `package.json`, `force-app/main/default/uiBundles/$NAME/` (the UI bundle), `scripts/`, `config/`, and `README.md`. See the framework reference from Step 1 for the specific bundle contents. If `sfdx-project.json` is missing or is one level down in `$DEST/$NAME/`, the flatten did not run — re-check before continuing.
74
78
 
75
79
  ## Step 3: Install dependencies (you do this — do NOT hand off uninstalled)
76
80
 
@@ -90,7 +94,7 @@ for b in "$DEST"/force-app/main/default/uiBundles/*/; do
90
94
  done
91
95
  ```
92
96
 
93
- > First-run install of the bundle is the heavy step (tailwind, radix-ui, recharts, vite, etc.); expect a short wait. If an install fails, surface it — don't hand off a half-installed project.
97
+ > First-run install of the bundle is the heavy step; expect a short wait. If an install fails, surface it — don't hand off a half-installed project.
94
98
 
95
99
  ## Step 4: Confirm and hand off
96
100
 
@@ -109,4 +113,4 @@ From here, continue development with the other ui-bundle skills (`experience-ui-
109
113
 
110
114
  - The starters are **minimal** — no seeded sample data or custom objects. Build the rest with the other ui-bundle skills.
111
115
  - These templates use the `uiBundles` metadata convention. The UI bundle directory and meta XML are named after the project name you pass to `--name`.
112
- - `sf template generate project --help` lists all available templates if the flag names ever change.
116
+ - `sf template generate project --help` lists all available templates if the flag names ever change.
@@ -0,0 +1,22 @@
1
+ # Angular UI Bundle Starter Templates
2
+
3
+ Reference for the **Angular** path of `experience-ui-bundle-project-generate`. Pick the `--template` flag that fits the user's audience, then return to Step 2 of `SKILL.md`.
4
+
5
+ ## Template options
6
+
7
+ | Template | `--template` flag | Best for |
8
+ |----------|-------------------|----------|
9
+ | Internal starter | `angularinternalapp` | Starter for internal, employee-facing Salesforce apps (e.g. support consoles, ops dashboards, internal admin apps) — users are already-authenticated employees. Includes agent chat container. No login flow or public access. |
10
+ | External starter | `angularexternalapp` | Starter for customer/partner-facing Salesforce apps/sites (e.g. portals, communities, storefront, public sites). Includes agent chat container. Full auth support (login, registration, reset, profile) — external users sign in with their own accounts. |
11
+
12
+ ## What the bundle contains
13
+
14
+ The generated UI bundle under `force-app/main/default/uiBundles/$NAME/` is an **Angular** app (Angular 21.2.x) built on **spartan-ng**:
15
+
16
+ - **Build:** Angular CLI driven by `angular.json` with the `@angular/build:application` builder (esbuild-based). Salesforce platform integration is wired through the `@salesforce/angular-plugin-ui-bundle` esbuild plugin (via `@angular-builders/custom-esbuild`), which handles API-version substitution, the org proxy, and Live Preview / `SFDC_ENV` / base-href injection.
17
+ - **Components:** standalone components + signals + native control flow (`@if`/`@for`); each component is a `.ts` + `.html` pair, named without a `.component` infix (e.g. `home.ts` + `home.html`).
18
+ - **App wiring:** entry `src/main.ts`; `src/app/app.ts` + `src/app/app.html`; `src/app/app.config.ts` (`APP_BASE_HREF` + `SFDC_ENV.basePath`); `src/app/app.routes.ts`. Pages under `src/app/pages/` (e.g. `home/`, `login/`, `account-search/`, `not-found/`).
19
+ - **UI library:** **spartan-ng** — `hlm-*` "Helm" primitives under `src/app/shared/` (alert, button, calendar, card, dialog, dropdown-menu, field, input, label, pagination, popover, select, separator, skeleton, spinner…), each folder exporting an `index.ts`. Configured via `components.json`; built on `@spartan-ng/brain` + `@spartan-ng/cli` + **Tailwind 4** (`src/styles.css`). Not Angular Material — there is no `theme.scss`. `src/app/components/` holds only `layout/app-layout` plus small `ui/` helpers.
20
+ - **Data layer:** injectable GraphQL data client `src/app/api/data-client.service.ts` (plus `user-profile.service.ts`, generated `graphql-operations-types.ts`, and typed operations under `src/app/api/account/`). GraphQL type codegen is optional and manual (not chained to the build).
21
+ - **Metadata & config:** `ui-bundle.json`, `$NAME.uibundle-meta.xml`, `tsconfig.*`, `eslint.config.js`, `playwright.config.ts`, `README.md`.
22
+ - **External (customer-facing) markers:** the external template also emits `networks/$NAME.network-meta.xml` + `sites/$NAME.site-meta.xml` and a full `src/app/features/authentication/` feature (login, register, forgot/reset/change-password, profile) — these distinguish the external starter from the internal one.
@@ -0,0 +1,20 @@
1
+ # React UI Bundle Starter Templates
2
+
3
+ Reference for the **React** path of `experience-ui-bundle-project-generate`. Pick the `--template` flag that fits the user's audience, then return to Step 2 of `SKILL.md`.
4
+
5
+ ## Template options
6
+
7
+ | Template | `--template` flag | Best for |
8
+ |----------|-------------------|----------|
9
+ | Internal starter | `reactinternalapp` | Starter for internal, employee-facing Salesforce apps (e.g. support consoles, ops dashboards, internal admin apps) — users are already-authenticated employees. Includes agent chat container. No login flow or public access. |
10
+ | External starter | `reactexternalapp` | Starter for customer/partner-facing Salesforce apps/sites (e.g. portals, communities, storefront, public sites). Includes agent chat container. Full auth support (login, registration, reset, profile) — external users sign in with their own accounts. |
11
+
12
+ ## What the bundle contains
13
+
14
+ The generated UI bundle under `force-app/main/default/uiBundles/$NAME/` is a **React/Vite** app:
15
+
16
+ - React + Vite toolchain, TypeScript, `vite.config.ts`.
17
+ - shadcn/ui primitives (`src/components/ui/`), Tailwind.
18
+ - `.tsx` pages/components; app entry `src/App.tsx`, pages under `src/pages/`.
19
+ - GraphQL client (`src/api/graphqlClient.ts`) + codegen (`codegen.yml`), `useAsyncData` data util.
20
+ - `ui-bundle.json`, `*.uibundle-meta.xml`, project config, `README.md`.
@@ -31,10 +31,10 @@ This file is the **workflow + guardrail spine**. Depth lives in linked docs:
31
31
  commands) that compiles a small JSON spec into a schema-correct, guardrail-applied query +
32
32
  variables + types. The preferred way to author the GraphQL in steps below; falls back to the
33
33
  schema-grep script when unavailable.
34
- - **[references/sdk-api.md](references/sdk-api.md)** — the new call API: `query`/`mutate`,
35
- `QueryResult`, typing, error-handling stances.
36
- - **[references/caching.md](references/caching.md)** — on-by-default cache + the **two refresh
37
- modes** (`result.refresh`/`subscribe` vs per-call `cacheControl`).
34
+ - **[references/sdk-api.md](references/sdk-api.md)** — `query`/`mutate` call surface + generated-type
35
+ placement; the behavior nuance (surfaces, error stances, `QueryResult`) grounds on **tier-2b**.
36
+ - **[references/caching.md](references/caching.md)** — the on-by-default cache + two refresh modes;
37
+ behavior grounds on **tier-2b** `docs/data/` when installed, with the full version-stamped fallback here.
38
38
  - **[references/graphql-hand-authoring.md](references/graphql-hand-authoring.md)** — schema lookup, read /
39
39
  mutation templates, every platform guardrail (`@optional`, pagination, limits,
40
40
  semi-join, wrappers, error table…).
@@ -99,7 +99,8 @@ declaration and note the drift; do not "correct" the types to match the prose.
99
99
  |---|---|---|---|
100
100
  | tier-1 | GraphQL **schema** | *what data exists* | graphiti / `graphql-search.sh` (Precondition #2) |
101
101
  | tier-2a | SDK **contract** | *how you call it* | the installed `.d.ts` above |
102
- | spine | this SKILL.md | workflow + guardrails that orchestrate both; the fallback when a tier can't ground |
102
+ | tier-2b | SDK **behavior** | *how it behaves* | the installed `docs/data/` folder (below) |
103
+ | spine | this SKILL.md | workflow + guardrails that orchestrate all three; the fallback when a tier can't ground |
103
104
 
104
105
  **Fallback when the `.d.ts` is absent** — the package **is installed** but ships
105
106
  no declarations (a stale or types-stripped build artifact). Then use this SKILL's
@@ -109,22 +110,35 @@ prose as best-effort. This fallback does **not** cover a missing package: if
109
110
 
110
111
  ---
111
112
 
112
- ## Surfaces — `sdk.graphql!` vs guard
113
+ ## Ground the SDK behavior on the installed docs (tier-2b)
113
114
 
114
- `createDataSDK()` runs on multiple surfaces, and **`sdk.graphql` / `sdk.fetch` are genuinely
115
- optional** (typed `graphql?: …`). Whether you may assert them with `!` depends entirely on
116
- where the bundle runs — this is the one surface decision that turns into a *runtime crash* if
117
- you get it wrong, so make it explicitly before writing any `query`/`mutate` call:
115
+ The same package ships an authored **behavior** guide beside its types:
116
+ `node_modules/@salesforce/platform-sdk/docs/data/` (numbered files, read them in order).
117
+ Tier-2a's `.d.ts` fixes the call *contract*; this folder is authoritative for the *behavior* the
118
+ contract doesn't spell out — the caching model, the surface `!`-vs-guard decision, error-handling
119
+ stances, the migration mindset. **Read it before choosing a caching policy, a surface assertion, or
120
+ an error stance, and let it win** — same precedence as tier-2a (the installed source beats this
121
+ prose; when present it's the fuller, version-current copy).
118
122
 
119
- | Surface(s) | `sdk.graphql` | Write |
120
- |---|---|---|
121
- | **WebApp only** | always present | `sdk.graphql!.query({...})` — `!` is safe; every shipped WebApp consumer uses it |
122
- | **Mosaic / OpenAI / MCPApps** (or any bundle that *might* run off-WebApp) | can be `undefined` | **guard first** (`if (!sdk.graphql) return …`), then call |
123
+ **Fallback when the folder is absent** (older SDK, or a types-only build): this SKILL keeps a thin
124
+ per-behavior fallback — below and in each section — sized only to keep you moving; act on it. As
125
+ with tier-2a, a missing *package* is different: if `@salesforce/platform-sdk` isn't installed, stop
126
+ and install it (Precondition #1).
123
127
 
124
- Rule of thumb: **if you cannot prove the bundle is WebApp-only, guard.** A bare `sdk.graphql!`
125
- that later ships to another surface throws `Cannot read properties of undefined` at runtime —
126
- TypeScript won't catch it because `!` silences exactly that check (same applies to `sdk.fetch!`).
127
- The portable guard snippet lives in [references/sdk-api.md](references/sdk-api.md#sdkgraphql-vs-guard).
128
+ ---
129
+
130
+ ## Surfaces — `sdk.graphql!` vs guard
131
+
132
+ `sdk.graphql` / `sdk.fetch` are genuinely optional (typed `graphql?: …`), and whether you may
133
+ assert them with `!` is a *runtime-crash* decision — make it before writing any `query`/`mutate`.
134
+ **Fallback rule: WebApp-only bundle → `sdk.graphql!` is safe; any bundle that might run
135
+ off-WebApp (Mosaic / OpenAI / MCPApps) → guard first (`if (!sdk.graphql) return …`), then call.**
136
+ If you cannot prove WebApp-only, guard — a bare `!` that later ships elsewhere throws
137
+ `Cannot read properties of undefined` and TypeScript won't catch it (same for `sdk.fetch!`).
138
+
139
+ The surface matrix, the portable guard snippet, and the full reasoning ground on **tier-2b**
140
+ `docs/data/` (fallback above); the guard snippet is also in
141
+ [references/sdk-api.md](references/sdk-api.md#sdkgraphql-vs-guard).
128
142
 
129
143
  ---
130
144
 
@@ -159,7 +173,7 @@ you grounded against the right file.
159
173
 
160
174
  | # | Requirement | Verify | If missing |
161
175
  |---|---|---|---|
162
- | 1 | `@salesforce/platform-sdk` installed **and its contract read** | `package.json` in the UI bundle dir lists it; then read `node_modules/@salesforce/platform-sdk/dist/core/data.d.ts` + `dist/data/index.d.ts` and let them win over this SKILL's prose ([tier-2a](#ground-the-sdk-contract-on-the-installed-types-tier-2a)) | Not installed → tell user to install it; cannot proceed. Installed but `.d.ts` absent (stale artifact) → use prose fallback |
176
+ | 1 | `@salesforce/platform-sdk` installed **and its contract + behavior docs read** | `package.json` in the UI bundle dir lists it; then read `dist/core/data.d.ts` + `dist/data/index.d.ts` ([tier-2a](#ground-the-sdk-contract-on-the-installed-types-tier-2a)) **and** the `docs/data/` folder ([tier-2b](#ground-the-sdk-behavior-on-the-installed-docs-tier-2b)), and let them win over this SKILL's prose | Not installed → tell user to install it; cannot proceed. Installed but `.d.ts` / `docs/` absent (stale or types-only artifact) → use prose fallback |
163
177
  | 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) |
164
178
  | 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 |
165
179
 
@@ -220,7 +234,8 @@ write GraphQL strings until the schema workflow is complete.
220
234
  ```
221
235
 
222
236
  Defend consuming code with `?.`/`??` (because `@optional` can omit fields). Error-handling
223
- stances (strict / tolerant / discriminated) and `NodeOfConnection` typing: [references/sdk-api.md](references/sdk-api.md).
237
+ stances (strict / tolerant / discriminated) ground on **tier-2b** `docs/data/` (fallback:
238
+ guardrail #1 — always check `result.errors`); `NodeOfConnection` typing in [references/sdk-api.md](references/sdk-api.md).
224
239
 
225
240
  ---
226
241
 
@@ -294,40 +309,28 @@ templates: [references/graphql-hand-authoring.md](references/graphql-hand-author
294
309
 
295
310
  ## Freshness & caching
296
311
 
297
- **Caching is ON by default on WebApp.** Every `sdk.graphql!.query()` is cached with a
298
- **300-second `max-age`** TTL — no opt-in flag, no factory, no import subpath. **Do not
299
- build your own cache** (no React Query, SWR, `localStorage`, or hand-rolled Map). The
300
- cache is **shared across SDK instances by `baseUrl`**: the same query+variables from a
301
- different `createDataSDK()` targeting the same host is a cache hit. Only non-empty,
302
- error-free `data` is cached. `mutate()` is never cached.
303
-
304
- There are **two distinct freshness tools** — keep them separate:
305
-
306
- 1. **Per-call `cacheControl`** — a one-shot policy override on the query options bag
307
- (`"no-cache"` / `"only-if-cached"` / `{ type: "max-age", maxAge: <seconds> }`). The type and
308
- exact per-value behavior live in [references/sdk-api.md](references/sdk-api.md#cachecontrol--the-per-call-cache-policy).
309
- Take `cacheControl` as an optional param on the read function and expose each distinct policy as
310
- a **thin named export in the same data-layer file** — a "call site" is a named export, not a new
311
- React component. For `getAccounts(first, after?, cacheControl?)`: `export const refreshAccounts =
312
- () => getAccounts(20, undefined, "no-cache")` (and likewise `offlineAccounts` → `"only-if-cached"`,
313
- `shortLivedAccounts` → `{ type: "max-age", maxAge: 10 }`). Keep the policy in the data layer.
314
- 2. **Reactive `subscribe` / `refresh`** — a stateful handle on a live `QueryResult`:
315
- `result.subscribe(cb)` fires on every later snapshot, `result.refresh()` re-fetches bypassing
316
- the cache and pushes to subscribers. Shape in [references/sdk-api.md](references/sdk-api.md#queryresultt--the-reactive-query-handle);
317
- subscription lifecycle (always unsubscribe on teardown) in [references/caching.md](references/caching.md).
318
-
319
- | Want | Reach for |
320
- |---|---|
321
- | Freshness within ~5 min is fine | nothing (default cache) |
322
- | This one read must bypass the cache (refresh button) | `cacheControl: "no-cache"` |
323
- | 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. |
324
- | Tighter/looser TTL for this query | `cacheControl: { type: "max-age", maxAge: 60 }` (`maxAge` is in **seconds**) |
325
- | Mounted component reflects updates over time | `result.subscribe(cb)` |
326
- | Re-fetch now + notify all subscribers (e.g. after a mutation) | `result.refresh()` |
327
-
328
- `cacheControl` is fire-and-forget at call time; `subscribe`/`refresh` is a live handle.
329
- Different mechanisms, different jobs — don't conflate "refresh" with "no-cache". Full
330
- behavior, the reactive-subscription lifecycle, and uncached-surface caveats: [references/caching.md](references/caching.md).
312
+ Ground the cache model on **tier-2b** `docs/data/` — cache-key mechanics, what-gets-cached,
313
+ the shared-by-`baseUrl` details, uncached-surface semantics, and the reactive-handle nuance all
314
+ live there ([references/caching.md](references/caching.md) restates it as a version-stamped fallback). The
315
+ **load-bearing fallback** (enough to act when the folder is absent):
316
+
317
+ - **Caching is ON by default on WebApp** — every `query()` cached at **300s**; no opt-in flag, no
318
+ factory, no `/cache` subpath. **Do not build your own cache** (React Query, SWR, `localStorage`,
319
+ hand-rolled `Map`). `mutate()` is never cached.
320
+ - **Shared by host + API version** — the same query+variables from another `createDataSDK()`
321
+ targeting the same host **and** `apiVersion` is a cache hit = one network call; the per-instance
322
+ fetch pipeline stays isolated.
323
+ - **Two distinct freshness tools — don't conflate them:**
324
+ 1. **Per-call `cacheControl`** (one-shot policy on the options bag): `"no-cache"` (bypass, writes
325
+ back) / `"only-if-cached"` / `{ type: "max-age", maxAge: <seconds> }`; default 300s. Thread it as
326
+ an optional param on the read fn and expose each policy as a **thin named export** in the same
327
+ data-layer file (`refreshAccounts` → `"no-cache"`, `offlineAccounts` → `"only-if-cached"`, …). An
328
+ `"only-if-cached"` **miss** surfaces on `result.errors` with `extensions.code === "CACHE_MISS"` —
329
+ render an empty state, **do not** fall back to the network (that defeats offline-first).
330
+ 2. **Reactive `subscribe` / `refresh`** (live handle on a `QueryResult`): `subscribe(cb)` fires on
331
+ **later** snapshots only (always `unsubscribe` on teardown); `refresh()` re-fetches, bypasses the
332
+ cache, pushes to subscribers — use it after a `mutate()` (which has no `refresh`). Multi-subscriber
333
+ fan-out / independence ground on **tier-2b** `docs/data/`.
331
334
 
332
335
  ---
333
336
 
@@ -433,6 +436,7 @@ silent runtime failures. (Details + templates: [references/graphql-hand-authorin
433
436
 
434
437
  - [ ] Surface decided: `sdk.graphql!` only if WebApp-only; otherwise guard with `if (!sdk.graphql) …` ([Surfaces](#surfaces--sdkgraphql-vs-guard))
435
438
  - [ ] SDK contract grounded on installed `dist/*.d.ts` (types win over prose) ([tier-2a](#ground-the-sdk-contract-on-the-installed-types-tier-2a))
439
+ - [ ] SDK behavior grounded on installed `docs/data/` (docs win over prose; fallback if absent) ([tier-2b](#ground-the-sdk-behavior-on-the-installed-docs-tier-2b))
436
440
  - [ ] Every field/entity verified — `sf-gql-discover` (preferred) or `graphql-search.sh` (fallback, against the right schema)
437
441
  - [ ] If compiled with graphiti: `warnings: []` confirmed (non-empty = degraded query, don't ship); `query` pasted verbatim
438
442
  - [ ] `@optional` on FLS-gated fields + relationships (NOT `Id`/`edges`/`node`/`pageInfo`); `?.`/`??` in consuming code