@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,273 @@
1
+ # Conventions & payload rules (Promotion BO API)
2
+
3
+ Load-bearing conventions and the eight cross-workflow payload rules the
4
+ `BO_API_Step_Input_Structure__c` schema alone cannot express. The main
5
+ `SKILL.md` references this file from Phase 5b (registering input paths) and
6
+ Phase 6 (composing/validating smoke payloads).
7
+
8
+ ## CLI shape
9
+
10
+ - `<alias>` — target org `sf` alias. Required.
11
+ - `--sales-org <4-char>` — default `0001`. Must match `^[A-Z0-9]{4}$` (mirrors `TPMSetupData.validateSalesOrg`).
12
+ - `--dry-run` — emit artifacts to `./out/`, skip destructive operations.
13
+ - `--yes` — skip confirmation gates (for CI callers).
14
+ - `--preset set-comment-value` / `--interview-file <path>` — bypass the interactive Phase 5 interview.
15
+
16
+ ## Runtime artifacts
17
+
18
+ - The skill writes to `./out/` inside its own directory.
19
+ - Rollback artifacts follow `./out/rollback-<subject>-<UTC-timestamp>.apex`.
20
+ - Before-snapshots follow `./out/<subject>-before.<ext>`.
21
+
22
+ ## Sales-org partitioning (pre-parameter for every lookup)
23
+
24
+ `--sales-org` is not just a filter — it is the tenancy boundary for **every
25
+ reference lookup** the skill performs. Before any Promotion/Tactic/Product-filter
26
+ payload is composed, every referenced entity MUST be resolved against records
27
+ that carry the same `<prefix>__Sales_Org__c` (namespace resolved at runtime per
28
+ `PREFIX_UNDER` from Phase 1; e.g. `cgcloud__` / `cgcloud_dev__` in installed
29
+ subscriber orgs, or the dev-package prefix).
30
+
31
+ | Reference in payload | SObject to query | Filter |
32
+ |---|---|---|
33
+ | `PromotionTemplate` | `<prefix>__Promotion_Template__c` | `Name = <val> AND <prefix>__Sales_Org__c = <sales-org>` |
34
+ | `Tactics[*].TacticTemplate` | `<prefix>__Tactic_Template__c` | `Name = <val> AND <prefix>__Sales_Org__c = <sales-org>` (and linked to the chosen PromotionTemplate via `<prefix>__Promotion_Template_Tactic_Template__c`) |
35
+ | `AnchorAccount` | `Account` | `<prefix>__ExternalId__c = <val> AND <prefix>__Sales_Org__c = <sales-org>` |
36
+ | `ProductFilter.Criteria.Brand[*]`, `.Category[*]`, `.Subcategory[*]`, `.Flavor[*]`, `.Package[*]` | `<prefix>__Product_Attribute__c` (or Category equivalents) | `Name = <val> AND <prefix>__Sales_Org__c = <sales-org>` |
37
+ | `ProductFilter.{Included,Excluded}Products[*]` | `Product2` | matched by external key AND `<prefix>__Sales_Org__c = <sales-org>` |
38
+ | `PromotionAccountSet` / `AnchorAccountSet` | `<prefix>__Account_Set__c` | `Name = <val> AND <prefix>__Sales_Org__c = <sales-org>` |
39
+
40
+ Fail-fast — before any REST call — if any user-supplied reference does not
41
+ resolve inside the sales org. "Belongs to another sales org" and "does not
42
+ exist" are the same error class for the user; report the sales org in the message.
43
+
44
+ ## Payload contract derivation (schema-first, before user input)
45
+
46
+ Never hard-code create/update/copy payload shape. The BO API workflow steps
47
+ themselves declare the accepted paths. Before interviewing the user or accepting
48
+ an intent JSON, retrieve the contract from the org:
49
+
50
+ ```sql
51
+ SELECT sis.<prefix>__Path__c,
52
+ sis.<prefix>__Required__c,
53
+ sis.<prefix>__Maps_To__c,
54
+ sis.<prefix>__Maximum_Items__c, sis.<prefix>__Minimum_Items__c,
55
+ sis.<prefix>__Maximum_Length__c, sis.<prefix>__Minimum_Length__c,
56
+ sis.<prefix>__Pattern__c,
57
+ sis.<prefix>__BO_API_Workflow_Step__r.Name step
58
+ FROM <prefix>__BO_API_Step_Input_Structure__c sis
59
+ WHERE sis.<prefix>__BO_API_Workflow_Step__c IN (
60
+ SELECT <prefix>__BO_API_Workflow_Step__c
61
+ FROM <prefix>__BO_API_Workflow_Workflow_Step__c
62
+ WHERE <prefix>__BO_API_Workflow__r.Name = '<workflow>'
63
+ AND <prefix>__BO_API_Workflow__r.<prefix>__BO_API__r.Name = '<entity>'
64
+ AND <prefix>__BO_API_Workflow__r.<prefix>__BO_API__r.<prefix>__Sales_Org__c = '<sales-org>'
65
+ )
66
+ ```
67
+
68
+ Merge rows across steps → the required set is the union of `Required__c = true`
69
+ paths; the optional set is everything else. The skill's interview and
70
+ mass-upload template are BOTH generated from this contract, per invocation. Two
71
+ additional constraints the schema alone cannot express and that the skill must
72
+ layer on:
73
+
74
+ - **Reference-data existence and sales-org membership.** Every non-primitive value (template name, external id, category name) must resolve per the table in "Sales-org partitioning" above.
75
+ - **Template-conditional required fields.** Some fields are required only for specific `PromotionTemplate` values (e.g. `AnchorAccount` is required for `Customer Promotion` even though the schema declares it optional). Detect these from `mapPromotionValues*` variants, `setPromotionAnchor*` variants, and by pre-flighting a validate call against the target template — do not hard-code the list; template membership can change.
76
+
77
+ Where mass upload is offered, provide a CSV/JSONL template whose columns are
78
+ exactly the union of paths from the derived contract (with `[R]` markers on
79
+ required columns).
80
+
81
+ ## Promotion payload — cross-workflow rules the schema cannot express
82
+
83
+ The `BO_API_Step_Input_Structure__c` contract declares WHICH keys the workflow
84
+ accepts and which are `Required__c=true`, but eight additional constraints govern
85
+ whether a create/update/copy/derive call will actually succeed. Enforce them
86
+ BEFORE the REST call — otherwise the request 202s, then dies asynchronously
87
+ inside a workflow step with a `BO_API_Transaction_Log__c.Status__c = 'Error'`.
88
+
89
+ ### R1. Always-required create fields (`DateFrom`, `DateThru`, `Slogan`)
90
+
91
+ Beyond the schema-declared requireds, `Promotion2BoApiCoreWorkflowSteps.setPromotionDates`
92
+ throws `PBACWS0008` when a new promotion is missing `DateFrom` or `DateThru`.
93
+ `Slogan` is required by the CSV row `SIS-068` on `mapPromotionValues2`. All three
94
+ are create-workflow only — update omits them.
95
+
96
+ `DateThru` is NOT derivable by the framework. When user intent is "N weeks
97
+ starting D" (e.g. "6 weeks starting 2026-08-01"), the skill MUST compute it
98
+ client-side:
99
+
100
+ ```text
101
+ DateThru = DateFrom + (duration_weeks * 7) - 1
102
+ ```
103
+
104
+ Never send a `durationWeeks` — the workflow does not accept it.
105
+ `PlacementDateFrom/OrderDateFrom/DeliveryDateFrom` (and their `-Thru` siblings)
106
+ default to the top-level dates on new promotions; do not need to be sent.
107
+
108
+ ### R2. Terminal status is `Calculated`, not `Processed`
109
+
110
+ `BO_API_Transaction_Log__c.Status__c` is a restricted picklist: `Initial,
111
+ Written, ToBeCalculated, Calculated, Error`. `Processed` is NOT in the picklist
112
+ and is never set anywhere in the codebase. The success predicate for every
113
+ workflow (`create`/`update`/`copy`/`derive`) is `Status__c = 'Calculated'`; the
114
+ pending predicate is `Status__c != 'Calculated' AND Status__c != 'Error'`. Any
115
+ poll of `/status` or the transaction log MUST match on `Calculated` — matching
116
+ on `Processed` hangs forever.
117
+
118
+ ### R3. ProductFilter criteria keys are level NAMES, not field API names
119
+
120
+ The payload sends level NAMES verbatim as the `ProductFilter.Criteria` keys —
121
+ `Category`, `SubCategory`, `Brand`, `Flavor`, `Package` — NOT Product2 field API
122
+ names. The framework consults `Product_Levels_Mapping__mdt` internally to
123
+ translate. Sending a Product2 field API name (e.g. `<prefix>__Criterion_1_Product__c`)
124
+ as a key fails schema validation with `"Properties other than those defined were
125
+ not expected at #.ProductFilter.Criteria.<key>"`.
126
+
127
+ ```json
128
+ "ProductFilter": {
129
+ "Criteria": {
130
+ "Category": ["Snacks"],
131
+ "Brand": ["YetiBar"]
132
+ }
133
+ }
134
+ ```
135
+
136
+ `Product_Levels_Mapping__mdt` still matters for two skill tasks:
137
+ 1. **Enumerate levels available in this sales org** — a level not present in the mapping for the sales org, and not in the fallback (`Category`, `SubCategory`, `Brand`, `Flavor`, `Package`), cannot be sent.
138
+ 2. **SOQL verification after ingest** — translate the level back to its Product2 field to build the verify SOQL.
139
+
140
+ Default mapping (fallback and every seeded metadata row):
141
+
142
+ | Level name (payload key) | Product2 field (verify SOQL target) |
143
+ |---|---|
144
+ | `Category` | `<prefix>__Criterion_1_Product__c` (invariant across every sales org) |
145
+ | `SubCategory` | `<prefix>__Criterion_2_Product__c` |
146
+ | `Brand` | `<prefix>__Criterion_3_Product__c` |
147
+ | `Flavor` | `<prefix>__Criterion_4_Product__c` |
148
+ | `Package` | `<prefix>__Criterion_5_Product__c` |
149
+
150
+ Discovery SOQL:
151
+ ```sql
152
+ SELECT <prefix>__Product_Level_Name__c, <prefix>__Product_SObject_Field__c, <prefix>__Hierarchy_Level__c
153
+ FROM <prefix>__Product_Levels_Mapping__mdt
154
+ WHERE <prefix>__Sales_Org__c = :salesOrg
155
+ ```
156
+
157
+ ### R4. Anchor account must have a `Plan` Customer Extension valid for the promotion period
158
+
159
+ Promotions can only be created for accounts with `Account_Plan_Type__c = 'Plan'`
160
+ on `Account_Extension__c` (label "Customer Extension") for the target sales org,
161
+ with `Promotion_Valid_From__c <= DateFrom` AND `Promotion_Valid_Thru__c >=
162
+ DateThru`. The create path validates the anchor exists in the sales org but does
163
+ NOT enforce Plan+dates — the skill MUST pre-flight. The object API name is
164
+ `Account_Extension__c` even though the label reads "Customer Extension"; there is
165
+ no `Customer_Extension__c` object.
166
+
167
+ ```sql
168
+ SELECT Id, <prefix>__Account__c
169
+ FROM <prefix>__Account_Extension__c
170
+ WHERE <prefix>__Account__c = :anchorId
171
+ AND <prefix>__Account_Plan_Type__c = 'Plan'
172
+ AND <prefix>__Sales_Org__c = :salesOrg
173
+ AND <prefix>__Promotion_Valid_From__c <= :dateFrom
174
+ AND <prefix>__Promotion_Valid_Thru__c >= :dateThru
175
+ LIMIT 1
176
+ ```
177
+ Zero rows → stop with `"Account <anchor> is not enrolled as a Plan account in sales org <salesOrg> for the period <dateFrom>..<dateThru>"`.
178
+
179
+ ### R5. ManualInputs KPIs are `editable` / `editable_calculated` on the template's KPI Set
180
+
181
+ `ManualInputs[]` accepts only KPIs the template's `KPI_Set__c` includes, and only
182
+ those whose `RecordType.DeveloperName` is `editable` or `editable_calculated`.
183
+ Any other KPI is rejected by `Promotion2BoApiCoreWorkflowSteps.validateManualInputKPI`.
184
+ Read the allowed set from the org — do not hard-code. If the template's
185
+ `KPI_Set__c` is null, ManualInputs are silently skipped (framework logs
186
+ `PBACWS0018`) — ask the user before proceeding.
187
+
188
+ The base object is `<prefix>__KPI_Definition__c` (NOT `KPI__c`).
189
+ `KPI_Definition__c` and `KPI_Set__c` have no `Sales_Org__c` field — filter via
190
+ `Promotion_Template__c.Sales_Org__c`.
191
+
192
+ ```sql
193
+ SELECT kd.Id, kd.Name, kd.<prefix>__Editable_Measure_Code__c,
194
+ kd.<prefix>__Compound_Main__c, kd.<prefix>__Editable_Fixed_Totals__c,
195
+ kd.<prefix>__BoM_Scope__c, kd.RecordType.DeveloperName
196
+ FROM <prefix>__KPI_Definition__c kd
197
+ WHERE Id IN (
198
+ SELECT <prefix>__KPI_Definition__c FROM <prefix>__KPI_Set_KPI_Definition__c
199
+ WHERE <prefix>__KPI_Set__c IN (
200
+ SELECT <prefix>__KPI_Set__c FROM <prefix>__Promotion_Template__c
201
+ WHERE Id = :promotionTemplateId AND <prefix>__Sales_Org__c = :salesOrg))
202
+ AND RecordType.DeveloperName IN ('editable','editable_calculated')
203
+ ```
204
+ Framework-enforced extras (fail on the REST call, not pre-flight): `editable`
205
+ KPIs with `Editable_Fixed_Totals__c=true` are rejected; `editable_calculated`
206
+ KPIs must have `Compound_Main__c=true` unless BoM components. The payload's
207
+ `ManualInputs[].KPI` value is `KPI_Definition__c.Name`; the downstream measure
208
+ code is `Editable_Measure_Code__c`.
209
+
210
+ ### R6. Derive / Copy required fields — read them from the workflow metadata
211
+
212
+ Do not hard-code the required set for `copy` and `derive`; derive them from
213
+ `BO_API_Step_Input_Structure__c` (`Required__c=true`) the same way as `create`,
214
+ then layer the Apex-only preconditions:
215
+
216
+ **Copy:**
217
+ - Schema-required: `.Id` (source promotion).
218
+ - Apex-only: source promotion's template must have `Is_Copyable__c = true`; template `Promotion_Type__c` must equal `'Promotion'`; if template `Anchor_Type__c = 'Customer'`, `.AnchorAccount` must resolve in the sales org; if `'CustomerSet'`, `.AnchorAccountSet` must resolve.
219
+
220
+ **Derive:**
221
+ - Schema-required: `.Id`, `.PromotionTemplate`, `.AnchorAccount` — all at the derive input root (`currentInput`), not nested (`PBACWS_REQUIRED_PROP_MISSING` fires against `Path__c` verbatim).
222
+ - Apex-only: source template `Is_Derivable__c = true`; a `Promotion_Template_Hierarchy__c` row with `Usage__c='Derive'` and `Child_Promotion_Template__c` = target; target template `Promotion_Type__c = 'Promotion'`; `.AnchorAccount`/`.AnchorAccountSet` valid within the promotion date range (recurses to R4).
223
+
224
+ > Note: **this skill wires only `{create, update, copy}`; `derive` is out of scope.** R6's derive detail is here for completeness / a follow-up skill.
225
+
226
+ If any Apex-only precondition is not derivable from the org, treat the field as mandatory in the interview.
227
+
228
+ ### R7. Tactic templates must be linked to the promotion template AND share its sales org
229
+
230
+ `Tactics[*].TacticTemplate` must resolve to a `Tactic_Template__c` in the target
231
+ sales org AND be linked to the chosen `PromotionTemplate` via the
232
+ `Promotion_Template_Tactic_Template__c` junction (no `Sales_Org__c` of its own;
233
+ Name is auto-number `PTTT-{0000}`) — filter through both parents:
234
+
235
+ ```sql
236
+ SELECT Id
237
+ FROM <prefix>__Promotion_Template_Tactic_Template__c
238
+ WHERE <prefix>__Promotion_Template__r.Name = :promotionTemplateName
239
+ AND <prefix>__Promotion_Template__r.<prefix>__Sales_Org__c = :salesOrg
240
+ AND <prefix>__Tactic_Template__r.Name = :tacticTemplateName
241
+ AND <prefix>__Tactic_Template__r.<prefix>__Sales_Org__c = :salesOrg
242
+ LIMIT 1
243
+ ```
244
+ Zero rows → stop with `"Tactic template <tt> is not linked to promotion template <pt> in sales org <salesOrg>"`.
245
+
246
+ ### R8. PromotionTemplate must be `Promotion_Type__c = 'Promotion'` — reject Sellable/Retail templates
247
+
248
+ `Promotion2BoApiCoreCommon.cls:867-875` throws `CPC0009` when the resolved
249
+ `PromotionTemplate.Promotion_Type__c != 'Promotion'`. Sellable/Retail templates
250
+ (`New Item Introduction`, `Selectable Promotion`, `Distributor Promotion`,
251
+ `Regional Promotion`, `Consumer Promotion`, `Campaign Promotion`) carry a
252
+ different type and belong to the retail sellable-promotion path.
253
+
254
+ On create the failure surfaces as an off-platform `WriteError` with a misleading
255
+ message (`Invalid JSON! /productfilter must be object`, HTTP 400 from the
256
+ Hyperforce CGCPS validator) — it blames `/productfilter` but the actual defect is
257
+ the template type. The user-facing error does NOT name the template type, so the
258
+ skill MUST fail-fast on template type before ingest.
259
+
260
+ ```sql
261
+ SELECT Id, Name, <prefix>__Promotion_Type__c
262
+ FROM <prefix>__Promotion_Template__c
263
+ WHERE Name IN :templateNames
264
+ AND <prefix>__Sales_Org__c = :salesOrg
265
+ ```
266
+ Reject any row where `Promotion_Type__c != 'Promotion'` with:
267
+ `"Promotion template <name> has Promotion_Type__c='<type>'; this workflow only supports templates with Promotion_Type__c='Promotion'. Sellable Promotions belong to the retail path."`
268
+
269
+ ## Wire-format quirks
270
+
271
+ - `POST /promotions/initialize` deserializes lowercase `salesorg` / `nrofitems`. Always emit lowercase.
272
+ - `BO_API_Workflow_Workflow_Step__c.Enabled__c` is not honored at runtime. To disable a step, DELETE the junction; do not flip the boolean.
273
+ - Success matches `Status__c = 'Calculated'` (R2). `Processed` is not a valid status; polling for it will hang.
@@ -0,0 +1,236 @@
1
+ # Phase 5a / 5b — Generate + deploy the Apex class, register + wire the step
2
+
3
+ Detailed runbook for the two write phases. `SKILL.md` Phase 5a/5b link here.
4
+ Both phases run only on a green Phase 4 and a confirmed Phase 5 interview.
5
+ `PREFIX_DOT` / `PREFIX_UNDER` come from the Phase-1 namespace detection.
6
+
7
+ ## Phase 5a — Generate + deploy the customer's Apex class
8
+
9
+ 1. **Idempotency guard** — is the class name already taken?
10
+
11
+ ```bash
12
+ sf data query --target-org <alias> --json --query "
13
+ SELECT NamespacePrefix FROM ApexClass WHERE Name = '<stepName>'
14
+ "
15
+ ```
16
+
17
+ Non-empty and non-namespaced → an ApexClass by that name already exists in
18
+ the customer's namespace. Ask: overwrite (redeploy same name), rename the
19
+ interview answer, or abort. Namespaced → a *packaged* class with that name
20
+ (unexpected); stop and ask before overwriting anything.
21
+
22
+ 2. **Confirm target-field writeability.** For every entry in `outputWrites`,
23
+ describe the resolved field `${PREFIX_UNDER}<field>` on
24
+ `${PREFIX_UNDER}<entity>__c` and verify `createable=true, updateable=true`
25
+ for the running user. Any failure → stop, print the failing field + FLS
26
+ remediation.
27
+
28
+ 3. **Generate the Apex class from the interview.** The generator takes the
29
+ schema captured in the interview (Phase 5.1) and emits a namespace-agnostic
30
+ Callable in the same shape as the reference example
31
+ (`references/reference-example-set-comment-value.md`). Rules (do not deviate):
32
+ - Declare the class **`global`** and the `call` method **`global`** — `global with sharing class <stepName> implements Callable { global Object call(...) }`. This is REQUIRED, not stylistic: on a managed-INSTALLED org the packaged `CallableManager` resolves/invokes this subscriber class across the namespace boundary via `Type.forName`, which cannot see a `public` class — the step then fails at *runtime* with `Type "<stepName>" does not exist` and the transaction-log row lands in `Error` (the class still shows `Active`, so this is invisible until an ingest runs). `public` only works on a same-namespace source/dev org (e.g. a `cgcloud_dev` scratch org); `global` works on both. Inner exception types may stay `public`.
33
+ - `implements Callable`; single `Object call(String action, Map<String, Object> context)` method.
34
+ - Guard `action != <actionName>` with a class-specific exception.
35
+ - Read `context.get('currentInput')` (Map) and `context.get('currentOutput')` (SObject) — these are the keys `BoTransformationEngine` seeds (verified at `BoTransformationEngine.cls:825-864`).
36
+ - Compute the namespace prefix from `String.valueOf(output.getSObjectType())` at runtime (see the reference example for the exact substring logic). Never emit a compile-time `<ns>__` prefix.
37
+ - For each `outputWrite`, resolve the source value from `currentInput` (via the `inputPaths` map), apply any preconditions, then call `output.put(prefix + '<field>', ...)`.
38
+ - Return `null`.
39
+
40
+ Materialize the generated source into a **project-relative** working tree —
41
+ never `/tmp`. Generated deployment artifacts must stay inside the project so
42
+ the workflow is portable and auditable (add `./.promotion-bo-api-deploy/` to
43
+ `.gitignore`; it is ephemeral scratch):
44
+
45
+ ```text
46
+ ./.promotion-bo-api-deploy/
47
+ sfdx-project.json
48
+ force-app/main/default/classes/
49
+ <stepName>.cls
50
+ <stepName>.cls-meta.xml
51
+ ```
52
+
53
+ `sfdx-project.json`:
54
+ ```json
55
+ {
56
+ "packageDirectories": [{ "path": "force-app", "default": true }],
57
+ "sourceApiVersion": "60.0"
58
+ }
59
+ ```
60
+
61
+ `<stepName>.cls-meta.xml`:
62
+ ```xml
63
+ <?xml version="1.0" encoding="UTF-8"?>
64
+ <ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
65
+ <apiVersion>60.0</apiVersion>
66
+ <status>Active</status>
67
+ </ApexClass>
68
+ ```
69
+
70
+ 4. **Show the user the generated class + confirm.** Print the full
71
+ `<stepName>.cls` body and ask "Deploy this? [y/N]". `n` → save to
72
+ `./.promotion-bo-api-deploy/` and exit; the user can hand-edit and re-run
73
+ with `--interview-file` pointing at their tweaked answers, or drop their own
74
+ `.cls` into the temp tree before re-invoking. `y` → deploy.
75
+
76
+ 5. **Deploy:**
77
+
78
+ ```bash
79
+ sf project deploy start \
80
+ --target-org <alias> \
81
+ --source-dir ./.promotion-bo-api-deploy/force-app \
82
+ --wait 10 --json
83
+ ```
84
+
85
+ On `--dry-run`, add `--dry-run` (fall back to `--check-only` on older CLIs).
86
+ Non-success → stop, print `result.details.componentFailures`.
87
+
88
+ ## Phase 5b — Register + wire the step (into the user-chosen workflows)
89
+
90
+ Insert one `${PREFIX_UNDER}BO_API_Workflow_Step__c` record for the generated
91
+ class, then one `${PREFIX_UNDER}BO_API_Workflow_Workflow_Step__c` junction per
92
+ **`workflows`** entry from the interview (subset of `{create, update, copy}` —
93
+ never assume all three).
94
+
95
+ Rationale (do not skip):
96
+ - Store the **bare** class name (`<stepName>`) in `Classname__c`. The packaged `CallableManager` resolves it against the caller's namespace, so `Classname__c` never carries the `cgcloud`/`cgcloud_dev` prefix. (Every shipped seed row uses a bare name.)
97
+ - Compute `Sort__c` per workflow. If the interview supplied a `sortAfter` step name, resolve its junction row for the target workflow and use `sortAfter.Sort__c + 5`. Otherwise use `max(existing sort in that workflow) + 10`.
98
+ - `derive` is never wired by this skill. If the user selected `derive`, print "the `derive` workflow is out of scope for this skill — a follow-up skill will handle it" and drop it.
99
+
100
+ Do the whole thing in a single anonymous-Apex transaction to avoid partial
101
+ state. Placeholders in `<...>` come from the Phase 5 interview:
102
+
103
+ ```apex
104
+ // ./.promotion-bo-api-deploy/register-step.apex
105
+ String salesOrg = '<salesOrg>';
106
+ String stepName = '<stepName>'; // e.g. 'SetCommentValue'
107
+ String actionName = '<actionName>'; // e.g. 'setCommentValue'
108
+ String entityName = '<entity>'; // e.g. 'Tactic'
109
+ String description = '<description>';
110
+ List<String> workflows = new List<String>{ <workflows-as-quoted-csv> }; // e.g. 'create','update','copy'
111
+ List<String> inputPaths = new List<String>{ <inputPaths-as-quoted-csv> }; // e.g. '.Comment' from the interview inputPaths
112
+ String inputPathType = '<inputPathType>'; // one of: String, Array, Boolean, Number, Object (SIS RecordType)
113
+
114
+ // Query the entity id (packaged)
115
+ List<${PREFIX_DOT}BO_API_Entity__c> entity = [
116
+ SELECT Id FROM ${PREFIX_DOT}BO_API_Entity__c
117
+ WHERE Name = :entityName
118
+ LIMIT 1
119
+ ];
120
+ if (entity.isEmpty()) {
121
+ throw new CGCloudException(entityName + ' BO API entity not found for sales org ' + salesOrg);
122
+ }
123
+
124
+ // Upsert the step (Unique_Key__c is a writable external id — caller must set it)
125
+ ${PREFIX_DOT}BO_API_Workflow_Step__c step = new ${PREFIX_DOT}BO_API_Workflow_Step__c(
126
+ Name = stepName,
127
+ ${PREFIX_DOT}Classname__c = stepName,
128
+ ${PREFIX_DOT}Method__c = actionName,
129
+ ${PREFIX_DOT}Sales_Org__c = salesOrg,
130
+ ${PREFIX_DOT}Entity__c = entity[0].Id,
131
+ ${PREFIX_DOT}Description__c = description,
132
+ ${PREFIX_DOT}Unique_Key__c = stepName + salesOrg
133
+ );
134
+ upsert step ${PREFIX_DOT}Unique_Key__c;
135
+
136
+ // Ensure entity is in each workflow's WE list before inserting junctions
137
+ // (validation rule requires it — copy fails otherwise)
138
+ Map<String, Id> wfByName = new Map<String, Id>();
139
+ for (${PREFIX_DOT}BO_API_Workflow__c wf : [
140
+ SELECT Id, Name FROM ${PREFIX_DOT}BO_API_Workflow__c
141
+ WHERE Name IN :workflows
142
+ AND ${PREFIX_DOT}BO_API__r.Name = 'Promotion'
143
+ AND ${PREFIX_DOT}BO_API__r.${PREFIX_DOT}Sales_Org__c = :salesOrg
144
+ ]) {
145
+ wfByName.put(wf.Name, wf.Id);
146
+ }
147
+ for (String wf : workflows) {
148
+ if (!wfByName.containsKey(wf)) {
149
+ throw new CGCloudException('BO API workflow "' + wf + '" not found for sales org ' + salesOrg);
150
+ }
151
+ Integer existing = [
152
+ SELECT COUNT() FROM ${PREFIX_DOT}BO_API_Workflow_Entity__c
153
+ WHERE ${PREFIX_DOT}BO_API_Workflow__c = :wfByName.get(wf)
154
+ AND ${PREFIX_DOT}BO_API_Entity__c = :entity[0].Id
155
+ ];
156
+ if (existing == 0) {
157
+ insert new ${PREFIX_DOT}BO_API_Workflow_Entity__c(
158
+ ${PREFIX_DOT}BO_API_Workflow__c = wfByName.get(wf),
159
+ ${PREFIX_DOT}BO_API_Entity__c = entity[0].Id
160
+ );
161
+ }
162
+ }
163
+
164
+ // Wire junctions into the interview's chosen workflows only
165
+ for (String wf : workflows) {
166
+ AggregateResult ar = [
167
+ SELECT MAX(${PREFIX_DOT}Sort__c) maxSort
168
+ FROM ${PREFIX_DOT}BO_API_Workflow_Workflow_Step__c
169
+ WHERE ${PREFIX_DOT}BO_API_Workflow__c = :wfByName.get(wf)
170
+ ];
171
+ Decimal maxSort = (Decimal) ar.get('maxSort');
172
+ Decimal nextSort = (maxSort == null ? 10 : maxSort + 10);
173
+ // If interview supplied sortAfter, resolve that here and use sortAfter.Sort__c + 5 instead.
174
+
175
+ ${PREFIX_DOT}BO_API_Workflow_Workflow_Step__c junction = new ${PREFIX_DOT}BO_API_Workflow_Workflow_Step__c(
176
+ ${PREFIX_DOT}BO_API_Workflow__c = wfByName.get(wf),
177
+ ${PREFIX_DOT}BO_API_Workflow_Step__c = step.Id,
178
+ ${PREFIX_DOT}Enabled__c = true,
179
+ ${PREFIX_DOT}Sort__c = nextSort,
180
+ ${PREFIX_DOT}Unique_Key__c = String.valueOf(wfByName.get(wf)) + stepName + salesOrg
181
+ );
182
+ upsert junction ${PREFIX_DOT}Unique_Key__c;
183
+ }
184
+
185
+ // Register accepted input paths (SIS rows). Required — without these,
186
+ // the payload validator rejects with "Properties other than those defined
187
+ // were not expected at #.<path>". SIS has no external id → insert with
188
+ // pre-check. SIS uses RecordType to declare the JSON type (String, Array,
189
+ // Boolean, Number, Object) — a missing RecordTypeId defaults to Array,
190
+ // which produces "Expected List<Object>" at runtime.
191
+ Id sisRecordTypeId = [
192
+ SELECT Id FROM RecordType
193
+ WHERE SobjectType = '${PREFIX_UNDER}BO_API_Step_Input_Structure__c'
194
+ AND DeveloperName = :inputPathType
195
+ LIMIT 1
196
+ ].Id;
197
+ Set<String> existingPaths = new Set<String>();
198
+ for (${PREFIX_DOT}BO_API_Step_Input_Structure__c s : [
199
+ SELECT ${PREFIX_DOT}Path__c FROM ${PREFIX_DOT}BO_API_Step_Input_Structure__c
200
+ WHERE ${PREFIX_DOT}BO_API_Workflow_Step__c = :step.Id
201
+ ]) {
202
+ existingPaths.add(String.valueOf(s.get('${PREFIX_DOT}Path__c')));
203
+ }
204
+ List<${PREFIX_DOT}BO_API_Step_Input_Structure__c> sisRows =
205
+ new List<${PREFIX_DOT}BO_API_Step_Input_Structure__c>();
206
+ for (String inputPath : inputPaths) {
207
+ if (existingPaths.contains(inputPath)) continue;
208
+ sisRows.add(new ${PREFIX_DOT}BO_API_Step_Input_Structure__c(
209
+ ${PREFIX_DOT}BO_API_Workflow_Step__c = step.Id,
210
+ ${PREFIX_DOT}Path__c = inputPath,
211
+ RecordTypeId = sisRecordTypeId
212
+ ));
213
+ }
214
+ if (!sisRows.isEmpty()) insert sisRows;
215
+ ```
216
+
217
+ `PREFIX_DOT` above is the placeholder the skill textually replaces before
218
+ writing the file to `./.promotion-bo-api-deploy/`. In the final on-disk file every `${PREFIX_DOT}`
219
+ becomes either `cgcloud.` / `cgcloud_dev.` / empty, and every
220
+ `${PREFIX_DOT}Sales_Org__c`-style token becomes `cgcloud__Sales_Org__c` /
221
+ `cgcloud_dev__Sales_Org__c` / bare.
222
+
223
+ Run:
224
+
225
+ ```bash
226
+ sf apex run --target-org <alias> --file ./.promotion-bo-api-deploy/register-step.apex
227
+ ```
228
+
229
+ Non-zero exit or non-`Compiled: true, Success: true` → stop, print output.
230
+
231
+ Re-run behaviour: both upserts are keyed on `Unique_Key__c` (writable external
232
+ id set from stable interview inputs), so a second run of Phase 5b is a no-op if
233
+ the answers are unchanged. Reducing the `workflows` set across runs does **not**
234
+ retire junctions the previous run created — the skill only adds. Print a warning
235
+ if Phase 2 shows a junction for `<stepName>` on a workflow no longer in the
236
+ interview list; a human should decide whether to disable it.
@@ -0,0 +1,132 @@
1
+ # Reference example — `SetCommentValue`
2
+
3
+ One worked customization the skill ships as a preset. It exists so a reviewer can
4
+ see a complete, verified answer to the Phase 5 interview. The skill does not
5
+ deploy it by default — the customer picks it explicitly via
6
+ `--preset set-comment-value` or by saying "use the SetCommentValue reference"
7
+ during the interview. All files below ship under
8
+ `assets/set-comment-value/`.
9
+
10
+ ## Interview answers
11
+
12
+ ```json
13
+ {
14
+ "stepName": "SetCommentValue",
15
+ "entity": "Tactic",
16
+ "workflows": ["create", "update", "copy"],
17
+ "actionName": "setCommentValue",
18
+ "inputPaths": ["Comment"],
19
+ "outputWrites": [
20
+ { "field": "Comment__c", "source": "Comment" }
21
+ ],
22
+ "description": "Copies the tactic input Comment to Tactic.Comment__c.",
23
+ "preconditions": null,
24
+ "sortAfter": null
25
+ }
26
+ ```
27
+
28
+ Ships at `assets/set-comment-value/interview-answers.json`.
29
+
30
+ ## Generated class
31
+
32
+ The class is namespace-agnostic — it works whether the target org has `cgcloud`,
33
+ `cgcloud_dev`, or no prefix, because it derives the Tactic namespace from the
34
+ passed SObject at runtime and never types `<ns>__Tactic__c` or `<ns>__Comment__c`
35
+ at compile time. Context-key names (`currentInput`, `currentOutput`) are the ones
36
+ the packaged `BoTransformationEngine` seeds — verified at lines 825–864 of
37
+ `BoTransformationEngine.cls`. Ships at
38
+ `assets/set-comment-value/SetCommentValue.cls`.
39
+
40
+ ```apex
41
+ /**
42
+ * Custom BO API Workflow Step: lifts a top-level `Comment` value out of
43
+ * the ingest tactic input onto Tactic.Comment__c.
44
+ *
45
+ * Registered against the `Tactic` BO API entity and wired into the
46
+ * `create`, `update`, and `copy` workflows.
47
+ */
48
+ global with sharing class SetCommentValue implements Callable {
49
+
50
+ public class SetCommentValueException extends Exception {}
51
+
52
+ // global (not public): the packaged CallableManager must resolve/invoke this
53
+ // subscriber Callable across the namespace boundary on a managed-installed org
54
+ // (a public class fails at runtime with `Type "SetCommentValue" does not exist`).
55
+ global Object call(String action, Map<String, Object> context) {
56
+ if (action != 'setCommentValue') {
57
+ throw new SetCommentValueException('Unsupported action: ' + action);
58
+ }
59
+ if (context == null) {
60
+ throw new SetCommentValueException('null context');
61
+ }
62
+
63
+ Object rawInput = context.get('currentInput');
64
+ Object rawOutput = context.get('currentOutput');
65
+
66
+ if (!(rawInput instanceof Map<String, Object>)) {
67
+ return null;
68
+ }
69
+ Map<String, Object> tacticInput = (Map<String, Object>) rawInput;
70
+ Object commentValue = tacticInput.get('Comment');
71
+ if (commentValue == null) {
72
+ return null;
73
+ }
74
+
75
+ // The engine wraps Tactic__c in TPM_Promotion.TacticRecord (see
76
+ // BoTransformationEngineEntityTree.generateObject:696 —
77
+ // promo.addTactic(new Tactic__c()) returns a TacticRecord wrapper).
78
+ // Unwrap to the underlying SObject via getRecord() before writing;
79
+ // a raw SObject cast fails silently and the write never persists.
80
+ SObject tactic;
81
+ if (rawOutput instanceof SObject) {
82
+ tactic = (SObject) rawOutput;
83
+ } else if (rawOutput instanceof TPM_Promotion.Record) {
84
+ tactic = ((TPM_Promotion.Record) rawOutput).getRecord();
85
+ } else {
86
+ return null;
87
+ }
88
+
89
+ String tacticType = String.valueOf(tactic.getSObjectType());
90
+ String prefix = '';
91
+ if (tacticType.contains('__') && tacticType.endsWith('__c')) {
92
+ Integer firstUnderscoreUnderscore = tacticType.indexOf('__');
93
+ Integer lastUnderscoreUnderscore = tacticType.lastIndexOf('__');
94
+ if (firstUnderscoreUnderscore < lastUnderscoreUnderscore) {
95
+ prefix = tacticType.substring(0, firstUnderscoreUnderscore) + '__';
96
+ }
97
+ }
98
+
99
+ tactic.put(prefix + 'Comment__c', String.valueOf(commentValue));
100
+ return null;
101
+ }
102
+ }
103
+ ```
104
+
105
+ ## Class metadata
106
+
107
+ Ships at `assets/set-comment-value/SetCommentValue.cls-meta.xml`.
108
+
109
+ ```xml
110
+ <?xml version="1.0" encoding="UTF-8"?>
111
+ <ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
112
+ <apiVersion>60.0</apiVersion>
113
+ <status>Active</status>
114
+ </ApexClass>
115
+ ```
116
+
117
+ ## Reference payloads
118
+
119
+ Live at `assets/set-comment-value/payloads/`:
120
+ - `create.json` — sets `Comment` on a new promotion's tactic.
121
+ - `update.json` — updates the tactic's `Comment`.
122
+ - `copy.json` — copies the source promotion; expects the tactic's `Comment` to be carried across.
123
+
124
+ Each carries the `REPLACED-AT-RUNTIME` marker for `importId` (and, for
125
+ `update`/`copy`, marker ids for the promotion + tactic captured from the
126
+ preceding response).
127
+
128
+ ## Expected write
129
+
130
+ `Tactic__c.Comment__c` on the created / updated / copied promotion's tactic must
131
+ equal the `Comment` value from the corresponding payload — this is the
132
+ interview's single `outputWrites` assertion.