@salesforce/afv-skills 1.61.0 → 1.62.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 (44) hide show
  1. package/package.json +3 -3
  2. package/skills/agentforce-architecture-analyze/README.md +2 -1
  3. package/skills/agentforce-d360-analyze/README.md +1 -1
  4. package/skills/agentforce-generate/README.md +1 -0
  5. package/skills/crm-analytics-wave-generate/SKILL.md +212 -0
  6. package/skills/crm-analytics-wave-generate/assets/dashboard-template.json +66 -0
  7. package/skills/crm-analytics-wave-generate/assets/wapp-meta-template.xml +10 -0
  8. package/skills/crm-analytics-wave-generate/assets/wdash-meta-template.xml +6 -0
  9. package/skills/crm-analytics-wave-generate/assets/wdpr-meta-template.xml +11 -0
  10. package/skills/crm-analytics-wave-generate/examples/industry-binding-dashboard.wdash +67 -0
  11. package/skills/crm-analytics-wave-generate/examples/opportunity-revenue-recipe.wdpr +132 -0
  12. package/skills/crm-analytics-wave-generate/examples/pipeline-by-industry-quarter.wdpr +236 -0
  13. package/skills/crm-analytics-wave-generate/examples/wdpr-skeleton.wdpr +62 -0
  14. package/skills/crm-analytics-wave-generate/references/dashboard-authoring.md +157 -0
  15. package/skills/crm-analytics-wave-generate/references/dataset-hygiene.md +27 -0
  16. package/skills/crm-analytics-wave-generate/references/external-data-upload.md +61 -0
  17. package/skills/crm-analytics-wave-generate/references/method-decision-tree.md +12 -0
  18. package/skills/crm-analytics-wave-generate/references/r3-node-schema.md +194 -0
  19. package/skills/crm-analytics-wave-generate/references/scripts-reference.md +17 -0
  20. package/skills/crm-analytics-wave-generate/references/ui-section-template.md +68 -0
  21. package/skills/crm-analytics-wave-generate/scripts/check_connector_error.sh +53 -0
  22. package/skills/crm-analytics-wave-generate/scripts/create_app.js +59 -0
  23. package/skills/crm-analytics-wave-generate/scripts/deploy_and_run_recipe.js +122 -0
  24. package/skills/crm-analytics-wave-generate/scripts/fetch_dataset_xmd.js +59 -0
  25. package/skills/crm-analytics-wave-generate/scripts/find_unused_dataset_fields.js +96 -0
  26. package/skills/crm-analytics-wave-generate/scripts/gen_synthetic_dataset.js +137 -0
  27. package/skills/crm-analytics-wave-generate/scripts/patch_t_oppo_view.js +99 -0
  28. package/skills/crm-analytics-wave-generate/scripts/probe_recipe.js +47 -0
  29. package/skills/crm-analytics-wave-generate/scripts/upload_dataset.js +119 -0
  30. package/skills/crm-analytics-wave-generate/scripts/verify_dashboard_steps.js +99 -0
  31. package/skills/crm-analytics-wave-generate/scripts/verify_synthetic_dataset.js +86 -0
  32. package/skills/service-itsm-agentic-setup-incident-management/SKILL.md +59 -20
  33. package/skills/service-itsm-agentic-setup-incident-management/examples/output-templates.md +5 -1
  34. package/skills/service-itsm-agentic-setup-incident-management/references/incident-persona-psg.md +2 -2
  35. package/skills/service-itsm-agentic-setup-incident-management/references/incident-preferences.md +2 -2
  36. package/skills/service-itsm-agentic-setup-incident-management/references/major-incident-management.md +5 -5
  37. package/skills/service-itsm-agentic-setup-incident-management/references/service-mgmt-privilege.md +5 -5
  38. package/skills/service-itsm-agentic-setup-incident-sla-configure/SKILL.md +67 -100
  39. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone-action.json +2 -2
  40. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone.json +2 -2
  41. package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/milestone-patterns.md +23 -22
  42. package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/output-templates.md +3 -3
  43. package/skills/service-itsm-agentic-setup-incident-sla-configure/references/mcp-invocation.md +182 -58
  44. package/skills/service-itsm-agentic-setup-incident-sla-configure/references/workflow.md +157 -88
@@ -0,0 +1,236 @@
1
+ {
2
+ "runMode": "full",
3
+ "nodes": {
4
+ "LOAD_OPPORTUNITY": {
5
+ "action": "load",
6
+ "sources": [],
7
+ "parameters": {
8
+ "dataset": {
9
+ "type": "connectedDataset",
10
+ "label": "Opportunity",
11
+ "connectionName": "SFDC_LOCAL",
12
+ "sourceObjectName": "Opportunity"
13
+ },
14
+ "fields": ["Id", "AccountId", "Name", "StageName", "CloseDate", "Amount"],
15
+ "mode": "SYNCED",
16
+ "preserveCurrencyFields": [],
17
+ "sampleDetails": {
18
+ "type": "TopN",
19
+ "sortBy": []
20
+ }
21
+ }
22
+ },
23
+ "LOAD_ACCOUNT": {
24
+ "action": "load",
25
+ "sources": [],
26
+ "parameters": {
27
+ "dataset": {
28
+ "type": "connectedDataset",
29
+ "label": "Account",
30
+ "connectionName": "SFDC_LOCAL",
31
+ "sourceObjectName": "Account"
32
+ },
33
+ "fields": ["Id", "Name", "Industry"],
34
+ "mode": "SYNCED",
35
+ "preserveCurrencyFields": [],
36
+ "sampleDetails": {
37
+ "type": "TopN",
38
+ "sortBy": []
39
+ }
40
+ }
41
+ },
42
+ "JOIN_ACCOUNT": {
43
+ "action": "join",
44
+ "sources": ["LOAD_OPPORTUNITY", "LOAD_ACCOUNT"],
45
+ "parameters": {
46
+ "joinType": "LOOKUP",
47
+ "leftKeys": ["AccountId"],
48
+ "rightKeys": ["Id"],
49
+ "rightQualifier": "Account"
50
+ }
51
+ },
52
+ "COMPUTE_TOTAL_REVENUE": {
53
+ "action": "formula",
54
+ "sources": ["JOIN_ACCOUNT"],
55
+ "parameters": {
56
+ "expressionType": "SQL",
57
+ "fields": [
58
+ {
59
+ "name": "TotalRevenue",
60
+ "label": "Total Revenue",
61
+ "type": "NUMBER",
62
+ "formulaExpression": "Amount"
63
+ }
64
+ ]
65
+ }
66
+ },
67
+ "COMPUTE_CLOSE_YEAR": {
68
+ "action": "formula",
69
+ "sources": ["COMPUTE_TOTAL_REVENUE"],
70
+ "parameters": {
71
+ "expressionType": "SQL",
72
+ "fields": [
73
+ {
74
+ "name": "CloseYear",
75
+ "label": "Close Year",
76
+ "type": "TEXT",
77
+ "formulaExpression": "date_format(CloseDate, 'yyyy')"
78
+ }
79
+ ]
80
+ }
81
+ },
82
+ "COMPUTE_CLOSE_QUARTER": {
83
+ "action": "formula",
84
+ "sources": ["COMPUTE_CLOSE_YEAR"],
85
+ "parameters": {
86
+ "expressionType": "SQL",
87
+ "fields": [
88
+ {
89
+ "name": "CloseQuarter",
90
+ "label": "Close Quarter",
91
+ "type": "TEXT",
92
+ "formulaExpression": "case when date_format(CloseDate, 'MM') <= '03' then 'Q1' when date_format(CloseDate, 'MM') <= '06' then 'Q2' when date_format(CloseDate, 'MM') <= '09' then 'Q3' else 'Q4' end"
93
+ }
94
+ ]
95
+ }
96
+ },
97
+ "COMPUTE_INDUSTRY_FILLED": {
98
+ "action": "formula",
99
+ "sources": ["COMPUTE_CLOSE_QUARTER"],
100
+ "parameters": {
101
+ "expressionType": "SQL",
102
+ "fields": [
103
+ {
104
+ "name": "IndustryFilled",
105
+ "label": "Industry Filled",
106
+ "type": "TEXT",
107
+ "formulaExpression": "case when (Account.Industry is null or Account.Industry = '') then 'Unknown' else Account.Industry end"
108
+ }
109
+ ]
110
+ }
111
+ },
112
+ "COMPUTE_DEAL_SIZE": {
113
+ "action": "formula",
114
+ "sources": ["COMPUTE_INDUSTRY_FILLED"],
115
+ "parameters": {
116
+ "expressionType": "SQL",
117
+ "fields": [
118
+ {
119
+ "name": "DealSize",
120
+ "label": "Deal Size",
121
+ "type": "TEXT",
122
+ "formulaExpression": "case when Amount >= 100000 then 'Large' when Amount >= 50000 then 'Medium' else 'Small' end"
123
+ }
124
+ ]
125
+ }
126
+ },
127
+ "EXTRACT0": {
128
+ "action": "extractGrains",
129
+ "sources": ["COMPUTE_DEAL_SIZE"],
130
+ "parameters": {
131
+ "grainExtractions": []
132
+ }
133
+ },
134
+ "AGGREGATE_BY_INDUSTRY_QUARTER": {
135
+ "action": "aggregate",
136
+ "sources": ["EXTRACT0"],
137
+ "parameters": {
138
+ "aggregations": [
139
+ {
140
+ "name": "TotalRevenue",
141
+ "label": "Total Revenue",
142
+ "action": "SUM",
143
+ "source": "TotalRevenue"
144
+ }
145
+ ],
146
+ "groupings": ["AccountId", "Account.Name", "IndustryFilled", "CloseYear", "CloseQuarter", "DealSize"],
147
+ "nodeType": "STANDARD",
148
+ "pivots": []
149
+ }
150
+ },
151
+ "OUTPUT_DTS_PIPELINE": {
152
+ "action": "save",
153
+ "sources": ["AGGREGATE_BY_INDUSTRY_QUARTER"],
154
+ "parameters": {
155
+ "dataset": {
156
+ "type": "analyticsDataset",
157
+ "label": "DTS_PipelineByIndustry",
158
+ "name": "DTS_PipelineByIndustry",
159
+ "folderName": "SkillBuildInsights"
160
+ },
161
+ "fields": [],
162
+ "measuresToCurrencies": []
163
+ }
164
+ }
165
+ },
166
+ "ui": {
167
+ "nodes": {
168
+ "LOAD_OPPORTUNITY": {
169
+ "label": "Opportunity",
170
+ "type": "LOAD_DATASET",
171
+ "top": 112,
172
+ "left": 112,
173
+ "parameters": { "sampleSize": 2000 }
174
+ },
175
+ "LOAD_ACCOUNT": {
176
+ "label": "Account",
177
+ "type": "LOAD_DATASET",
178
+ "top": 252,
179
+ "left": 252,
180
+ "parameters": { "sampleSize": 2000 }
181
+ },
182
+ "JOIN_ACCOUNT": {
183
+ "label": "JOIN_ACCOUNT",
184
+ "type": "JOIN",
185
+ "top": 112,
186
+ "left": 392
187
+ },
188
+ "TRANSFORM0": {
189
+ "label": "TRANSFORM0",
190
+ "type": "TRANSFORM",
191
+ "top": 112,
192
+ "left": 532,
193
+ "graph": {
194
+ "COMPUTE_TOTAL_REVENUE": { "parameters": { "type": "BASE_FORMULA_UI" } },
195
+ "COMPUTE_CLOSE_YEAR": { "parameters": { "type": "BASE_FORMULA_UI" } },
196
+ "COMPUTE_CLOSE_QUARTER": { "parameters": { "type": "BASE_FORMULA_UI" } },
197
+ "COMPUTE_INDUSTRY_FILLED": { "parameters": { "type": "BASE_FORMULA_UI" } },
198
+ "COMPUTE_DEAL_SIZE": { "parameters": { "type": "BASE_FORMULA_UI" } }
199
+ },
200
+ "connectors": [
201
+ { "source": "COMPUTE_TOTAL_REVENUE", "target": "COMPUTE_CLOSE_YEAR" },
202
+ { "source": "COMPUTE_CLOSE_YEAR", "target": "COMPUTE_CLOSE_QUARTER" },
203
+ { "source": "COMPUTE_CLOSE_QUARTER", "target": "COMPUTE_INDUSTRY_FILLED" },
204
+ { "source": "COMPUTE_INDUSTRY_FILLED", "target": "COMPUTE_DEAL_SIZE" }
205
+ ]
206
+ },
207
+ "AGGREGATE0": {
208
+ "label": "AGGREGATE0",
209
+ "type": "AGGREGATE",
210
+ "top": 112,
211
+ "left": 672,
212
+ "graph": {
213
+ "EXTRACT0": null,
214
+ "AGGREGATE_BY_INDUSTRY_QUARTER": null
215
+ },
216
+ "connectors": [
217
+ { "source": "EXTRACT0", "target": "AGGREGATE_BY_INDUSTRY_QUARTER" }
218
+ ]
219
+ },
220
+ "OUTPUT_DTS_PIPELINE": {
221
+ "label": "OUTPUT_DTS_PIPELINE",
222
+ "type": "OUTPUT",
223
+ "top": 112,
224
+ "left": 812
225
+ }
226
+ },
227
+ "connectors": [
228
+ { "source": "LOAD_OPPORTUNITY", "target": "JOIN_ACCOUNT" },
229
+ { "source": "LOAD_ACCOUNT", "target": "JOIN_ACCOUNT" },
230
+ { "source": "JOIN_ACCOUNT", "target": "TRANSFORM0" },
231
+ { "source": "TRANSFORM0", "target": "AGGREGATE0" },
232
+ { "source": "AGGREGATE0", "target": "OUTPUT_DTS_PIPELINE" }
233
+ ],
234
+ "hiddenColumns": []
235
+ }
236
+ }
@@ -0,0 +1,62 @@
1
+ {
2
+ "runMode": "full",
3
+ "nodes": {
4
+ "LOAD_OPP": {
5
+ "action": "load",
6
+ "sources": [],
7
+ "parameters": {
8
+ "dataset": { "type": "connectedDataset", "label": "Opportunity", "connectionName": "SFDC_LOCAL", "sourceObjectName": "Opportunity" },
9
+ "fields": ["Id", "Amount", "CloseDate", "StageName"],
10
+ "mode": "SYNCED",
11
+ "preserveCurrencyFields": [],
12
+ "sampleDetails": { "type": "TopN", "sortBy": [] }
13
+ }
14
+ },
15
+ "COMPUTE_YEAR": {
16
+ "action": "formula",
17
+ "sources": ["LOAD_OPP"],
18
+ "parameters": {
19
+ "expressionType": "SQL",
20
+ "fields": [{ "name": "CloseYear", "label": "Close Year", "type": "TEXT", "formulaExpression": "date_format(CloseDate, 'yyyy')" }]
21
+ }
22
+ },
23
+ "EXTRACT0": {
24
+ "action": "extractGrains",
25
+ "sources": ["COMPUTE_YEAR"],
26
+ "parameters": { "grainExtractions": [] }
27
+ },
28
+ "AGG0": {
29
+ "action": "aggregate",
30
+ "sources": ["EXTRACT0"],
31
+ "parameters": {
32
+ "aggregations": [{ "name": "TotalAmount", "label": "Total Amount", "action": "SUM", "source": "Amount" }],
33
+ "groupings": ["StageName"],
34
+ "nodeType": "STANDARD",
35
+ "pivots": []
36
+ }
37
+ },
38
+ "OUTPUT": {
39
+ "action": "save",
40
+ "sources": ["AGG0"],
41
+ "parameters": {
42
+ "dataset": { "type": "analyticsDataset", "label": "DTS_Opportunity", "name": "DTS_Opportunity", "folderName": "MyApp" },
43
+ "fields": [],
44
+ "measuresToCurrencies": []
45
+ }
46
+ }
47
+ },
48
+ "ui": {
49
+ "nodes": {
50
+ "LOAD_OPP": { "label": "Opportunity", "type": "LOAD_DATASET", "top": 112, "left": 112, "parameters": { "sampleSize": 2000 } },
51
+ "TRANSFORM0": { "label": "TRANSFORM0", "type": "TRANSFORM", "top": 112, "left": 252, "graph": { "COMPUTE_YEAR": { "parameters": { "type": "BASE_FORMULA_UI" } } }, "connectors": [] },
52
+ "AGGREGATE0": { "label": "AGGREGATE0", "type": "AGGREGATE", "top": 112, "left": 392, "graph": { "EXTRACT0": null, "AGG0": null }, "connectors": [{ "source": "EXTRACT0", "target": "AGG0" }] },
53
+ "OUTPUT": { "label": "Output", "type": "OUTPUT", "top": 112, "left": 532 }
54
+ },
55
+ "connectors": [
56
+ { "source": "LOAD_OPP", "target": "TRANSFORM0" },
57
+ { "source": "TRANSFORM0", "target": "AGGREGATE0" },
58
+ { "source": "AGGREGATE0", "target": "OUTPUT" }
59
+ ],
60
+ "hiddenColumns": []
61
+ }
62
+ }
@@ -0,0 +1,157 @@
1
+ # Dashboard Authoring
2
+
3
+ ## Deploy-time rules
4
+
5
+ The `.wdash` file contains ONLY the `state` object — no `label`, `description`, or `application` keys at root (those belong in `.wdash-meta.xml`).
6
+
7
+ Critical deploy-time rules:
8
+ - `state.dataSourceLinksInfo` must be an **object** (`{}`), not an array.
9
+ - `state.gridLayouts[].numColumns` is `24` for modern dashboards.
10
+ - **Widget `type` and `parameters` go in the top-level `widgets` dict, NOT in `gridLayouts`.** `gridLayouts[].pages[].widgets[]` entries hold positioning keys: `name`, `row`, `column`, `colspan`, `rowspan`, and optionally `widgetStyle`. Do NOT use `col` (singular) — that triggers `Unrecognized field "col"`.
11
+ - Do NOT include `type` on gridLayouts widget entries — rejected with `Unrecognized field "type" (Class GridLayoutWidgetInputRepresentation)`.
12
+ - Step `type`: use `saql` or `aggregateflex`. SAQL steps must NOT include `isGlobal`, `receiveFacet`, `datasets`, or `label`.
13
+ - Keep widget `parameters` minimal at deploy time: `{ step }` for listselector/valuestable, `{ step, measureField }` for number, `{ step, visualizationType }` for chart. Add everything else via PATCH.
14
+
15
+ ## Proven-accepted PATCH fields per widget type
16
+
17
+ | Widget type | Accepted in `parameters` |
18
+ |-------------|--------------------------|
19
+ | `number` | `step`, `measureField`, `title`, `titleColor`, `titleSize`, `numberColor`, `numberSize`, `textAlignment`, `compact`, `showActionMenu`, `exploreLink`, `tooltip: { customizeTooltip: true }`, `interactions: []` |
20
+ | `chart` | `step`, `visualizationType`, `title` (object), `titleColor`, `titleSize`, `legend`, `trellis`, `theme`, `showActionMenu`, `exploreLink`, `interactions: []`, `dimensionAxis`, `measureAxis1` — **no `tooltip`** |
21
+ | `listselector` | `step`, `title`, `compact`, `displayMode`, `expanded`, `exploreLink`, `filterStyle: { titleColor, valueColor }`, `instant`, `interactions: []`, `showActionMenu` |
22
+ | `valuestable` | `step` ONLY — all styling fields rejected. Use `chart` with `visualizationType: "flatTable"` for dark theming |
23
+ | `text` | `content` (rich text), `interactions: []` — no styling fields |
24
+
25
+ **`chart` `title` object shape** (not a plain string):
26
+ ```json
27
+ "title": { "label": "Revenue by Industry", "fontSize": 14, "subtitleFontSize": 11, "align": "left", "subtitleLabel": "" }
28
+ ```
29
+
30
+ **`chart` `dimensionAxis` and `measureAxis1`** (make bars draw with labelled axes):
31
+ ```json
32
+ "dimensionAxis": { "showTitle": false, "customSize": "auto", "showAxis": true, "title": "Industry" },
33
+ "measureAxis1": { "sqrtScale": false, "showTitle": false, "showAxis": true, "title": "Revenue ($)", "customDomain": { "showDomain": false } }
34
+ ```
35
+
36
+ ## Outer PATCH fields
37
+
38
+ - `gridLayouts[i].style`: `backgroundColor`, `cellSpacingX`, `cellSpacingY`, `gutterColor`, `alignmentX`, `alignmentY`, `fit`.
39
+ - `gridLayouts[i].pages[j].widgets[k].widgetStyle`: `backgroundColor`, `borderColor`, `borderEdges`, `borderRadius` (**integer**, e.g. `8`), `borderWidth`.
40
+
41
+ **Dark theme values** (page bg `#0d1117`, card bg `#161b22`, accent `#4cc9f0`):
42
+ ```json
43
+ "gridLayouts[0].style": { "backgroundColor": "#0d1117", "cellSpacingX": 8, "cellSpacingY": 8, "gutterColor": "#0d1117" }
44
+ "widgetStyle": { "backgroundColor": "#161b22", "borderColor": "#30363d", "borderEdges": ["all"], "borderRadius": 8, "borderWidth": 1 }
45
+ ```
46
+
47
+ ## The SAQL re-encoding trap
48
+
49
+ When the API stores a dashboard, it HTML-entity-encodes SAQL (e.g. `"` → `&quot;`). Single-level encoding is normal. If you GET → PATCH without decoding, the API encodes again → `&amp;quot;`. After two cycles widgets render "Error" with errorCode 119 `Unexpected character '&'`.
50
+
51
+ **Always decode every `state.steps[*].query` immediately after a GET, before any mutation + PATCH.**
52
+
53
+ ```javascript
54
+ function decodeAll(s) {
55
+ let prev = null;
56
+ while (prev !== s) {
57
+ prev = s;
58
+ s = s.replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, '&');
59
+ }
60
+ return s;
61
+ }
62
+ ```
63
+
64
+ ## Dataset references in SAQL
65
+
66
+ Store name-based load: `q = load "DTS_OpportunityRev"; …`. The dashboard runtime resolves the name internally.
67
+
68
+ Do NOT use id/version form `q = load "0Fb.../0Fc..."` — the PATCH input rep rejects the `datasets` binding required for id-based loads, so widgets render *"To use the [0Fb...] datasets in the query, add them to the lens first."*
69
+
70
+ To verify a stored name-based SAQL step: fetch → decode → **swap name for `datasetId/currentVersionId`** → POST to `/wave/query`. The `/wave/query` endpoint cannot resolve names.
71
+
72
+ ## KPI total — always use `group q by all`
73
+
74
+ ```saql
75
+ q = load "MyDataset";
76
+ q = group q by all;
77
+ q = foreach q generate sum('TotalRevenue') as 'TotalRevenue';
78
+ ```
79
+
80
+ `foreach` without `group by all` acts as a window function returning one row per source row.
81
+
82
+ ## Query bindings — selection widget driving another step's SAQL
83
+
84
+ Faceting (`broadcastFacet: true` / `useGlobal: true`) is the default filter path. When a listselector must drive a **specific** SAQL filter in another step — not a global facet — use an explicit query binding.
85
+
86
+ Proven pattern from working org dashboards:
87
+
88
+ ```saql
89
+ q = load "DTS_PipelineByIndustry";
90
+ q = filter q by {{column(step_industry_sel.selection, ["Industry"]).asEquality("Account.Industry")}};
91
+ q = group q by all;
92
+ q = foreach q generate sum('TotalRevenue') as 'TotalRevenue';
93
+ ```
94
+
95
+ | Binding | Use when |
96
+ |---|---|
97
+ | `{{column(step.selection, ["Field"]).asEquality("TargetField")}}` | Selection should become `TargetField == value` (or `in` for multi). Empty selection is a no-op (all rows). **This is the default for a listselector driving a filter.** |
98
+ | `{{column(step.selection, ["Field"]).asObject()}}` | Inject the selected values as a SAQL list. Empty selection can yield zero rows — prefer `asEquality` unless you need the raw list. |
99
+ | `{{cell(step.selection, 0, "Field").asString()}}` | Single-cell string (e.g. a title). Do **not** use this as the filter itself — null selection breaks the query. |
100
+
101
+ Rules:
102
+ - The selector step aliases the field to the name used in `column(..., ["Industry"])`. If the step projects `'Account.Industry' as 'Industry'`, bind `["Industry"]`, not `["Account.Industry"]`.
103
+ - Set `broadcastFacet: false` and `useGlobal: false` on **both** the selector step and the bound data steps. Combining faceting with an explicit binding double-filters.
104
+ - Bindings are dashboard-runtime only. `/wave/query` cannot resolve `{{column(...)}}`. To verify a bound step: strip the `filter q by {{...}}` clause (or substitute a literal), swap the dataset name for `id/version`, then POST.
105
+ - Store the binding in the SAQL string with raw quotes. Never HTML-encode it yourself — the API encodes on write. Always `decodeAll` before PATCH (see below).
106
+
107
+ Selector step (no binding on itself):
108
+
109
+ ```saql
110
+ q = load "DTS_PipelineByIndustry";
111
+ q = group q by 'Account.Industry';
112
+ q = foreach q generate 'Account.Industry' as 'Industry';
113
+ q = order q by 'Industry' asc;
114
+ ```
115
+
116
+ ## Filter widgets — use `listselector`
117
+
118
+ `toggleGroup` does not exist as a stored type. Always use `listselector`.
119
+
120
+ For a measure-typed field (e.g. CloseMonth as NUMBER): use `foreach` without GROUP BY — `listselector` deduplicates internally:
121
+ ```saql
122
+ q = load "MyDataset";
123
+ q = foreach q generate 'CloseMonth' as 'CloseMonth';
124
+ q = limit q 1000;
125
+ ```
126
+
127
+ ## Step shape that renders correctly
128
+
129
+ ```json
130
+ {
131
+ "type": "saql",
132
+ "query": "q = load \"<DatasetName>\"; …",
133
+ "broadcastFacet": true,
134
+ "useGlobal": true,
135
+ "selectMode": "single",
136
+ "groups": [],
137
+ "numbers": [],
138
+ "strings": []
139
+ }
140
+ ```
141
+
142
+ Do NOT include: `datasets`, `start`, `isGlobal`, `receiveFacet`, `visualizationParameters`, `label`.
143
+
144
+ ## Common errors → fixes
145
+
146
+ | Symptom | Cause | Fix |
147
+ |---------|-------|-----|
148
+ | `errorCode 264 "Classic Dashboards have been retired"` on POST | REST POST only creates legacy dashboards | Use Metadata API deploy |
149
+ | `Unrecognized field "label"` on deploy | Top-level metadata inside `.wdash` | Move `label`, `description`, `application` to `.wdash-meta.xml` |
150
+ | `Can not deserialize … out of START_ARRAY token` | `dataSourceLinksInfo` was an array | Change to empty object `{}` |
151
+ | `Unrecognized field "type" (Class GridLayoutWidgetInputRepresentation)` | Widget `type` in gridLayouts entries | Move `type` + `parameters` to top-level `widgets` dict |
152
+ | `Unrecognized field "col"` | Used `col` instead of `column` | Rename to `column` |
153
+ | All widgets "Error" after PATCH | SAQL re-encoding | Apply `decodeAll` before every PATCH |
154
+ | Bound step shows all rows even when a value is selected | Selector uses `broadcastFacet`/`useGlobal` *and* an explicit `{{column}}` binding, or the `column` alias doesn't match the `as 'Industry'` projection | Set both flags `false`; bind the projected alias |
155
+ | `/wave/query` 400 on a bound step | Bindings are dashboard-runtime only | Strip `filter q by {{...}}` (or substitute a literal) before posting |
156
+ | `Unrecognized field "datasets"` on PATCH | `state.steps[*].datasets` is output-only | Remove from PATCH body |
157
+ | Widgets: "To use [0Fb...] datasets … add them to the lens first" | Stored SAQL uses id/version load | Switch to name-based `q = load "<DatasetName>"` |
@@ -0,0 +1,27 @@
1
+ # Dataset Hygiene and Dashboard Auditing
2
+
3
+ ## Finding unused dataset fields
4
+
5
+ Run `scripts/find_unused_dataset_fields.js <alias> <DatasetName>`. It:
6
+
7
+ 1. Resolves the dataset → fetches its main XMD → enumerates `dimensions ∪ measures ∪ dates` → set of canonical field names.
8
+ 2. Pages through `GET /wave/dashboards?pageSize=200`.
9
+ 3. For each dashboard, `GET /wave/dashboards/{id}` → concatenates every `state.steps[*].query`.
10
+ 4. Keeps only dashboards whose SAQL contains `load "<DatasetName>"` (tolerates `&quot;` HTML-encoded form).
11
+ 5. Extracts every single-quoted token from SAQL, intersects with the field-name set.
12
+ 6. Outputs the unused set.
13
+
14
+ **Caveats:**
15
+ - Compound steps referencing fields with the same name across multiple datasets will look "used" against any dataset with a matching field name.
16
+ - Aggregateflex steps store column references in a structured `columns` block, not in a SAQL `query`. The scanner only handles SAQL steps — extend the extractor for `state.steps[*].columns[*].field` if needed.
17
+ - Dashboards in retired/archived apps still count as "users" unless you filter by folder.
18
+
19
+ ## Finding where a field is used
20
+
21
+ Same scanner, inverted: for a single field name, list every (dashboard, step) whose SAQL token set contains it. Adapt `find_unused_dataset_fields.js` — replace the field-set intersection with a single-field membership test.
22
+
23
+ ## Detecting SAQL re-encoding decay
24
+
25
+ Dashboards edited via PATCH without first decoding SAQL accumulate `&amp;quot;`, `&amp;amp;quot;`, etc. Detection: GET every dashboard in the target app, count occurrences of the substring `&amp;` in each `state.steps[*].query`. Anything > 0 means at least one extra encoding layer. (Single-level `&quot;` is normal and expected.)
26
+
27
+ Fix: apply the `decodeAll` helper from `references/dashboard-authoring.md` to every affected step's query, then PATCH the dashboard with the decoded state.
@@ -0,0 +1,61 @@
1
+ # External Data Uploads (CSV → Dataset)
2
+
3
+ Use this when landing a CSV into a Wave dataset without running a recipe — e.g. synthetic test data, one-off external feeds, or standing up a dashboard before recipe sources are available.
4
+
5
+ ## Process (4 steps)
6
+
7
+ 1. `POST /services/data/v66.0/sobjects/InsightsExternalData` with:
8
+ ```json
9
+ { "Format": "Csv", "EdgemartAlias": "<datasetName>", "EdgemartContainer": "<folderId>",
10
+ "Operation": "Overwrite", "Action": "None", "MetadataJson": "<base64 of metadata.json>" }
11
+ ```
12
+ Returns `{ id }` — the header id.
13
+
14
+ 2. `POST /services/data/v66.0/sobjects/InsightsExternalDataPart` for each chunk:
15
+ ```json
16
+ { "InsightsExternalDataId": "<headerId>", "PartNumber": 1, "DataFile": "<base64 of CSV bytes>" }
17
+ ```
18
+ Keep parts ≤ 8 MB.
19
+
20
+ 3. `PATCH /services/data/v66.0/sobjects/InsightsExternalData/<headerId>` with `{ "Action": "Process" }`. Returns 204; ingestion is queued.
21
+
22
+ 4. Poll: `SELECT Id, Status, StatusMessage FROM InsightsExternalData WHERE Id = '<headerId>'`. Status flow: `New` → `Queued` → `InProgress` → `Completed` | `CompletedWithWarnings` | `Failed`.
23
+
24
+ ## metadata.json minimum-viable schema
25
+
26
+ ```json
27
+ {
28
+ "fileFormat": { "charsetName": "UTF-8", "fieldsDelimitedBy": ",", "linesTerminatedBy": "\n", "numberOfLinesToIgnore": 1 },
29
+ "objects": [{
30
+ "connector": "CSV",
31
+ "fullyQualifiedName": "<datasetName>",
32
+ "name": "<datasetName>",
33
+ "label": "<display label>",
34
+ "fields": [
35
+ { "name": "AccountId", "fullyQualifiedName": "AccountId", "label": "Account ID", "type": "Text" },
36
+ { "name": "TotalRevenue", "fullyQualifiedName": "TotalRevenue", "label": "Total Revenue", "type": "Numeric", "precision": 18, "scale": 2, "defaultValue": "0", "format": "$#,##0.00" }
37
+ ]
38
+ }]
39
+ }
40
+ ```
41
+
42
+ ## Critical rules
43
+
44
+ - **Every field needs `fullyQualifiedName`** — omitting it returns `FIELD_INTEGRITY_EXCEPTION: The fully-qualified name for field [X] cannot be empty`.
45
+ - **Field names cannot contain dots** — recipe join fields like `Account.Name` must be renamed `Account_Name`. Labels can keep the original spelling.
46
+ - **`type`** must be `Text`, `Numeric`, or `Date`. For Numeric: supply `precision` + `scale`. For Date: `format` must be a SimpleDateFormat string (e.g. `"yyyy-MM-dd"`).
47
+ - **CSV nulls** are empty fields — do not write the literal string `null`.
48
+ - **Folder** = `EdgemartContainer` is the WaveApplication folder id from `GET /wave/folders`, not the folder name.
49
+ - **Operation modes**: `Overwrite` replaces data; `Append` adds rows; `Upsert` requires `isUniqueId: true`; `Delete` removes rows matching key.
50
+ - **A 200 on Action=Process ≠ data is queryable.** Wait for `Status = Completed`, then verify with a SAQL count query.
51
+
52
+ ## Common errors
53
+
54
+ | Symptom | Cause | Fix |
55
+ |---------|-------|-----|
56
+ | `FIELD_INTEGRITY_EXCEPTION: fully-qualified name … cannot be empty` | Missing `fullyQualifiedName` | Add to every field |
57
+ | `Object name not allowed` / dot in name | Dataset or field name contains `.` or `-` | Use `[A-Za-z][A-Za-z0-9_]*` only |
58
+ | `Status: Failed` referencing a specific row | CSV value doesn't parse as declared type | Check row in `StatusMessage`; ensure no currency symbols in numeric data |
59
+ | `Status: Completed` but 0 rows | Wrong `Operation` or `numberOfLinesToIgnore` too large | Check both values |
60
+ | SAQL `Syntax Error … as 'rows'` | `rows` is a reserved SAQL word | Use `'row_count'` or `'cnt'` as alias |
61
+ | `Invalid group expression: <Field>` (errorCode 119) | Field is a measure (NUMBER type) in XMD | Use `case` in SAQL or change recipe to emit TEXT version |
@@ -0,0 +1,12 @@
1
+ # Method Decision Tree
2
+
3
+ | Goal | Use | Why |
4
+ |------|-----|-----|
5
+ | Create a `WaveApplication` (app/folder) | Analytics REST `POST /wave/folders` | SFDX metadata deploy for WaveApplication is finicky and almost always fails; REST works in one call |
6
+ | Create a `WaveRecipe` that compiles and runs | Analytics REST `POST /wave/recipes?format=R3` with `recipeDefinition` in JSON body | Multipart upload creates the recipe but leaves it in `New` status with no `targetDataflowId` — it can never run |
7
+ | Run a recipe ("Run Now") | Analytics REST `POST /wave/dataflowjobs` with `{ dataflowId: targetDataflowId, command: "start" }` | `dataflowId` is the recipe's `targetDataflowId` (02K...), never the recipe id (05v...) |
8
+ | Upload CSV into a new or existing dataset | InsightsExternalData sObject API (header → parts → `Action=Process` → poll) | Only supported no-UI path; `sf data tree import` and generic bulk APIs don't produce Wave datasets |
9
+ | Call REST from a script | `curl` via `execSync` with `-s -w '\nHTTP_STATUS:%{http_code}'` (no `-f`) | `-f` swallows the response body on non-2xx; the status-suffix pattern surfaces the full error message |
10
+ | Bypass OAuth scopes for quick REST call | Apex `Http` callout via `sf apex run` using `UserInfo.getSessionId()` | Internal session bypasses External Client App scope restrictions — for diagnosis only |
11
+ | Deploy a modern grid dashboard | Metadata API: `sf project deploy start` for `.wdash` + `.wdash-meta.xml` | `POST /wave/dashboards` only creates retired classic dashboards (errorCode 264) |
12
+ | Enrich dashboard styling after deploy | `PATCH /wave/dashboards/{id}` with `{ state: <enriched state> }` | Deploy input rep is minimal; styling fields (`gridLayouts[].style`, `widgetStyle`) are only accepted at PATCH time |