@salesforce/afv-skills 1.66.0 → 1.67.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 (53) hide show
  1. package/package.json +2 -4
  2. package/skills/agentforce-offline-experimentation-run/SKILL.md +13 -28
  3. package/skills/commerce-react-storefront-create/SKILL.md +350 -0
  4. package/skills/consumer-goods-promotion-bo-api-deploy/SKILL.md +2 -2
  5. package/skills/consumer-goods-promotion-bo-api-deploy/references/generate-and-wire.md +28 -16
  6. package/skills/consumer-goods-promotion-bo-api-deploy/references/smoke-and-verify.md +18 -1
  7. package/skills/education-cloud-domain-configure/SKILL.md +21 -23
  8. package/skills/education-cloud-domain-configure/references/dependent-features.md +9 -0
  9. package/skills/education-cloud-domain-configure/references/domain-features.md +15 -0
  10. package/skills/education-cloud-domain-configure/references/gotchas-extended.md +8 -0
  11. package/skills/education-cloud-domain-configure/references/setup-widgets.md +123 -0
  12. package/skills/experience-search-coordinate/SKILL.md +2 -2
  13. package/skills/experience-search-coordinate/references/media-route.md +30 -4
  14. package/skills/experience-ui-bundle-features-generate/SKILL.md +1 -1
  15. package/skills/experience-ui-bundle-mfa-configure/SKILL.md +1 -1
  16. package/skills/platform-metadata-deploy/README.md +1 -1
  17. package/skills/platform-metadata-deploy/SKILL.md +114 -15
  18. package/skills/platform-metadata-deploy/references/agent-deployment-guide.md +3 -4
  19. package/skills/platform-metadata-deploy/references/orchestration.md +2 -3
  20. package/skills/platform-value-set-generate/SKILL.md +19 -19
  21. package/skills/platform-widget-generate/SKILL.md +25 -51
  22. package/skills/platform-widget-generate/examples/formulas.json +19 -2
  23. package/skills/platform-widget-generate/examples/localized-labels.json +57 -0
  24. package/skills/platform-widget-generate/references/widget-actions.md +29 -0
  25. package/skills/platform-widget-generate/references/widget-bundle-validation.md +1 -0
  26. package/skills/platform-widget-generate/references/widget-formulas.md +24 -7
  27. package/skills/platform-widget-generate/references/widget-gotchas.md +10 -0
  28. package/skills/platform-widget-generate/references/widget-labels.md +78 -0
  29. package/skills/platform-widget-generate/references/widget-styling.md +12 -0
  30. package/skills/sales-call-scoring-configure/SKILL.md +17 -10
  31. package/skills/sales-call-scoring-configure/scripts/enable-call-scoring.sh +55 -33
  32. package/skills/service-digital-engagement-channel-configure/SKILL.md +4 -2
  33. package/skills/service-digital-engagement-channel-configure/references/channel_settings.md +13 -2
  34. package/skills/service-digital-engagement-deployment-configure/SKILL.md +37 -100
  35. package/skills/service-digital-engagement-deployment-configure/assets/esd_web_update_template.xml +83 -8
  36. package/skills/service-digital-engagement-deployment-configure/examples/esd_web_prechat_fields.xml +74 -0
  37. package/skills/service-digital-engagement-deployment-configure/references/connect_api_creation.md +13 -3
  38. package/skills/service-digital-engagement-deployment-configure/references/deployment_settings.md +13 -78
  39. package/skills/service-digital-engagement-deployment-configure/references/onboarding_contract.md +5 -6
  40. package/skills/service-digital-engagement-deployment-configure/references/prechat_forms.md +245 -0
  41. package/skills/service-itsm-agentic-setup-incident-management/SKILL.md +164 -120
  42. package/skills/service-itsm-agentic-setup-incident-management/examples/output-templates.md +61 -37
  43. package/skills/service-itsm-agentic-setup-incident-management/references/audit-tracking.md +42 -0
  44. package/skills/service-itsm-agentic-setup-incident-management/references/broadcast-communications.md +23 -0
  45. package/skills/service-itsm-agentic-setup-incident-management/references/email-to-incident.md +23 -0
  46. package/skills/service-itsm-agentic-setup-incident-management/references/incident-dashboards.md +25 -0
  47. package/skills/service-itsm-agentic-setup-incident-management/references/incident-persona-psg.md +30 -146
  48. package/skills/service-itsm-agentic-setup-incident-management/references/incident-preferences.md +32 -136
  49. package/skills/service-itsm-agentic-setup-incident-management/references/major-incident-management.md +32 -155
  50. package/skills/service-itsm-agentic-setup-incident-management/references/manage-incidents-with-ai.md +45 -0
  51. package/skills/service-itsm-agentic-setup-incident-management/references/priority-matrix.md +32 -0
  52. package/skills/service-itsm-agentic-setup-incident-management/references/service-mgmt-privilege.md +30 -140
  53. package/skills/service-itsm-agentic-setup-incident-management/references/sla-milestones.md +110 -0
@@ -0,0 +1,57 @@
1
+ {
2
+ "type": "lightning__agentforceWidget",
3
+ "contentBody": {
4
+ "widgetBody": {
5
+ "definition": "tile/widget",
6
+ "children": [
7
+ {
8
+ "definition": "tile/column",
9
+ "attributes": { "gap": "md" },
10
+ "children": [
11
+ {
12
+ "definition": "tile/text",
13
+ "attributes": { "text": "{!$Label.c.OrderStatus_Heading}", "variant": "h1" }
14
+ },
15
+ {
16
+ "definition": "tile/row",
17
+ "attributes": { "gap": "sm", "align": "center" },
18
+ "children": [
19
+ {
20
+ "definition": "tile/text",
21
+ "attributes": { "text": "{!$Label.c.OrderStatus_OrderNumberCaption}", "variant": "caption" }
22
+ },
23
+ {
24
+ "definition": "tile/text",
25
+ "attributes": { "text": "{!$attrs.orderNumber}" }
26
+ }
27
+ ]
28
+ },
29
+ {
30
+ "definition": "tile/badge",
31
+ "attributes": {
32
+ "label": "{!IF($attrs.isShipped, $Label.c.OrderStatus_ShippedStatus, $Label.c.OrderStatus_ProcessingStatus)}",
33
+ "variant": "{!IF($attrs.isShipped, 'success', 'info')}"
34
+ }
35
+ },
36
+ {
37
+ "definition": "tile/text",
38
+ "attributes": { "text": "{!$Label.c.OrderStatus_ItemsPrefix & ' ' & TEXT($attrs.itemCount)}" }
39
+ },
40
+ {
41
+ "definition": "tile/button",
42
+ "attributes": {
43
+ "label": "{!$Label.c.OrderStatus_TrackButton}",
44
+ "variant": "primary",
45
+ "actions": {
46
+ "click": [
47
+ { "definition": "action/openLink", "attributes": { "url": "{!$attrs.trackingUrl}" } }
48
+ ]
49
+ }
50
+ }
51
+ }
52
+ ]
53
+ }
54
+ ]
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,29 @@
1
+ # Widget Actions
2
+
3
+ Complete `tile/button` examples for the two supported action definitions. The action table and the action-less-button rule live in SKILL.md *Actions*.
4
+
5
+ ---
6
+
7
+ ## `action/sendMessage` and `action/openLink`
8
+
9
+ ```json
10
+ {
11
+ "definition": "tile/button",
12
+ "attributes": {
13
+ "label": "Button Label",
14
+ "variant": "primary",
15
+ "actions": { "click": [ { "definition": "action/sendMessage", "attributes": { "content": "content" } } ] }
16
+ }
17
+ }
18
+ ```
19
+
20
+ ```json
21
+ {
22
+ "definition": "tile/button",
23
+ "attributes": {
24
+ "label": "Button Label",
25
+ "variant": "primary",
26
+ "actions": { "click": [ { "definition": "action/openLink", "attributes": { "url": "https://www.example.com" } } ] }
27
+ }
28
+ }
29
+ ```
@@ -12,3 +12,4 @@ Report each check individually as `pass` or `fail (<reason>)`.
12
12
  - **`metaxml-elements`** — `<widgetName>.uiwidget-meta.xml` has root `<UiWidgetBundle>` and contains `<masterLabel>` (non-empty), `<description>` (non-empty), and `<widgetType>JSON</widgetType>`.
13
13
  - **`files-present`** — all three files exist at the resolved `<pkgDir>/uiWidgets/<widgetName>/` path.
14
14
  - **`button-actions-present`** — every `tile/button` in `<widgetName>.json` has an `actions.click` action node whose `definition` is `action/openLink` or `action/sendMessage`.
15
+ - **`labels-valid`** — if the user did not ask for localization, `<widgetName>.json` contains no `$Label` (report `pass (not requested)`). Otherwise every `$Label` follows the rules in `references/widget-labels.md` and appears in the step 10 report.
@@ -17,6 +17,7 @@ Every formula expression is wrapped in `{!...}`, similar to a plain binding:
17
17
  - Functions and operators are case-sensitive UPPERCASE.
18
18
  - A formula can nest functions and mix functions with operators: `{!TEXT(ROUND($attrs.property, 2))}`, `{!LEFT($attrs.property, 5) & "..."}`.
19
19
  - A formula-bearing attribute is still bound to schema properties the same way as a plain binding — every `$attrs.X` / `$item.X` referenced inside a formula must resolve to a real schema property or loop variable, per the `bindings-resolve` self-validation check.
20
+ - A Custom Label reference `$Label.c.<LabelName>` can be used as a text value inside a formula. See `references/widget-labels.md`.
20
21
 
21
22
  ---
22
23
 
@@ -34,7 +35,7 @@ Only the operators/functions listed here are confirmed supported — do not inve
34
35
  | `/` | Divide | `$attrs.operand1 / $attrs.operand2` |
35
36
  | `^` | Exponent | `$attrs.operand1 ^ $attrs.operand2` |
36
37
 
37
- Standard precedence applies (`^` > `*`/`/` > `+`/`-`); parenthesize to be explicit, especially when combining multiple arithmetic operators.
38
+ Unary minus (`-$attrs.fieldName`) is also supported. Precedence, lowest to highest: `||`, `&&`, equality, relational, `+` `-` `&`, `^`, `*` `/`, unary. Note that `*` and `/` bind tighter than `^`, so `2 * 3 ^ 2` is `(2 * 3) ^ 2`; parenthesize to be explicit.
38
39
 
39
40
  ### 2. Comparison operators
40
41
 
@@ -47,36 +48,40 @@ Return `true`/`false`; commonly used inside `IF()` or `meta.if`.
47
48
  | `>` / `<` | Greater / less than | `$attrs.property > 1000` |
48
49
  | `>=` / `<=` | Greater-or-equal / less-or-equal | `$attrs.property1 >= $attrs.property2` |
49
50
 
51
+ `==` and `!=` are accepted as aliases for `=` and `<>`. Both operands must have the same type; text is never compared with a number.
52
+
50
53
  ### 3. Logical functions
51
54
 
52
55
  | Function | Description | Example |
53
56
  |---|---|------------------------------------------------------------------------------------------------------|
54
- | `IF(cond, then, else)` | Conditional value; nests for multi-branch logic | `IF($attrs.property > value, 'result1', 'result2')` |
57
+ | `IF(cond, then, else)` | Conditional value; nests for multi-branch logic. `cond` must be a boolean (`IF($attrs.count > 0, …)`, not `IF($attrs.count, …)`) and both branches must have the same type | `IF($attrs.property > value, 'result1', 'result2')` |
55
58
  | `CASE(expr, val1, res1, val2, res2, ..., default)` | Multi-way match, cleaner than nested `IF` for 3+ branches | `CASE($attrs.property, 'value1', 'result1', 'value2', 'result2', 'value3', 'result3', 'defaultResult')` |
56
59
  | `AND(...)` | All conditions true | `AND($attrs.property1, $attrs.property2 > value)` |
57
60
  | `OR(...)` | Any condition true | `OR($attrs.property1, $attrs.property2 > value)` |
58
61
  | `NOT(val)` | Negate a boolean | `NOT($attrs.property)` |
59
- | `ISNULL(val)` | True if the value is null | `IF(ISNULL($attrs.property), 'null', 'has-value')` |
62
+ | `&&` / `\|\|` / `!` (operators) | Aliases for `AND` / `OR` / `NOT` | `$attrs.property1 && !$attrs.property2` |
63
+ | `ISNULL(val)` | True if the value is null. Always false for text — use `ISBLANK` for strings | `IF(ISNULL($attrs.property), 'null', 'has-value')` |
60
64
  | `ISBLANK(val)` | True if null or empty string | `IF(ISBLANK($attrs.property), 'blank', 'has-value')` |
61
65
 
62
66
  ### 4. Text functions
63
67
 
64
68
  | Function | Description | Example |
65
69
  |---|---|-----------------------------------------------------|
66
- | `UPPER(str)` / `LOWER(str)` | Case conversion | `UPPER($attrs.property)` |
70
+ | `UPPER(str[, locale])` / `LOWER(str[, locale])` | Case conversion, optionally for a locale such as `'tr'` | `UPPER($attrs.property)` |
67
71
  | `LEFT(str, n)` / `RIGHT(str, n)` | First/last `n` characters | `LEFT($attrs.property, 3)` |
68
72
  | `MID(str, start, len)` | Substring, **1-based** start index (first character is position 1, not 0) | `MID($attrs.property, 1, 3)` |
73
+ | `SUBSTR(str, start[, len])` | Substring with a **1-based** start; a negative `start` counts from the end; without `len`, runs to the end | `SUBSTR($attrs.property, -4)` |
69
74
  | `LEN(val)` | Length of a string | `LEN($attrs.property)` |
70
75
  | `TRIM(str)` | Strip leading/trailing whitespace | `TRIM($attrs.property)` |
71
76
  | `REVERSE(str)` | Reverse a string | `REVERSE($attrs.property)` |
72
77
  | `SUBSTITUTE(str, old, new)` | Replace all occurrences | `SUBSTITUTE($attrs.property, 'string1', 'string2')` |
73
- | `FIND(search, str)` | 1-based index of `search` in `str`, or falsy if absent | `FIND('string', $attrs.property)` |
78
+ | `FIND(search, str[, start])` | 1-based index of `search` in `str`, or `0` if absent; optional 1-based `start` position | `FIND('string', $attrs.property)` |
74
79
  | `LPAD(str, len, pad)` / `RPAD(str, len, pad)` | Pad to a fixed width | `LPAD($attrs.property, 12, '.')` |
75
80
  | `TEXT(val)` | Coerce a non-string value to text (commonly paired with `ROUND`) | `TEXT(ROUND($attrs.property, 2))` |
76
81
  | `VALUE(str)` | Parse text to a number (inverse of `TEXT`) | `VALUE($attrs.property)` |
77
82
  | `CONTAINS(str, sub)` | String contains substring — boolean, pair with `IF` | `CONTAINS($attrs.property, 'string')` |
78
83
  | `BEGINS(str, prefix)` | String starts with — boolean, pair with `IF` | `BEGINS($attrs.property, 'string')` |
79
- | `&` (operator) | Concatenate strings | `$attrs.property1 & ' — ' & $attrs.property2` |
84
+ | `&` (operator) | Concatenate strings. Both sides must be text, so wrap numbers and dates in `TEXT()` — `'Total: ' & TEXT($attrs.total)` | `$attrs.property1 & ' — ' & $attrs.property2` |
80
85
 
81
86
  ### 5. Math functions
82
87
 
@@ -99,7 +104,7 @@ Return `true`/`false`; commonly used inside `IF()` or `meta.if`.
99
104
  | `DAY(date)` / `MONTH(date)` / `YEAR(date)` | Extract date components | `DAY(DATEVALUE($attrs.fieldName))` |
100
105
  | `WEEKDAY(date)` | Day of week, `1`=Sunday … `7`=Saturday | `WEEKDAY(DATEVALUE($attrs.fieldName))` |
101
106
  | `ADDMONTHS(date, n)` | Add `n` whole months to a date | `ADDMONTHS(DATEVALUE($attrs.fieldName), 3)` |
102
- | `TODAY()` / `NOW()` / `TIMENOW()` | Current server date / datetime / time — no arguments | `TODAY()` |
107
+ | `TODAY()` / `NOW()` / `TIMENOW()` | Current date / datetime / time in UTC — no arguments | `TODAY()` |
103
108
  | `DATE(year, month, day)` | Construct a date literal | `DATE(2026, 12, 31)` |
104
109
  | `HOUR(dt)` / `MINUTE(dt)` / `SECOND(dt)` / `MILLISECOND(dt)` | Extract time components from a datetime | `HOUR(NOW())` |
105
110
 
@@ -121,3 +126,15 @@ A raw date/datetime **string** attribute (e.g. an ISO string from `$attrs`) must
121
126
  | `ISNUMBER(val)` | True if the value parses as a number | `ISNUMBER($attrs.fieldName)` |
122
127
  | `REGEX(str, pattern)` | True if `str` matches the regex `pattern` (standard regex string) | `REGEX($attrs.fieldName, '^[A-Z]{2} [0-9]+$')` |
123
128
 
129
+ ### 9. Locale formatting functions
130
+
131
+ Format values for the viewer's locale. A null or blank input returns null. The optional last argument is a decimal count for the number functions and `'short'` (default), `'medium'` or `'long'` for the date and time functions.
132
+
133
+ | Function | Description | Example |
134
+ |---|---|---|
135
+ | `FORMATNUMBER(num[, decimals])` | Locale-formatted number | `FORMATNUMBER($attrs.fieldName, 2)` |
136
+ | `FORMATPERCENT(num[, decimals])` | Locale-formatted percent (`0.25` → `25%`) | `FORMATPERCENT($attrs.fieldName, 1)` |
137
+ | `FORMATCURRENCY(num)` | Currency code followed by the amount, e.g. `USD 1,234.50` (no currency symbol) | `FORMATCURRENCY($attrs.fieldName)` |
138
+ | `FORMATDATE(date[, style])` | Locale date | `FORMATDATE($attrs.fieldName, 'medium')` |
139
+ | `FORMATDATETIME(dt[, style])` | Locale date and time | `FORMATDATETIME($attrs.fieldName, 'long')` |
140
+ | `FORMATTIME(time[, style])` | Locale time | `FORMATTIME($attrs.fieldName)` |
@@ -0,0 +1,10 @@
1
+ # Widget Generation Gotchas
2
+
3
+ | Issue | Resolution |
4
+ |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
5
+ | `getUiComponentSchemas` returns a partial-failure entry | Pick a different block from `discoverUiComponents`; do not silently continue without a schema |
6
+ | Body references `{!$attrs.foo}` but `foo` is not under `schema.json` `properties.attributes.properties` | Add `foo` to `schema.json` `properties.attributes.properties` OR remove the body reference |
7
+ | Output written outside `<pkgDir>/uiWidgets/<widgetName>/` | `<pkgDir>` = `<packageDirectories[].path>/main/default` (see `references/widget-bundle-layout.md`). Dropping the `main/default/` segment is the common cause of widgets landing at `force-app/uiWidgets/...` instead of `force-app/main/default/uiWidgets/...` |
8
+ | `if` bound directly to a raw string or number relies on truthiness and is unreliable | Bind to a `lightning__booleanType` property, or use a formula comparison that evaluates to boolean |
9
+ | `tile/button` renders but does nothing when clicked | No `actions.click` entry was set — an action-less button renders disabled by design. Add one (see SKILL.md *Actions*) |
10
+ | Using `action/sendMessage` for pure navigation, or `action/openLink` when the agent should respond | `action/openLink` is synchronous and does not consume a turn; `action/sendMessage` is asynchronous and earns a fresh turn. Pick the one matching the intended UX |
@@ -0,0 +1,78 @@
1
+ # Widget Localization with Custom Labels
2
+
3
+ A widget shows translated text by referencing a Salesforce Custom Label instead of a hardcoded string.
4
+
5
+ ---
6
+
7
+ ## Syntax
8
+
9
+ ```text
10
+ {!$Label.c.<LabelName>}
11
+ ```
12
+
13
+ - `c` is the org's default namespace. Always write it.
14
+ - The label name is static. Never build it from data — `{!$Label.c.$attrs.key}` can't be validated when the widget is saved.
15
+ - A `$Label` reference is not a `$attrs` binding and has no entry in `schema.json`.
16
+
17
+ ---
18
+
19
+ ## What to localize
20
+
21
+ Replace every static, user-visible string in an allowed attribute, unless the user limited the request to specific strings.
22
+
23
+ **Allowed**
24
+
25
+ - Display text: `text`, `label`, `placeholder`, `title`, and other free-form string attributes (per `getUiComponentSchemas`)
26
+ - Text results inside a formula, e.g. the branches of an `IF` or `CASE`
27
+
28
+ **Not allowed**
29
+
30
+ - `meta` directives (`if`, `forEach`, `forItem`)
31
+ - Enum-valued attributes (`variant`, `size`, `color`, `gap`, `align`, `justify`) and icon names
32
+ - Anything inside `actions` (`url`, `target`, `sendMessage` `content`)
33
+ - Data bound with `$attrs` / `$item` — payload values are not translated
34
+ - `schema.json` and the `-meta.xml`
35
+
36
+ ---
37
+
38
+ ## Label names
39
+
40
+ Reuse existing labels first. If the user names labels, or the project's CustomLabels file (`labels/CustomLabels.labels-meta.xml`) already has a label whose value matches the text, reference that label's `fullName` verbatim. Otherwise propose `<WidgetName>_<Role>`.
41
+
42
+ ---
43
+
44
+ ## Combining labels with data
45
+
46
+ - Join a label and data with `&` (see `references/widget-formulas.md`):
47
+ `"text": "{!$Label.c.<LabelName> & ' ' & TEXT($attrs.<attrName>)}"`
48
+ - Choose between labels with `IF` / `CASE`:
49
+ `"label": "{!IF($attrs.<booleanAttr>, $Label.c.<LabelWhenTrue>, $Label.c.<LabelWhenFalse>)}"`
50
+
51
+ ---
52
+
53
+ ## Creating labels
54
+
55
+ This skill gives no guidance on authoring label metadata, and the widget is complete without it. Create labels only when the user asks you to. When you do, add them to the project's existing CustomLabels file instead of overwriting it, and never redefine a label that already exists. The user is responsible for checking the labels and any translations in the org and for deploying them with or before the widget.
56
+
57
+ ---
58
+
59
+ ## Label report
60
+
61
+ End the response with this report:
62
+
63
+ ```markdown
64
+ ### Custom Labels used by this widget
65
+
66
+ These labels must exist in the target org before you deploy — deploying a widget that references a missing label fails.
67
+
68
+ | Label name | Value (default language) | Status | Used in |
69
+ |---|---|---|---|
70
+ | OrderStatus_Heading | Order status | Existing | `tile/text` `text` (widget heading) |
71
+ | OrderStatus_TrackButton | Track order | To create | `tile/button` `label` |
72
+ ```
73
+
74
+ - List each distinct label once, in the order it first appears in the widget.
75
+ - **Label name** is the `fullName`, without the `c.` prefix.
76
+ - **Value** is the literal text the label replaced.
77
+ - **Status** is `Existing` (already in the project or named by the user), `Created` (you wrote it), or `To create` (the user must create it).
78
+ - **Used in** names the block definition and attribute.
@@ -0,0 +1,12 @@
1
+ # Widget Styling Best Practices
2
+
3
+ Widgets express *intent*, not pixels. Each surface provides a default look and feel; brand/theme overrides apply automatically.
4
+
5
+ - **Style semantically.** Use `variant`, `size`, and other enum-typed attributes (`primary`, `destructive`, `success`, `warning`). Do not pin literal colors or pixel values.
6
+ - **One primary action per visible group.** At most one `tile/button` with `variant: primary`. Use `secondary` or `destructive` for additional actions (see the `tile/button` schema for the full variant enum).
7
+ - **Every `tile/button` needs a `click` action.** See SKILL.md *Actions* — an action-less button renders disabled.
8
+ - **One `h1` per widget.** Use `h2`/`h3` for sub-section headings, `body` for prose, `caption` for helper text.
9
+ - **Use semantic state variants on state-bearing blocks** (`tile/badge`, `tile/callout`).
10
+ - **Omit attributes that equal the schema default.** Check each property's `default` in the `getUiComponentSchemas` metadata (step 4). If the value you would write matches it — e.g. `gap`, `size`, or `action/openLink` `target: "_blank"` — leave the attribute out. Set an attribute only to change it.
11
+ - **Don't pin `width`** unless a content constraint requires it.
12
+ - **Use the Lucide icon set.** Pass the Lucide name (`"check"`, `"alert-circle"`); other icon libraries are not supported.
@@ -146,12 +146,19 @@ Classify the org into one of 4 states:
146
146
  ### Phase 3 — Prerequisite enablement (State 2 only)
147
147
 
148
148
  - **License gate (checked first):** query `OrganizationSettingsDetail`
149
- (Tooling API) for the `ECICallScoringAI` OrgPermission. If it isn't
150
- `true`, exit with: "AI call coaching is not available in this org and org
151
- requires Agentforce for Sales license." Non-zero exit. This runs even
152
- when `enableCallCoaching`/`enableECICallScoring` already read favorably,
153
- since those settings don't by themselves confirm the org is licensed.
154
- **Must run before the ECI-off check below** — a genuinely unlicensed org
149
+ (Tooling API) for the `ECICallScoringAI` OrgPermission. Only an **explicit
150
+ `false`** refuses — exit with: "AI call coaching is not available in this
151
+ org and org requires Agentforce for Sales license." (Non-zero exit.) If the
152
+ permission **can't be read** — the query is rejected with `INVALID_TYPE`
153
+ (some org editions, for example Developer Edition) or returns no row — treat
154
+ it as **unknown and proceed**; do NOT fail closed on an unreadable signal.
155
+ The Einstein GenAI `TYPE_UNAVAILABLE` pre-check catches most unlicensed
156
+ orgs; the enable `updateMetadata` write is the final check. This mirrors the Phase 0
157
+ `ConversationPilot` gate, which defers on any non-conclusive read.
158
+ This runs even when `enableCallCoaching`/`enableECICallScoring`
159
+ already read favorably, since those settings don't by themselves confirm the
160
+ org is licensed. **Must run before the ECI-off check below** — a genuinely
161
+ unlicensed org
155
162
  and a licensed-but-toggled-off org both read `enableCallCoaching=false`,
156
163
  so checking ECI first would misdiagnose a licensing gap as a self-serve
157
164
  Setup toggle (the regression the `NoLicense` eval dataset guards against).
@@ -217,7 +224,7 @@ Re-read `enableECICallScoring`. Require `true`. Print SOAP response on failure.
217
224
  is server-side read-only and `updateMetadata` returns
218
225
  `INSUFFICIENT_ACCESS_OR_READONLY`. `enable-call-scoring.sh` maps this to a
219
226
  transient "still finishing turning on — wait a few minutes and re-run"
220
- message rather than a terminal failure (W-23968462).
227
+ message rather than a terminal failure.
221
228
  - **Every SOAP call is bounded by a 30s timeout** (`CURL_TIMEOUT` in
222
229
  `enable-call-scoring.sh`, matching the only existing timeout precedent in
223
230
  this repo — the Pipeline skill's `setup-all.sh`) and classified via
@@ -243,14 +250,14 @@ Re-read `enableECICallScoring`. Require `true`. Print SOAP response on failure.
243
250
  - **SOAP helpers**: `scripts/shared/soap.sh` (copied from the Pipeline skill —
244
251
  `redact_token`, `parse_soap_response`)
245
252
  - **Server-side gate** (declarative access check):
246
- `core/conversation-udd/java/resources/udd/conversation-udd/ConversationalIntelligence.accessChecks.xml`
253
+ `ConversationalIntelligence.accessChecks.xml`
247
254
  — `orgHasCallScoring` = ECI arm OR River Rush arm; `orgHasECICallScoringWithAI`
248
255
  adds Einstein GenAI + `ECICallScoringAI` license perm on top.
249
256
  - **Pref declaration**:
250
- `core/conversation-udd/java/resources/udd/conversation-udd/ConversationalIntelligence.settings.xml`
257
+ `ConversationalIntelligence.settings.xml`
251
258
  — `<orgPreference name="ECICallScoringEnabled" mdApiName="enableECICallScoring" ...>`
252
259
  - **Java feature registration** (would be the entry point if a
253
- SetupDiscoveryService REST/MCP wrapper existed): `core/conversation-impl/java/src/conversation/features/CoachingCompetenciesFeature.java`
260
+ SetupDiscoveryService REST/MCP wrapper existed): `CoachingCompetenciesFeature.java`
254
261
 
255
262
  ## Competency CRUD workflow
256
263
 
@@ -61,14 +61,14 @@ resolve_org_auth_with_token "$ORG_ALIAS" || exit 1
61
61
  # orgs) reject the OrganizationSettingsDetail query with INVALID_TYPE. That's
62
62
  # not a licensing signal — it's a metadata-availability signal. Treat any
63
63
  # non-conclusive result as "unknown, defer to SOAP" and only refuse when the
64
- # query explicitly comes back "false" (W-24022554).
64
+ # query explicitly comes back "false".
65
65
  CONVERSATIONPILOT_ENABLED=""
66
66
  if CONVERSATIONPILOT_RAW=$(sf data query --target-org "$ORG_ALIAS" --use-tooling-api \
67
67
  --query "SELECT SettingValue FROM OrganizationSettingsDetail WHERE SettingName='ConversationPilot' AND DataType='OrgPermissionSettings'" \
68
68
  --json 2>/dev/null); then
69
69
  CONVERSATIONPILOT_JSON=$(echo "$CONVERSATIONPILOT_RAW" | awk '/^{/,EOF')
70
70
  if echo "$CONVERSATIONPILOT_JSON" | jq -e '.status == 0' >/dev/null 2>&1; then
71
- CONVERSATIONPILOT_ENABLED=$(echo "$CONVERSATIONPILOT_JSON" | jq -r '.result.records[0].SettingValue // empty')
71
+ CONVERSATIONPILOT_ENABLED=$(echo "$CONVERSATIONPILOT_JSON" | jq -r '.result.records[0].SettingValue | select(. != null)')
72
72
  fi
73
73
  fi
74
74
 
@@ -215,7 +215,6 @@ fi
215
215
  # doing a write — Einstein GenAI Platform is a hard prerequisite for AI
216
216
  # Call Scoring, so an org that can't even see that settings type won't be
217
217
  # able to enable Call Coaching regardless of what the ECI toggle says.
218
- # W-24026047.
219
218
  #
220
219
  # Not perfect: an org could have the GenAI platform present but still lack
221
220
  # the specific Call Scoring license. State 2c's ECICallScoringAI
@@ -245,19 +244,33 @@ if ECICALLSCORINGAI_RAW=$(sf data query --target-org "$ORG_ALIAS" --use-tooling-
245
244
  --json 2>/dev/null); then
246
245
  ECICALLSCORINGAI_JSON=$(echo "$ECICALLSCORINGAI_RAW" | awk '/^{/,EOF')
247
246
  if echo "$ECICALLSCORINGAI_JSON" | jq -e '.status == 0' >/dev/null 2>&1; then
248
- ECICALLSCORINGAI_ENABLED=$(echo "$ECICALLSCORINGAI_JSON" | jq -r '.result.records[0].SettingValue // "false"')
247
+ # A missing row means "couldn't read it", NOT "license is off" — default
248
+ # to empty (unknown), never to "false". OrganizationSettingsDetail is not
249
+ # queryable (INVALID_TYPE) on some org editions (for example Developer
250
+ # Edition) and can return zero rows even when queryable; both are "can't
251
+ # tell", and collapsing them to "false" fails closed. Use select(), not
252
+ # `// empty`: SettingValue is a boolean and `//` drops an explicit false.
253
+ ECICALLSCORINGAI_ENABLED=$(echo "$ECICALLSCORINGAI_JSON" | jq -r '.result.records[0].SettingValue | select(. != null)')
249
254
  fi
250
255
  fi
251
256
 
252
- if [[ -z "$ECICALLSCORINGAI_ENABLED" ]]; then
253
- echo "Could not determine AI call coaching license state — ECICallScoringAI query failed."
254
- echo " Verify the CLI session is valid (sf org login web --alias $ORG_ALIAS) and re-run."
257
+ # Only an EXPLICIT negative refuses. An unknown result — the query was
258
+ # rejected (INVALID_TYPE on this org shape) or returned no row — must NOT
259
+ # hard-fail: doing so blocks legitimately-licensed orgs where
260
+ # OrganizationSettingsDetail simply isn't queryable. This mirrors the Phase 0
261
+ # ConversationPilot gate, which defers to SOAP on any non-conclusive read. The
262
+ # 2a-pre GenAI TYPE_UNAVAILABLE gate above catches most unlicensed orgs, and
263
+ # the updateMetadata write below is the final check — a truly unlicensed org
264
+ # faults on the write. Closes the residual gap the earlier
265
+ # GenAI-proxy fix left behind.
266
+ if [[ "$ECICALLSCORINGAI_ENABLED" == "false" ]]; then
267
+ echo "AI call coaching is not available in this org and org requires Agentforce for Sales license."
255
268
  exit 1
256
269
  fi
257
270
 
258
- if [[ "$ECICALLSCORINGAI_ENABLED" != "true" ]]; then
259
- echo "AI call coaching is not available in this org and org requires Agentforce for Sales license."
260
- exit 1
271
+ if [[ -z "$ECICALLSCORINGAI_ENABLED" ]]; then
272
+ echo "Couldn't confirm the AI Call Scoring license on this org — continuing;"
273
+ echo " if the license is missing, the enable step will report it."
261
274
  fi
262
275
 
263
276
  # 2b. ECI must be enabled — this skill is ECI-only.
@@ -363,7 +376,7 @@ EOF
363
376
  # read-only and updateMetadata returns INSUFFICIENT_ACCESS_OR_READONLY.
364
377
  # This is a transient state that self-resolves in a few minutes, not a
365
378
  # permanent failure — surface a wait-and-retry message instead of the
366
- # generic error. W-23968462.
379
+ # generic error.
367
380
  case "$CS_RESULT" in
368
381
  WRITE_ERROR:INSUFFICIENT_ACCESS_OR_READONLY*)
369
382
  echo " Einstein Conversation Insights is still finishing turning on."
@@ -388,31 +401,40 @@ CCR_LOOKUP=$(sf api request rest \
388
401
  "services/data/v68.0/tooling/query/?q=SELECT+Id,SettingValue+FROM+OrganizationSettingsDetail+WHERE+SettingName='CallCoachingReady'+AND+DataType='OrgPermissionSettings'" \
389
402
  --target-org "$ORG_ALIAS" 2>/dev/null) || true
390
403
 
391
- CCR_COUNT=$(echo "$CCR_LOOKUP" | jq -r '.totalSize // 0' 2>/dev/null || echo 0)
392
- if [[ "$CCR_COUNT" == "0" ]]; then
393
- echo "Could not locate the CallCoachingReady OrgPermission record on this org."
394
- exit 1
395
- fi
404
+ # On some org editions (for example Developer Edition), OrganizationSettingsDetail
405
+ # isn't exposed at all (INVALID_TYPE) — the same unqueryable shape the Phase 3
406
+ # license gate tolerates. The authoritative enable (enableECICallScoring, flipped
407
+ # above and re-verified below) has already succeeded, so a CallCoachingReady
408
+ # lookup we physically cannot run must not fail the enable closed.
409
+ if echo "$CCR_LOOKUP" | grep -q '"errorCode"[[:space:]]*:[[:space:]]*"INVALID_TYPE"'; then
410
+ echo "CallCoachingReady OrgPermission isn't queryable on this org — skipping (Call Coaching is already enabled)."
411
+ else
412
+ CCR_COUNT=$(echo "$CCR_LOOKUP" | jq -r '.totalSize // 0' 2>/dev/null || echo 0)
413
+ if [[ "$CCR_COUNT" == "0" ]]; then
414
+ echo "Could not locate the CallCoachingReady OrgPermission record on this org."
415
+ exit 1
416
+ fi
396
417
 
397
- CCR_ID=$(echo "$CCR_LOOKUP" | jq -r '.records[0].Id')
398
- CCR_CURRENT=$(echo "$CCR_LOOKUP" | jq -r '.records[0].SettingValue')
418
+ CCR_ID=$(echo "$CCR_LOOKUP" | jq -r '.records[0].Id')
419
+ CCR_CURRENT=$(echo "$CCR_LOOKUP" | jq -r '.records[0].SettingValue')
399
420
 
400
- if [[ "$CCR_CURRENT" == "true" ]]; then
401
- echo "CallCoachingReady is already enabled — skipping."
402
- else
403
- echo "Enabling CallCoachingReady..."
404
- if CCR_PATCH_RESP=$(sf api request rest \
405
- "services/data/v68.0/tooling/sobjects/OrganizationSettingsDetail/${CCR_ID}" \
406
- --target-org "$ORG_ALIAS" \
407
- --method PATCH \
408
- --header 'Content-Type: application/json' \
409
- --body '{"SettingValue": true}' \
410
- 2>/dev/null); then
411
- echo " CallCoachingReady enabled"
421
+ if [[ "$CCR_CURRENT" == "true" ]]; then
422
+ echo "CallCoachingReady is already enabled — skipping."
412
423
  else
413
- echo " Failed to enable CallCoachingReady"
414
- echo " $CCR_PATCH_RESP"
415
- exit 1
424
+ echo "Enabling CallCoachingReady..."
425
+ if CCR_PATCH_RESP=$(sf api request rest \
426
+ "services/data/v68.0/tooling/sobjects/OrganizationSettingsDetail/${CCR_ID}" \
427
+ --target-org "$ORG_ALIAS" \
428
+ --method PATCH \
429
+ --header 'Content-Type: application/json' \
430
+ --body '{"SettingValue": true}' \
431
+ 2>/dev/null); then
432
+ echo " CallCoachingReady enabled"
433
+ else
434
+ echo " Failed to enable CallCoachingReady"
435
+ echo " $CCR_PATCH_RESP"
436
+ exit 1
437
+ fi
416
438
  fi
417
439
  fi
418
440
 
@@ -144,7 +144,7 @@ All steps are sequential. Do not skip or reorder.
144
144
 
145
145
  11. **Apply standard parameters** — if the user needs standard pre-chat fields, generate `<standardParameters>` elements with `parameterType`. If the channel uses Flow-based routing and the user specifies flow variable mappings, include `<actionParameterMappings>` with `actionParameterName` to map each parameter to a flow input variable.
146
146
 
147
- 12. **Apply custom parameters** — if the user needs pre-chat data collection, generate `<customParameters>` elements with `name`, `masterLabel`, `parameterDataType`, `externalParameterName`, and `maxLength`. If the channel uses Flow-based routing and the user specifies flow variable mappings, include `<actionParameterMappings>` with `actionParameterName` to map each parameter to a flow input variable.
147
+ 12. **Apply custom parameters** — if the user needs pre-chat data collection, generate `<customParameters>` elements with `name`, `masterLabel`, `parameterDataType`, `externalParameterName`, and `maxLength`. Always set `parameterDataType` to `String` — the channel accepts no other value (the field's actual type, including Checkbox/Number/dropdown, is set at the deployment level via `formFieldType`, not here). If the channel uses Flow-based routing and the user specifies flow variable mappings, include `<actionParameterMappings>` with `actionParameterName` to map each parameter to a flow input variable.
148
148
 
149
149
  13. **Generate the file** — produce the `.messagingChannel-meta.xml` file following the template structure. Place at the path the user specifies, or default to the project's metadata source path under `messagingChannels/`.
150
150
 
@@ -216,7 +216,9 @@ All steps are sequential. Do not skip or reorder.
216
216
  | "Only active Agentforce Service Agents are supported" on deploy | Bot is inactive — run `sf agent activate` before deploying |
217
217
  | Flow or ASA routing fails without fallback queue | `sessionHandlerQueue` is mandatory when `sessionHandlerType` is `Flow` or `AgentforceServiceAgent` |
218
218
  | JWT verification not working | Connected app and certificate must be configured for the org |
219
- | Custom parameters not collected | `name` must be unique per channel; `parameterDataType` defaults to `Text` |
219
+ | Custom parameters not collected | `name` must be unique per channel |
220
+ | `<customParameters>` deploy fails: "bad value for restricted picklist field" | `parameterDataType` was set to something other than `String`. Channel custom parameters are always `String`; set the field's actual type (Number, Checkbox, dropdown) at the deployment level, not here |
221
+ | Removing a custom parameter: "can't be deleted as it is referenced elsewhere" | **Check before deploying, not after.** Every deployment on this channel whose pre-chat form uses the param must first drop that field and be published (user-approved); remove the param's `actionParameterMappings` in a separate deploy first. See `references/channel_settings.md` §Removing a Custom Parameter |
220
222
  | Automated responses not showing | Use exact `type` values (`OptOutConfirmation`, `HelpResponse`); XML-escape special characters |
221
223
  | Channel deployed but messages not routing | Channel must be activated after deployment — it defaults to inactive |
222
224
  | "You can't enable Session-Based Chat for verified users" | Set `isSynchronousChatEnabled` to `false` for Auth channels — session-based chat is only valid for UnAuth |
@@ -196,11 +196,13 @@ Repeatable element — one block per pre-chat field for data collection.
196
196
  |-------|------|----------|-------------|
197
197
  | `name` | String | Yes | Unique API name for the parameter |
198
198
  | `masterLabel` | String | Yes | Display label shown to agent/user |
199
- | `parameterDataType` | Enum | Yes | `String`, `Number`, `Boolean` |
199
+ | `parameterDataType` | String | Yes | **Always `String`** — see note below |
200
200
  | `externalParameterName` | String | Yes | External mapping name for the parameter |
201
- | `maxLength` | Integer | No | Max chars for Text type (default 255) |
201
+ | `maxLength` | Integer | No | Max chars (default 255) |
202
202
  | `actionParameterMappings` | Object | No | Maps this parameter to an Omni-Channel Flow action input variable |
203
203
 
204
+ > **`parameterDataType` is always `String`.** Every channel custom parameter deploys as `String`, regardless of the field's logical type. The metadata field's declared type is the broad `FlowDataType` enum (`Apex`, `Boolean`, `Currency`, `Date`, `DateTime`, `Multipicklist`, `Number`, `Picklist`, `SObject`, `String`, `Time`), but `MessagingChannel` restricts it at deploy time to `String` only — any other value fails with *"bad value for restricted picklist field."* The parameter's actual data type (Number, Checkbox/Boolean, dropdown, etc.) is not set on the channel; it is chosen at the **deployment** level in the pre-chat New Custom Parameter screen — i.e. via the Embedded Service form field's `formFieldType` (see `service-digital-engagement-deployment-configure`).
205
+
204
206
  ### Action Parameter Mappings
205
207
 
206
208
  When the channel uses Flow-based routing, `actionParameterMappings` maps a custom parameter to a flow input variable so the value collected from the user is passed into the routing flow.
@@ -222,6 +224,15 @@ When the channel uses Flow-based routing, `actionParameterMappings` maps a custo
222
224
  </customParameters>
223
225
  ```
224
226
 
227
+ ### Removing a Custom Parameter
228
+
229
+ A parameter still used by a pre-chat form can't be deleted ("can't be deleted as it is referenced elsewhere"). Several deployments can share one channel, so **check every deployment before writing**: retrieve all `EmbeddedServiceConfig` and find each one whose `<messagingChannel>` is this channel and whose form has `<formField>` = the param `name`. Don't use a failed channel deploy to find them.
230
+
231
+ 1. If another deployment the user didn't mention still uses the param, stop and ask; changing it is outside the request.
232
+ 2. Each referencing deployment: remove the field (via `service-digital-engagement-deployment-configure`) and publish it after the user approves.
233
+ 3. If the param has `actionParameterMappings`, deploy the channel with the mapping removed first.
234
+ 4. Deploy the channel without that parameter's `<customParameters>` block; keep every other parameter.
235
+
225
236
  ---
226
237
 
227
238
  ## Automated Responses (`<automatedResponses>`)