@salesforce/afv-skills 1.44.0 → 1.45.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.
- package/package.json +1 -1
- package/skills/consumer-goods-rtr-datacloud-export-configure/SKILL.md +72 -0
- package/skills/consumer-goods-rtr-datacloud-export-configure/references/inputs-and-namespace.md +38 -0
- package/skills/consumer-goods-rtr-datacloud-export-configure/references/procedure.md +158 -0
- package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/detect-namespace.js +86 -0
- package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/render-apex.js +64 -0
- package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/resolve-id-by-name.js +47 -0
- package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/sf-rest.js +171 -0
- package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/soql-escape.js +26 -0
- package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/upsert-report-config.apex +51 -0
- package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/upsert-system-setting.apex +21 -0
- package/skills/consumer-goods-tpe-dashboard-configure/SKILL.md +74 -0
- package/skills/consumer-goods-tpe-dashboard-configure/references/phases-1-6.md +112 -0
- package/skills/consumer-goods-tpe-dashboard-configure/references/phases-7-12.md +157 -0
- package/skills/consumer-goods-tpe-dashboard-configure/scripts/find-failure-reason.js +132 -0
- package/skills/consumer-goods-tpe-dashboard-configure/scripts/poll-status.js +116 -0
- package/skills/consumer-goods-tpe-dashboard-configure/scripts/render-apex.js +64 -0
- package/skills/consumer-goods-tpe-dashboard-configure/scripts/run-data-transform.js +121 -0
- package/skills/consumer-goods-tpe-dashboard-configure/scripts/schedule-business-period-export.apex +27 -0
- package/skills/consumer-goods-tpe-dashboard-configure/scripts/sf-rest.js +171 -0
- package/skills/consumer-goods-tpe-dashboard-configure/scripts/soql-escape.js +25 -0
- package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/SKILL.md +141 -0
- package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/references/payload-shapes.md +447 -0
- package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/references/procedure.md +263 -0
- package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/scripts/clone-tpe-dashboards.js +537 -0
- package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/scripts/sf-rest.js +195 -0
- package/skills/consumer-goods-tpe-datakit-deploy/SKILL.md +157 -0
- package/skills/consumer-goods-tpe-datakit-deploy/scripts/detect-namespace.js +86 -0
- package/skills/consumer-goods-tpe-datakit-deploy/scripts/download-static-resource.js +151 -0
- package/skills/consumer-goods-tpe-datakit-deploy/scripts/extract-crm-field-permissions.js +115 -0
- package/skills/consumer-goods-tpe-datakit-deploy/scripts/sf-rest.js +109 -0
- package/skills/consumer-goods-tpe-datakit-deploy/scripts/update-field-permissions.js +433 -0
- package/skills/service-catalog-template-coordinate/SKILL.md +263 -0
- package/skills/service-catalog-template-coordinate/examples/output-templates.md +44 -0
- package/skills/service-catalog-template-coordinate/references/mcp-invocation.md +183 -0
- package/skills/service-catalog-template-coordinate/references/operations.md +230 -0
- package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/SKILL.md +243 -0
- package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/references/cli-invocation.md +205 -0
- package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/references/helper-contracts.md +236 -0
- package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/references/permset-topology.md +132 -0
- package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-activated-agents.mjs +106 -0
- package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-agent-access-state.mjs +113 -0
- package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-assignment-state.mjs +99 -0
- package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-platform-permset-availability.mjs +155 -0
- package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/gate-unified-catalog-tiers.mjs +100 -0
- package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/rank-candidate-users.mjs +95 -0
- package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/resolve-target-user.mjs +86 -0
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +44 -23
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/examples/output-templates.md +33 -9
- package/skills/service-itsm-agentic-setup-agentforce-studio-configure/SKILL.md +17 -18
- package/skills/service-itsm-agentic-setup-cmdb-coordinate/SKILL.md +3 -1
- package/skills/service-itsm-agentic-setup-configure/SKILL.md +20 -12
- package/skills/service-itsm-agentic-setup-configure/examples/output-templates.md +73 -5
- package/skills/service-itsm-agentic-setup-employee-agent-configure/SKILL.md +8 -7
- package/skills/service-itsm-agentic-setup-employee-agent-configure/references/cli-invocation.md +45 -33
- package/skills/service-itsm-agentic-setup-employee-agent-configure/references/reactivation.md +8 -6
- package/skills/service-itsm-agentic-setup-employee-agent-configure/references/workflow-detail.md +10 -10
- package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/classify-agent-existence.mjs +114 -56
- package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/classify-preflight.mjs +33 -17
- package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/render-report.mjs +9 -3
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/SKILL.md +8 -7
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/cli-invocation.md +43 -32
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/reactivation.md +6 -4
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/workflow-detail.md +10 -10
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-agent-existence.mjs +106 -55
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-preflight.mjs +27 -13
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/render-report.mjs +9 -3
- package/skills/service-itsm-agentic-setup-incident-sla-configure/SKILL.md +159 -161
- package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone-action.json +51 -0
- package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone.json +1 -1
- package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/predefined-incident-policy.json +120 -0
- package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/milestone-patterns.md +28 -5
- package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/output-templates.md +19 -1
- package/skills/service-itsm-agentic-setup-incident-sla-configure/references/mcp-invocation.md +350 -30
- package/skills/service-itsm-channels-coordinate/SKILL.md +80 -213
- package/skills/service-itsm-slack-itservice-configure/SKILL.md +363 -0
- package/skills/service-itsm-slack-itservice-configure/references/connect-agentforce-to-slack.md +159 -0
- package/skills/service-itsm-slack-itservice-configure/references/manage-slack-connection.md +88 -0
- package/skills/service-itsm-slack-itservice-configure/references/manage-user-access.md +117 -0
- package/skills/service-itsm-slack-itservice-configure/references/record-visibility.md +78 -0
- package/skills/service-itsm-slack-itservice-configure/references/site-membership-verification.md +126 -0
- package/skills/service-itsm-slack-itservice-configure/scripts/classify-user-access.mjs +167 -0
- package/skills/service-itsm-teams-configure/SKILL.md +50 -47
- package/skills/service-itsm-teams-configure/references/azure-credential-population.md +42 -28
- package/skills/service-itsm-teams-configure/references/gotchas.md +1 -2
- package/skills/service-itsm-teams-coordinate/SKILL.md +22 -18
- package/skills/service-itsm-teams-coordinate/examples/output-templates.md +12 -9
- package/skills/service-itsm-teams-itdesk-configure/SKILL.md +60 -44
- package/skills/service-itsm-teams-itservice-configure/SKILL.md +56 -70
- package/skills/service-catalog-template-deploy/SKILL.md +0 -310
- package/skills/service-catalog-template-deploy/references/cli-invocation.md +0 -258
- package/skills/service-catalog-template-deploy/scripts/activate-verify.mjs +0 -164
- package/skills/service-catalog-template-deploy/scripts/build-deploy-payload.mjs +0 -94
- package/skills/service-catalog-template-deploy/scripts/resolve-template.mjs +0 -331
- package/skills/service-catalog-template-search/SKILL.md +0 -212
- package/skills/service-catalog-template-search/references/cli-invocation.md +0 -128
- package/skills/service-catalog-template-search/scripts/classify-catalog.mjs +0 -205
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: consumer-goods-tpe-dashboard-custom-kpi-configure
|
|
3
|
+
description: "Customize the Trade Promotion Effectiveness (TPE) Analytics dashboards' generic KPI slots (Promotion Measure 1-3, Tactic Measure 1-2) with customer-chosen KPI measure codes and display names, without touching the shipped base model. Use when a customer/admin wants to \"customize TPE dashboards\", \"change the KPIs on the Promotion/Tactic Analysis dashboard\", or \"swap in our own measure codes on the TPE dashboard\"."
|
|
4
|
+
metadata:
|
|
5
|
+
version: "1.0"
|
|
6
|
+
domains: ["Consumer Goods"]
|
|
7
|
+
relatedSkills:
|
|
8
|
+
- "consumer-goods-tpe-dashboard-configure"
|
|
9
|
+
cliTools:
|
|
10
|
+
- tool: ["node"]
|
|
11
|
+
semver: ">=20.0.0"
|
|
12
|
+
- tool: ["sf"]
|
|
13
|
+
semver: ">=2.0.0"
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Customize TPE Dashboard KPIs
|
|
17
|
+
|
|
18
|
+
Headlessly clones the base TPE Analytics semantic-model parameters, visualizations, and dashboards
|
|
19
|
+
under a customer-chosen suffix, so a customer's own KPI measure codes appear on the Promotion/Tactic
|
|
20
|
+
Analysis dashboards without ever mutating the shipped base model. Driven entirely by `sf` CLI +
|
|
21
|
+
REST against an already-authenticated org. Requires `consumer-goods-tpe-dashboard-configure` to
|
|
22
|
+
have already installed the TPE Analytics app and the Extended TPM Analytics SDM — this skill never
|
|
23
|
+
installs those, only customizes on top of them.
|
|
24
|
+
|
|
25
|
+
Persona: a **Salesforce TPM System Admin** customizing TPE dashboards for their org's own KPIs.
|
|
26
|
+
|
|
27
|
+
There is no native "clone" endpoint for any of the three artifact types this skill touches
|
|
28
|
+
(semantic-model parameters, visualizations, dashboards) — every clone is the same three-step
|
|
29
|
+
pattern: **GET** the base artifact, **deep-copy** its JSON and apply overrides, **POST** the copy as
|
|
30
|
+
a new artifact. Base-model artifacts are never mutated in place.
|
|
31
|
+
|
|
32
|
+
## The 5 KPI slots — two artifacts per slot, not one
|
|
33
|
+
|
|
34
|
+
The shipped Extended TPM Analytics SDM exposes 5 generic **List-type parameters** that the standard
|
|
35
|
+
dashboards bind to: Promotion Measure 1-3 (`Promotion_Measure_{1,2,3}_prm` / `_value`) and Tactic
|
|
36
|
+
Measure 1-2 (`Tactic_Measure_{1,2}_prm` / `_clc`). **Never assume Tactic mirrors Promotion's
|
|
37
|
+
`_value` naming** — resolve both slots' measurement apiNames from the live model per slot, never by
|
|
38
|
+
pattern. See `references/payload-shapes.md` for the confirmed apiNames and JSON shapes.
|
|
39
|
+
|
|
40
|
+
Each slot is **two SDM artifacts**: the **parameter** (`*_prm`, a `List` type with `allowedValues`
|
|
41
|
+
menu entries and a `defaultValue`) and the **calculated measurement** (what visualizations actually
|
|
42
|
+
query; its `expression` links back to the parameter by apiName). **Customizing a slot means cloning
|
|
43
|
+
both** — the parameter gets the customer's KPI as a new/selected `allowedValues` entry, and the
|
|
44
|
+
measurement's `expression` is rewritten to reference the cloned parameter's new apiName.
|
|
45
|
+
|
|
46
|
+
This skill always produces clones of **all 5 slots (10 artifacts)**, whether or not a given slot has
|
|
47
|
+
a requested override — an unmentioned slot is still cloned as an exact copy of the base artifacts
|
|
48
|
+
(only apiName/label suffixed).
|
|
49
|
+
|
|
50
|
+
Two override modes per slot (see `references/procedure.md` for exact `--overrides` shapes):
|
|
51
|
+
|
|
52
|
+
- **APPEND mode** (default): upserts one `{measureCode, displayName}` entry into the base menu by
|
|
53
|
+
measure code — same code relabels in place, new code appends.
|
|
54
|
+
- **REPLACE mode**: a full `allowedValues` array supplied by the customer becomes the entire menu.
|
|
55
|
+
|
|
56
|
+
Either way this skill assumes the underlying measure data is already populated by prior
|
|
57
|
+
`setup-rtr-datacloud-export`/data-kit setup — it does not create that underlying data itself.
|
|
58
|
+
|
|
59
|
+
## Inputs to collect first
|
|
60
|
+
|
|
61
|
+
Ask before starting. Do not guess.
|
|
62
|
+
|
|
63
|
+
1. **TPM System Admin username** — verify connectivity via
|
|
64
|
+
`node ./scripts/sf-rest.js org-status --target-org <username>` (never `sf org display --json`
|
|
65
|
+
directly — that leaks a live `accessToken`). Non-`Connected` ⇒ stop, ask the user to log in.
|
|
66
|
+
2. **Dry-run?** — offer by default.
|
|
67
|
+
3. **Suffix** — distinguishes cloned metadata from the base model (e.g. `Cust`). Suggest `Cust` as a
|
|
68
|
+
default. Applied to every new artifact's API name/label — never reuse a base-model API name.
|
|
69
|
+
4. **Per-measure overrides** — walk the customer through the 5 measures via `AskUserQuestion`,
|
|
70
|
+
**batched by dashboard** (Promotion Measure 1-3 in one call, Tactic Measure 1-2 in a second call —
|
|
71
|
+
never all 5 in one call, the tool caps at 4 questions per call), never using the internal term
|
|
72
|
+
"slot" toward the user. **Never ask a separate plain free-text follow-up question to collect the
|
|
73
|
+
list** — a plain-text message doesn't block the next tool call, so if you fire another
|
|
74
|
+
`AskUserQuestion` for the next measure before the customer's reply lands, their answer races
|
|
75
|
+
ahead and lands on the wrong question. Instead, collect the full list **inside the same blocking
|
|
76
|
+
question**: every `AskUserQuestion` question automatically gets a "Type Something" free-text
|
|
77
|
+
option appended after your listed options — no need to add your own placeholder option for it,
|
|
78
|
+
and never rely on a bare canned option label (e.g. "Replace with custom list") to actually carry
|
|
79
|
+
the list, since selecting it only returns that label text, not the customer's data. Word the
|
|
80
|
+
question and its second option's description to point the customer at "Type Something"
|
|
81
|
+
explicitly, e.g. option 2 = "Replace with custom list", description: "Select **Type Something**
|
|
82
|
+
(last option, below) and type comma-separated `{measureCode}: {displayName}` pairs, plus which
|
|
83
|
+
one should be default." **A menu can have more than one entry — always collect the full array,
|
|
84
|
+
never just one pair** (a single-pair answer is still an array of length 1). This collected array
|
|
85
|
+
maps directly to that slot's REPLACE-mode override (`{"allowedValues":[...], "defaultValue":
|
|
86
|
+
"<first displayName unless the customer says otherwise>"}`) — see `references/procedure.md`'s
|
|
87
|
+
override-mode shapes. Confirm the resolved list per measure back to the user before Phase 0.
|
|
88
|
+
5. **Which dashboards to produce** — Promotion Analysis, Tactic Analysis, or both. Default to both
|
|
89
|
+
if any measure on either has an override; otherwise ask explicitly.
|
|
90
|
+
|
|
91
|
+
## How it works — phase list
|
|
92
|
+
|
|
93
|
+
1. **Namespace detection** — resolve `NS` via `sf package installed list` for consistency with
|
|
94
|
+
sibling skills, but never apply it to any `/services/data/...` path (those are never namespaced).
|
|
95
|
+
2. **Phase 0 — Preflight & discovery**: verify connectivity, pin API version `v67.0`, resolve the
|
|
96
|
+
installed app + asset ids (model, both dashboards, visualizations) by walking the app's asset
|
|
97
|
+
list — never hardcode an id. Block with a pointer to `consumer-goods-tpe-dashboard-configure` if
|
|
98
|
+
prerequisites are missing. Full detail: `references/procedure.md`.
|
|
99
|
+
3. **Phase 1 — Resolve the 5-slot plan**: deep-copy each base parameter/measurement with overrides
|
|
100
|
+
applied, print the plan, get explicit user confirmation before any writes.
|
|
101
|
+
4. **Phases 2-4 — Clone parameters, measurements, visualizations, dashboards**: run
|
|
102
|
+
`scripts/clone-tpe-dashboards.js` (preferred over hand-rolling individual REST calls) — see
|
|
103
|
+
`references/procedure.md` for the full CLI usage, override-mode payload shapes, and every
|
|
104
|
+
real-write gotcha the script encodes (label uniqueness, dataSource `.type` stripping rules,
|
|
105
|
+
cloning every KPI-bound visualization not just one "designated" viz, cosmetic label-text
|
|
106
|
+
replacement, upsert-by-measure-code).
|
|
107
|
+
5. **Phase 5 — Verify**: GET every created artifact back and confirm parameters, measurements,
|
|
108
|
+
visualizations, and dashboards all correctly reference the new suffixed clones, never a base
|
|
109
|
+
apiName. Full detail: `references/procedure.md`.
|
|
110
|
+
|
|
111
|
+
## Key rules (see `references/procedure.md` for the complete list)
|
|
112
|
+
|
|
113
|
+
- Never mutate a base-model artifact — every write is a new POST of a suffixed copy.
|
|
114
|
+
- Never create anything without explicit user go-ahead on the Phase 1 plan.
|
|
115
|
+
- Always produce all 5 slots' worth of clones, even with zero overrides.
|
|
116
|
+
- Never leave a cloned measurement's `expression` referencing a base parameter apiName.
|
|
117
|
+
- Never hardcode an artifact id, api-name, or namespace across orgs.
|
|
118
|
+
- Never assume only one visualization per dashboard needs cloning — clone every widget whose viz
|
|
119
|
+
references a slot measurement.
|
|
120
|
+
|
|
121
|
+
## Reference map
|
|
122
|
+
|
|
123
|
+
| Topic | File |
|
|
124
|
+
|---|---|
|
|
125
|
+
| Full phase-by-phase procedure, script CLI usage, all real-write gotchas, complete Rules list | `references/procedure.md` |
|
|
126
|
+
| Confirmed live JSON shapes for parameters, calculated measurements, visualizations, dashboards, and POST body shapes for all 4 create endpoints | `references/payload-shapes.md` |
|
|
127
|
+
| Org REST helper (strips `accessToken`; `org-status` subcommand) | `scripts/sf-rest.js` |
|
|
128
|
+
| Deterministic driver for Phases 2-4 | `scripts/clone-tpe-dashboards.js` |
|
|
129
|
+
|
|
130
|
+
## Report
|
|
131
|
+
|
|
132
|
+
At the end of a run, give the user a structured status per phase (0-5): pass / blocked / pending,
|
|
133
|
+
with the specific blocking reason where applicable, and the final list of created artifacts (slot →
|
|
134
|
+
new parameter, base viz → new viz, base dashboard → new dashboard). Call out anything skipped due
|
|
135
|
+
to dry-run or a declined confirmation.
|
|
136
|
+
|
|
137
|
+
**When invoked via delegation** (the calling skill used the `Skill` tool to reach this file, rather
|
|
138
|
+
than the user directly — e.g. `consumer-goods-tpe-dashboard-configure`'s customization step): this
|
|
139
|
+
report is an intermediate result. Return it to the calling skill and let it continue with its own
|
|
140
|
+
report — do not present this as the final answer to the user unless this skill was invoked
|
|
141
|
+
standalone.
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
# Payload shapes — captured from a live test org (2026-08-17)
|
|
2
|
+
|
|
3
|
+
Captured against a live TPM test org (namespaced install, org apiVersion `68.0`). **API version note:** `v64.0` (assumed in the original SKILL.md draft) returns
|
|
4
|
+
`DOWNGRADE_VERSION_ERROR` on `GET /tableau/visualizations/{id}` in this org — "uses features that
|
|
5
|
+
aren't available in this version of the API." The initial capture pass used `v68.0` (the org's own
|
|
6
|
+
apiVersion at the time). **Per standing instruction, `scripts/clone-tpe-dashboards.js` and SKILL.md
|
|
7
|
+
now pin every call to `v67.0` instead of resolving the org's own apiVersion** — the `v68.0` examples
|
|
8
|
+
below reflect the original discovery capture and remain valid shape references, but the driver
|
|
9
|
+
script itself always sends `v67.0`.
|
|
10
|
+
|
|
11
|
+
## How to find the real artifact ids — don't guess names, walk the app's asset list
|
|
12
|
+
|
|
13
|
+
`GET /services/data/v68.0/app-framework/apps?templateSourceId=sfdc_internal__Trade_Promotion_Effectiveness`
|
|
14
|
+
returns the installed TPE_Analytics app(s) (`applicationStatus: "SuccessStatus"`). Each app object
|
|
15
|
+
has an `assetUrl` (`/services/data/v68.0/app-framework/apps/{appId}/assets`). GET that — it returns
|
|
16
|
+
every asset the install created, typed and named:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{"appId":"...", "assets":[
|
|
20
|
+
{"id":"1zB...","type":"Dataspace", ...},
|
|
21
|
+
{"id":"1zB...","type":"Workspace","templateAssetSourceName":"Trade_Promotion_Effectiveness_Analytics", ...},
|
|
22
|
+
{"id":"1zB...","type":"SemanticModel","templateAssetSourceName":"Trade_Promotion_Management_Analytics", ...},
|
|
23
|
+
{"id":"1zB...","type":"SemanticModel","templateAssetSourceName":"Extended_Trade_Promotion_Management_Analytics","assetIdOrName":"2SMVW000000BkJT4A0","assetIdOrName2":"Extended_Trade_Promotion_Managemen_4a9"},
|
|
24
|
+
{"id":"1zB...","type":"Visualization","templateAssetSourceName":"Promotion_Performance_Measures","assetIdOrName":"1AKVW000004YvHl4AK", ...},
|
|
25
|
+
{"id":"1zB...","type":"Visualization","templateAssetSourceName":"Tactic_Performance_Measures", ...},
|
|
26
|
+
{"id":"1zB...","type":"Dashboard","templateAssetSourceName":"PromotionAnalysis","assetIdOrName":"0TrVW0000004Fi10AE", ...},
|
|
27
|
+
{"id":"1zB...","type":"Dashboard","templateAssetSourceName":"TacticAnalysis","assetIdOrName":"0TrVW0000004Fi20AE", ...},
|
|
28
|
+
... 17 more Visualization assets ...
|
|
29
|
+
]}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Each asset's `assetIdOrName` is the real record Id to `GET` directly (e.g.
|
|
33
|
+
`/tableau/visualizations/{assetIdOrName}`, `/tableau/dashboards/{assetIdOrName}`, or
|
|
34
|
+
`/ssot/semantic/models/{assetIdOrName}`). This is the authoritative way for Phase 0 to resolve
|
|
35
|
+
"which Extended SDM / which base dashboards / which base visualizations" for a given org — never
|
|
36
|
+
hardcode an id or api-name across orgs.
|
|
37
|
+
|
|
38
|
+
## Extended TPM Analytics SDM — GET the whole model, not per-parameter
|
|
39
|
+
|
|
40
|
+
`GET /services/data/v68.0/ssot/semantic/models/{modelId}` (e.g. `2SMVW000000BkJT4A0`) returns the
|
|
41
|
+
**entire model** in one response — there is no working single-parameter GET confirmed; read the
|
|
42
|
+
whole model and index into `semanticParameters[]` / `semanticCalculatedMeasurements[]` by
|
|
43
|
+
`apiName`. Top-level keys of interest: `apiName`, `semanticParameters`, `semanticCalculatedMeasurements`,
|
|
44
|
+
`semanticCalculatedDimensions`, `semanticDataObjects`, `semanticMetrics`, `semanticRelationships`.
|
|
45
|
+
|
|
46
|
+
### `semanticParameters[]` — exactly 5 entries in the base Extended model, one per slot
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"allowedValues": [
|
|
51
|
+
{"displayName": "Actual Gross Profit", "value": "PATP"},
|
|
52
|
+
{"displayName": "Actual Gross Revenue", "value": "TARE"},
|
|
53
|
+
{"displayName": "Actual Incr. Gross Revenue", "value": "PAIR"},
|
|
54
|
+
{"displayName": "Actual Non-Promo Spend", "value": "PATL"},
|
|
55
|
+
{"displayName": "Actual Profit Margin %", "value": "PAPM"},
|
|
56
|
+
{"displayName": "Actual Total Promo Spend", "value": "PAPS"},
|
|
57
|
+
{"displayName": "Actual Total Volume", "value": "TAVO"},
|
|
58
|
+
{"displayName": "LE Gross Profit", "value": "PLTP"},
|
|
59
|
+
{"displayName": "LE Gross Revenue", "value": "PLTR"},
|
|
60
|
+
{"displayName": "LE Incr. Gross Revenue", "value": "PLIR"},
|
|
61
|
+
{"displayName": "LE Non-Promo Spend", "value": "PLTL"},
|
|
62
|
+
{"displayName": "LE Total Promo Spend", "value": "PLPS"},
|
|
63
|
+
{"displayName": "LE Total Volume", "value": "PLTV"}
|
|
64
|
+
],
|
|
65
|
+
"apiName": "Promotion_Measure_1_prm",
|
|
66
|
+
"baseModelApiName": "Trade_Promotion_Management_Analyti_4a9",
|
|
67
|
+
"dataType": "Text",
|
|
68
|
+
"defaultValue": "Actual Total Promo Spend",
|
|
69
|
+
"id": "1DOVW000000pMdP4AU",
|
|
70
|
+
"label": "Promotion Measure 1",
|
|
71
|
+
"type": "List",
|
|
72
|
+
"values": []
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Corrected understanding — this is NOT a single measure-code field to overwrite.** A slot
|
|
77
|
+
parameter is a **List** parameter whose `allowedValues` is the full menu of KPI measure codes the
|
|
78
|
+
end user can pick at runtime (`value` = 4-char measure code, `displayName` = label shown in the UI),
|
|
79
|
+
and `defaultValue` is which `displayName` is preselected. The 13 entries above already cover the
|
|
80
|
+
base model's standard KPIs — these are **not customer input**, they're what ships.
|
|
81
|
+
|
|
82
|
+
**What "customer measure code + display name" actually means for this skill:** the customer is
|
|
83
|
+
either (a) picking a **different existing** `allowedValues` entry as the new default (a pure
|
|
84
|
+
`defaultValue` swap, no new entry needed), or (b) introducing a **genuinely new** KPI not in the
|
|
85
|
+
base 13, which requires **appending** a new `{displayName, value}` entry to `allowedValues` *and*
|
|
86
|
+
setting `defaultValue` to that new `displayName`. Either way, the underlying `value` (measure code)
|
|
87
|
+
must resolve against real data already reachable from the semantic model (the `Measure_Definition`
|
|
88
|
+
dimension referenced by the calculated measurement below) — this skill does not create that
|
|
89
|
+
underlying data; it assumes `setup-rtr-datacloud-export`/data-kit setup already populated it.
|
|
90
|
+
Never guess whether a code is "new" — check it against the base `allowedValues` first.
|
|
91
|
+
|
|
92
|
+
### `semanticCalculatedMeasurements[]` — the field a visualization actually references
|
|
93
|
+
|
|
94
|
+
Each slot has a matching calculated measurement (`Promotion_Measure_1_value`,
|
|
95
|
+
`Promotion_Measure_2_value`, `Promotion_Measure_3_value`, and presumably `Tactic_Measure_1_value`,
|
|
96
|
+
`Tactic_Measure_2_value` on the Tactic side — confirm both dashboards' sets during a real run):
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"aggregationType": "Sum",
|
|
101
|
+
"apiName": "Promotion_Measure_1_value",
|
|
102
|
+
"baseModelApiName": "Trade_Promotion_Management_Analyti_4a9",
|
|
103
|
+
"dataType": "Number",
|
|
104
|
+
"directionality": "Up",
|
|
105
|
+
"expression": "IF [Parameters].[Promotion_Measure_1_prm] == [Measure_Definition].[Measure_Code] THEN [Promotion_Product_Measure].[Measure_Value]\nELSE null\nEND",
|
|
106
|
+
"id": "1DOVW000000pMct4AE",
|
|
107
|
+
"isOverrideBase": false,
|
|
108
|
+
"isQueryable": "Queryable",
|
|
109
|
+
"isVisible": true,
|
|
110
|
+
"label": "Promotion Measure 1",
|
|
111
|
+
"level": "Row",
|
|
112
|
+
"sentiment": "SentimentTypeUpIsGood",
|
|
113
|
+
"shouldTreatNullsAsZeros": false,
|
|
114
|
+
"totalAggregationType": "Sum"
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**This is the critical corrected finding versus the original SKILL.md draft: visualizations and
|
|
119
|
+
dashboard `parameter` widgets reference the *parameter* directly by `apiName`
|
|
120
|
+
(`Promotion_Measure_1_prm`), but visualizations reference the *calculated measurement* by `apiName`
|
|
121
|
+
(`Promotion_Measure_1_value`) — two different artifacts, two different apiNames, linked to each
|
|
122
|
+
other only through the measurement's `expression` string** (`[Parameters].[Promotion_Measure_1_prm]`).
|
|
123
|
+
**Cloning a slot means cloning BOTH the parameter and its calculated measurement** — the cloned
|
|
124
|
+
measurement's `expression` must be rewritten to reference the cloned parameter's new apiName, not
|
|
125
|
+
the base one.
|
|
126
|
+
|
|
127
|
+
Create endpoints (unverified against a live POST in this pass — confirm request-body shape matches
|
|
128
|
+
the GET shape above before relying on it, per SKILL.md Phase 0 Rules):
|
|
129
|
+
- `POST /services/data/v68.0/ssot/semantic/models/{modelApiName}/parameters`
|
|
130
|
+
- `POST /services/data/v68.0/ssot/semantic/models/{modelApiName}/calculated-measurements`
|
|
131
|
+
|
|
132
|
+
## Visualization — `GET /services/data/v68.0/tableau/visualizations/{id}`
|
|
133
|
+
|
|
134
|
+
Top-level keys: `createdBy`, `createdDate`, `dataSource`, `description`, `fields`, `id`,
|
|
135
|
+
`interactions`, `label`, `lastModifiedBy`, `lastModifiedDate`, `name`, `permissions`,
|
|
136
|
+
`sourceVersion`, `templateSource`, `view`, `visualSpecification`, `workspace`.
|
|
137
|
+
|
|
138
|
+
- `dataSource.name` — the semantic model's apiName (`Extended_Trade_Promotion_Managemen_4a9`).
|
|
139
|
+
Stays the same for a customer clone (same Extended model, not a new one).
|
|
140
|
+
- `fields.{F1..Fn}` — each field entry may have a `fieldName` pointing at a calculated measurement
|
|
141
|
+
apiName (e.g. `"fieldName": "Promotion_Measure_1_value"`) plus a `label` that's a cosmetic string
|
|
142
|
+
literally embedding the *parameter's* **label** (not its apiName), e.g.
|
|
143
|
+
`"<[Parameters].[Promotion Measure 1]>"` (stored HTML-double-escaped as
|
|
144
|
+
`"&lt;[Parameters].[Promotion Measure 1]&gt;"`). **Confirmed live this must be updated,
|
|
145
|
+
not left as-is — see the `Cust5` section below.** An apiName-only string replace never touches
|
|
146
|
+
this text (it contains no apiName substring), so it silently keeps showing the base parameter's
|
|
147
|
+
label after a clone unless the parameter's label is *also* replaced inside the
|
|
148
|
+
`[Parameters].[<label>]` pattern.
|
|
149
|
+
- **The `Promotion_Measure_1_value` apiName string can also appear inside expression strings**,
|
|
150
|
+
e.g. `view.viewSpecification.aggregateFilter.filters[].filter.fields[].expression`:
|
|
151
|
+
`"SUM([Promotion_Measure_1_value])"`. **A structural find-and-replace on `fields[].fieldName`
|
|
152
|
+
alone is not sufficient** — Phase 3's clone-and-swap must do a full-JSON string replace of the
|
|
153
|
+
base calculated-measurement apiName with the new cloned one, not just patch `fieldName` fields.
|
|
154
|
+
Do the replace on the longest/most-specific apiNames first to avoid partial-substring collisions
|
|
155
|
+
if slot-count ever grows past single digits (not an issue with the current 5 slots, still good
|
|
156
|
+
practice).
|
|
157
|
+
- Fields to strip before POSTing a clone: `id`, `createdBy`, `createdDate`, `lastModifiedBy`,
|
|
158
|
+
`lastModifiedDate`, `permissions`, `templateSource`, `sourceVersion`, `view.id`,
|
|
159
|
+
`view.viewSpecification` sub-ids if present — **unconfirmed which fields the create endpoint
|
|
160
|
+
actually rejects vs. ignores; verify with a real trial POST before assuming this strip list is
|
|
161
|
+
complete or correct.**
|
|
162
|
+
- `name` is the visualization's unique apiName — must be suffixed for the clone
|
|
163
|
+
(`Promotion_Performance_Measures` → `Promotion_Performance_Measures_<Suffix>`).
|
|
164
|
+
|
|
165
|
+
Create endpoint (unverified against a live POST in this pass):
|
|
166
|
+
`POST /services/data/v68.0/tableau/visualizations`
|
|
167
|
+
|
|
168
|
+
## Dashboard — `GET /services/data/v68.0/tableau/dashboards/{id}`
|
|
169
|
+
|
|
170
|
+
Top-level keys: `createdBy`, `createdDate`, `customViews`, `description`, `id`, `label`,
|
|
171
|
+
`lastModifiedBy`, `lastModifiedDate`, `layouts`, `name`, `permissions`, `style`, `templateSource`,
|
|
172
|
+
`url`, `widgets`, `workspaceIdOrApiName`.
|
|
173
|
+
|
|
174
|
+
`widgets` is a map keyed by widget name (`visualization_1`, `parameter_3`, `filter_2`, `text_18`,
|
|
175
|
+
`container_1`, ...). **Three widget `type`s matter for the swap:**
|
|
176
|
+
|
|
177
|
+
- **`type: "visualization"`** — `widget.source = {id, label, name, type: "Visualization"}` points
|
|
178
|
+
at the base visualization by `id`/`name`. Must be repointed at the Phase 3 cloned visualization's
|
|
179
|
+
new `id`/`name`.
|
|
180
|
+
- **`type: "parameter"`** — this is the on-dashboard KPI picker control the end user interacts
|
|
181
|
+
with. `widget.parameters.parameterName = "Promotion_Measure_1_prm"` (the parameter's apiName,
|
|
182
|
+
directly — not the measurement). `widget.source` points at the SemanticModel (unchanged — same
|
|
183
|
+
Extended model). Must repoint `parameterName` at the Phase 2 cloned parameter's new apiName.
|
|
184
|
+
Confirmed 3 such widgets on `PromotionAnalysis` (`parameter_3/4/5` → `Promotion_Measure_1/2/3_prm`
|
|
185
|
+
respectively) with human-friendly `label`s ("Primary Measure", "Compare Measure", "Measure For
|
|
186
|
+
Bubble Plot") distinct from the parameter's own `label` — preserve those dashboard-widget labels
|
|
187
|
+
as-is on clone unless the customer wants them renamed too.
|
|
188
|
+
- **`type: "text"`** with a `dynamicTokens` map — cosmetic KPI-value callouts
|
|
189
|
+
(`"label": "Sum(Promotion_Measure_1_value)"`, `source.name` = the Extended model). References a
|
|
190
|
+
calculated-measurement apiName inside the token's generated label/expression. Not required for
|
|
191
|
+
the dashboard to function correctly post-swap, but leaving these unswapped means the on-dashboard
|
|
192
|
+
text callouts still show the *base* KPI's computed value/label instead of the customer's chosen
|
|
193
|
+
one — cosmetic drift, not a functional bug. Decide during implementation whether to swap these
|
|
194
|
+
too for full fidelity, or explicitly document them as a known-cosmetic gap.
|
|
195
|
+
- `layouts[].pages[].widgets[]` reference widgets **by `name`** (the same key as in the `widgets`
|
|
196
|
+
map) — since a clone keeps the same widget names/layout, **layouts require no changes**, only
|
|
197
|
+
the referenced widgets' `source`/`parameterName` do.
|
|
198
|
+
|
|
199
|
+
Fields to strip before POSTing a clone: `id`, `createdBy`, `createdDate`, `lastModifiedBy`,
|
|
200
|
+
`lastModifiedDate`, `permissions`, `templateSource`, `url` — **unverified, confirm with a live trial
|
|
201
|
+
POST.**
|
|
202
|
+
|
|
203
|
+
Create endpoint (unverified against a live POST in this pass):
|
|
204
|
+
`POST /services/data/v68.0/tableau/dashboards`
|
|
205
|
+
|
|
206
|
+
## CORRECTED: Tactic-side calculated measurements are named `_clc`, not `_value`
|
|
207
|
+
|
|
208
|
+
Confirmed live (2026-08-17, second real trial-write pass): the Promotion slots' calculated
|
|
209
|
+
measurements are `Promotion_Measure_{1,2,3}_value`, but the **Tactic slots' calculated measurements
|
|
210
|
+
are `Tactic_Measure_1_clc` / `Tactic_Measure_2_clc`** — a different suffix, not the `_value` pattern
|
|
211
|
+
assumed by symmetry in the first discovery pass. There is no `Tactic_Measure_1_value` /
|
|
212
|
+
`Tactic_Measure_2_value` in the base model. Their `expression`s also reference a different measure
|
|
213
|
+
object (`[Promotion_Offer_Product_Measure2].[Measure_Value3]`, not
|
|
214
|
+
`[Promotion_Product_Measure].[Measure_Value]`). Confirmed via
|
|
215
|
+
`GET /ssot/semantic/models/Extended_Trade_Promotion_Managemen_4a91` and cross-checked against the
|
|
216
|
+
`Tactic_Performance_Measures` visualization's `fields[].fieldName`, which reference `_clc` apiNames
|
|
217
|
+
directly. **Never assume Tactic mirrors Promotion's `_value` naming — always resolve both slots'
|
|
218
|
+
measurement apiNames from the live model, per slot, never by pattern.**
|
|
219
|
+
|
|
220
|
+
## CONFIRMED: real POST body shapes (live trial writes, 2026-08-17)
|
|
221
|
+
|
|
222
|
+
All four create endpoints were exercised for a full 5-slot clone (suffix `Cust`) and succeeded.
|
|
223
|
+
**The create body is a small curated subset of the GET shape — not the GET response minus
|
|
224
|
+
`id`/audit fields.** Passing extra GET-only fields causes `JSON_PARSER_ERROR` (`"Unrecognized
|
|
225
|
+
field ..."`), one field at a time (Jackson strict deserialization — it reports only the first
|
|
226
|
+
unrecognized field per request, so a naive strip-and-retry loop surfaces one bad field per attempt).
|
|
227
|
+
|
|
228
|
+
### `POST /ssot/semantic/models/{modelApiName}/parameters`
|
|
229
|
+
|
|
230
|
+
Minimal accepted body — no `id`, `baseModelApiName`, `createdBy/Date`, `lastModifiedBy/Date`,
|
|
231
|
+
`overriddenProperties`, `values`:
|
|
232
|
+
```json
|
|
233
|
+
{
|
|
234
|
+
"apiName": "Promotion_Measure_1_prm_Cust",
|
|
235
|
+
"label": "Promotion Measure 1",
|
|
236
|
+
"type": "List",
|
|
237
|
+
"dataType": "Text",
|
|
238
|
+
"allowedValues": [ /* full array, base entries + any new/changed entry */ ],
|
|
239
|
+
"defaultValue": "Actual Total Promo Spend Cust"
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
Response echoes back the full GET shape (adds `id`, `createdBy`, etc.) — HTTP 200, synchronous, no
|
|
243
|
+
poll needed.
|
|
244
|
+
|
|
245
|
+
### `POST /ssot/semantic/models/{modelApiName}/calculated-measurements`
|
|
246
|
+
|
|
247
|
+
Minimal accepted body — no `id`, `baseModelApiName`, `createdBy/Date`, `lastModifiedBy/Date`,
|
|
248
|
+
`overriddenProperties`, `filters`, `decimalPlace`, `displayCategory`, `semanticDataType`,
|
|
249
|
+
`sortOrder`:
|
|
250
|
+
```json
|
|
251
|
+
{
|
|
252
|
+
"apiName": "Promotion_Measure_1_value_Cust",
|
|
253
|
+
"label": "Promotion Measure 1",
|
|
254
|
+
"dataType": "Number",
|
|
255
|
+
"aggregationType": "Sum",
|
|
256
|
+
"totalAggregationType": "Sum",
|
|
257
|
+
"level": "Row",
|
|
258
|
+
"isQueryable": "Queryable",
|
|
259
|
+
"isVisible": true,
|
|
260
|
+
"directionality": "Up",
|
|
261
|
+
"sentiment": "SentimentTypeUpIsGood",
|
|
262
|
+
"shouldTreatNullsAsZeros": false,
|
|
263
|
+
"expression": "IF [Parameters].[Promotion_Measure_1_prm_Cust] == [Measure_Definition].[Measure_Code] THEN [Promotion_Product_Measure].[Measure_Value]\nELSE null\nEND"
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
Create the parameter first — the expression must reference the parameter's *final* apiName.
|
|
267
|
+
Synchronous, HTTP 200, no poll needed.
|
|
268
|
+
|
|
269
|
+
### `POST /tableau/visualizations`
|
|
270
|
+
|
|
271
|
+
Take the full GET response and:
|
|
272
|
+
1. `del(.id, .createdBy, .createdDate, .lastModifiedBy, .lastModifiedDate, .permissions, .sourceVersion, .templateSource, .view.id, .view.isOriginal)`
|
|
273
|
+
2. Recursively (`walk`) `del(.id, .url, .permissions, .createdBy, .createdDate, .lastModifiedBy, .lastModifiedDate, .sourceVersion, .templateSource)` at every nested object.
|
|
274
|
+
3. Do the full-JSON string replace of every base calculated-measurement apiName with its clone
|
|
275
|
+
(covers `fields[].fieldName` and the `aggregateFilter` expression string in one pass).
|
|
276
|
+
4. Set `.name` to the suffixed apiName.
|
|
277
|
+
`dataSource` stays the same Extended model reference (with only `.name` surviving the strip — no
|
|
278
|
+
`.url`). Synchronous, HTTP 200, no poll needed.
|
|
279
|
+
|
|
280
|
+
### `POST /tableau/dashboards`
|
|
281
|
+
|
|
282
|
+
Take the full GET response and:
|
|
283
|
+
1. `del(.customViews)` — rejected outright, not just its `id`/audit sub-fields.
|
|
284
|
+
2. Recursively `del(.id, .url, .permissions, .createdBy, .createdDate, .lastModifiedBy, .lastModifiedDate, .sourceVersion, .templateSource, .status)` at every nested object — `.status` ("Ok") on every widget is rejected.
|
|
285
|
+
3. Recursively, for any object whose `.type` is `"Visualization"` or `"SemanticModel"` (i.e. every
|
|
286
|
+
`source`/`dynamicTokens[].source` reference), `del(.label, .type)` — these ref objects accept
|
|
287
|
+
**only `.name`** (plus `.id` if you want to also pass it, but it's optional/ignored on create;
|
|
288
|
+
this pass omitted it after the generic id-strip and it worked). **Do NOT delete `.type` on the
|
|
289
|
+
widget object itself** (`widgets.*.type`, e.g. `"text"`/`"visualization"`/`"parameter"`/`"filter"`/`"container"`)
|
|
290
|
+
— that one is the required polymorphic discriminator
|
|
291
|
+
(`sfdc.unified.analytics.connect.api.input.widgets.AnalyticsWidgetInputRepresentation`); deleting
|
|
292
|
+
it errors with `"missing property 'type' that is to contain type id"`.
|
|
293
|
+
4. String-replace every base parameter/measurement apiName occurrence (parameter widgets'
|
|
294
|
+
`parameters.parameterName`, text widgets' `dynamicTokens.*.tokenSpec.fieldName` and `.label`,
|
|
295
|
+
and the same string inside `content[].insert.token.label`) with the suffixed one.
|
|
296
|
+
5. Patch the specific `visualization`-type widget(s)' `source.name` to the new cloned visualization
|
|
297
|
+
name (leave everything else about that widget, including its own `id`→already-stripped, alone).
|
|
298
|
+
6. Set `.name` to the suffixed apiName.
|
|
299
|
+
`layouts[]` needs no changes — it references widgets only by their unchanged `name` keys, confirmed
|
|
300
|
+
on both dashboards. Synchronous, HTTP 200, no poll needed.
|
|
301
|
+
|
|
302
|
+
## Resolved open questions
|
|
303
|
+
|
|
304
|
+
- **POST body shapes**: confirmed above for all four endpoints, via a real 5-slot clone (Promotion
|
|
305
|
+
Measure 1-3 + Tactic Measure 1-2, suffix `Cust`) that ran end-to-end and was re-verified with GETs.
|
|
306
|
+
- **Synchronous vs. async**: all four create calls return the created resource directly, HTTP 200 —
|
|
307
|
+
no polling needed for any of the four (unlike the `app-framework/apps` install flow elsewhere in
|
|
308
|
+
this skill family).
|
|
309
|
+
- **apiName uniqueness**: confirmed — enforced. A same-suffix re-run against already-cloned
|
|
310
|
+
`apiName`s fails on the parameters POST with `CREATE_SEMANTIC_ENTITY_FAILED`. Always use a suffix
|
|
311
|
+
that hasn't been used before in that model, or delete the prior clone set first.
|
|
312
|
+
- **Tactic dashboard/visualization shape symmetry**: confirmed — `TacticAnalysis`/`Tactic_Performance_Measures`
|
|
313
|
+
follow the same widget/field shapes as the Promotion side, with the one naming difference noted
|
|
314
|
+
above (`_clc` vs `_value`).
|
|
315
|
+
|
|
316
|
+
## CONFIRMED (2026-08-17, second real end-to-end run, suffix `Cust3`): two more real-write bugs found and fixed
|
|
317
|
+
|
|
318
|
+
The full `scripts/clone-tpe-dashboards.js` driver (see that file) was run for real, twice, after the
|
|
319
|
+
manual `Cust` pass above. Both runs surfaced additional real errors not caught by the manual pass —
|
|
320
|
+
manual jq/curl trial-and-error does not automatically generalize into working reusable code; always
|
|
321
|
+
re-verify a generalized script against a real write, not just a dry-run.
|
|
322
|
+
|
|
323
|
+
1. **Parameter/measurement `label` must also be suffixed — it's uniqueness-checked, not just `apiName`.**
|
|
324
|
+
A second parameter clone reusing the base `label` verbatim (`"Promotion Measure 1"`, matching the
|
|
325
|
+
shape shown for `Cust` above) failed on a *third* create with `ENTITY_NOT_VALID` /
|
|
326
|
+
`CREATE_SEMANTIC_ENTITY_FAILED`: `"The Promotion Measure 1 label already exists in parameter:
|
|
327
|
+
Promotion Measure 1."` The base parameter and the first (`Cust`) clone apparently got away with a
|
|
328
|
+
shared label, but a second clone with the same label did not — behavior that looks order/count
|
|
329
|
+
dependent, not deterministic, so don't rely on "it worked once" for the label field. **Fix: always
|
|
330
|
+
suffix the label too** (`` `${baseParam.label} ${suffix}` ``, `` `${baseMeasurement.label} ${suffix}` ``),
|
|
331
|
+
not just the `apiName`. The `Cust`-suffixed artifacts created earlier in this file's capture have
|
|
332
|
+
an unsuffixed label and were not corrected retroactively — a future consumer should treat that
|
|
333
|
+
specific `Cust` pass as label-non-conformant, and only the `_clc`/`_value` naming + body-shape
|
|
334
|
+
findings from it as authoritative.
|
|
335
|
+
2. **The visualization body's `dataSource` ref must keep its `.type` — the dashboard-only strip rule
|
|
336
|
+
does not apply to visualizations.** Applying the dashboard's "strip `.label`/`.type` from any
|
|
337
|
+
object whose own `.type` is `Visualization`/`SemanticModel`" rule (section above) to the
|
|
338
|
+
visualization POST body too (reusing the same helper function) strips `dataSource.type`
|
|
339
|
+
(`"SemanticModel"`) and fails with `INVALID_INPUT: "Value required for [type]."` The visualization
|
|
340
|
+
create body needs only the generic `id/url/permissions/createdBy/createdDate/lastModifiedBy/
|
|
341
|
+
lastModifiedDate/sourceVersion/templateSource/status` strip (recursive) plus `view.isOriginal` —
|
|
342
|
+
never the source-ref `.label`/`.type` strip. That stricter rule is dashboard-widget-specific only.
|
|
343
|
+
|
|
344
|
+
Both fixes are applied in `scripts/clone-tpe-dashboards.js`; a full real run with a fresh suffix
|
|
345
|
+
(`Cust3`) after these fixes created 5 parameters, 5 measurements, 2 visualizations, and 2 dashboards
|
|
346
|
+
successfully end-to-end, and a follow-up GET on the cloned Tactic dashboard confirmed its
|
|
347
|
+
`visualization`-type widget's `source.name` and both `parameter`-type widgets' `parameterName` all
|
|
348
|
+
correctly point at the new `_Cust3` artifacts. **This "2 visualizations" count and the "every
|
|
349
|
+
unrelated widget left untouched" framing were both wrong — see the `Cust4` section below**, which
|
|
350
|
+
corrects a real functional gap this `Cust3` verification pass missed (it only checked the one
|
|
351
|
+
designated viz widget, not every widget on the dashboard).
|
|
352
|
+
|
|
353
|
+
## CORRECTED (2026-08-17, third real end-to-end run, suffix `Cust4`): must clone EVERY visualization on the dashboard, not just one "designated" one
|
|
354
|
+
|
|
355
|
+
The original design assumption — one app-asset visualization per dashboard
|
|
356
|
+
(`templateAssetSourceName: "Promotion_Performance_Measures"` / `"Tactic_Performance_Measures"`) is
|
|
357
|
+
"the" slot visualization to clone — is **wrong**. Confirmed live by scanning every `visualization`-type
|
|
358
|
+
widget on both base dashboards (`PromotionAnalysis1`: 9 widgets; `TacticAnalysis1`: 7 widgets) for
|
|
359
|
+
occurrences of any of the 5 base calculated-measurement apiNames: **every single one** of those 16
|
|
360
|
+
visualizations (top/bottom performers, trend charts, category/product breakdowns — not just the
|
|
361
|
+
one obviously-named "Performance_Measures" viz) references at least one of the 5 slot measurements
|
|
362
|
+
in its `fields[]`. A `Cust3`-generation clone that only cloned and repointed the one designated viz
|
|
363
|
+
left the other 8 (Promotion) / 6 (Tactic) charts on the cloned dashboard still bound to the **base**
|
|
364
|
+
measurement/parameter — those charts would silently keep reflecting the base KPI selection, out of
|
|
365
|
+
sync with the customer's chosen measure code, even though the dashboard itself was a "clone."
|
|
366
|
+
|
|
367
|
+
**Fix, now in `scripts/clone-tpe-dashboards.js`:** for each dashboard, walk its base `widgets{}`
|
|
368
|
+
directly (never a hardcoded/guessed viz template name), collect every distinct `visualization`-type
|
|
369
|
+
widget's source viz, GET each one, and clone **only those whose JSON contains at least one of that
|
|
370
|
+
dashboard's slot measurement apiNames** (a widget with no such reference is genuinely unrelated —
|
|
371
|
+
e.g. a purely-narrative chart with no KPI binding — and is correctly left pointing at its base viz,
|
|
372
|
+
though no such widget was actually found on either TPE dashboard in this org). Then, when building
|
|
373
|
+
the cloned dashboard body, repoint **every** `visualization`-type widget whose base source name has
|
|
374
|
+
a clone, not just the one previously assumed.
|
|
375
|
+
|
|
376
|
+
A full real run with suffix `Cust4` created 5 parameters, 5 measurements, **16 visualizations** (9
|
|
377
|
+
Promotion + 7 Tactic), and 2 dashboards. A follow-up GET scanning every widget on both cloned
|
|
378
|
+
dashboards confirmed **100% of visualization widgets (9/9 Promotion, 7/7 Tactic) and all 5 parameter
|
|
379
|
+
widgets** point at `_Cust4` clones, with zero widgets left pointing at a base artifact.
|
|
380
|
+
|
|
381
|
+
## FIXED (2026-08-17, fourth real end-to-end run, suffix `Cust5`): cosmetic `fields[].label` text was not being updated
|
|
382
|
+
|
|
383
|
+
Reported by direct inspection of `Promotion_Performance_Measures2_Cust4`: its `fieldName`s were
|
|
384
|
+
correctly repointed to `_Cust4` measurement apiNames, but every field's `label` still read the
|
|
385
|
+
unreplaced base text `"&lt;[Parameters].[Promotion Measure 1]&gt;"` (displays as
|
|
386
|
+
`<[Parameters].[Promotion Measure 1]>`) — the base parameter's **label**, not its apiName. Root
|
|
387
|
+
cause: `replaceApiNamesInJson`'s replacement pairs were built only from apiNames
|
|
388
|
+
(`baseParamApi`→`newParamApi`, `baseMeasurementApi`→`newMeasurementApi`); this cosmetic label string
|
|
389
|
+
contains neither, so it was never a match.
|
|
390
|
+
|
|
391
|
+
**Fix, now in `scripts/clone-tpe-dashboards.js`:** the plan now also carries `baseParamLabel` /
|
|
392
|
+
`newParamLabel` (`` `${baseParam.label} ${suffix}` ``, matching the already-suffixed parameter
|
|
393
|
+
`label` used in the create body). A third replacement-pair list, `labelReplacements`, replaces
|
|
394
|
+
`` `[Parameters].[${baseParamLabel}]` `` with `` `[Parameters].[${newParamLabel}]` `` (scoped to the
|
|
395
|
+
exact `[Parameters].[<label>]` bracket pattern, not a bare label substring, to avoid clobbering
|
|
396
|
+
unrelated text that happens to contain the same words). These pairs are folded into both the
|
|
397
|
+
per-visualization replacement pass (added to `relevantReplacements` whenever a viz's JSON contains
|
|
398
|
+
that pattern) and the dashboard body's `allReplacements`.
|
|
399
|
+
|
|
400
|
+
A full real run with suffix `Cust5` created 5 parameters, 5 measurements, 16 visualizations, and 2
|
|
401
|
+
dashboards. Follow-up GETs on `Promotion_Performance_Measures2_Cust5` and
|
|
402
|
+
`Tactic_Performance_Measures2_Cust5` confirmed every field's `label` now reads e.g.
|
|
403
|
+
`<[Parameters].[Promotion Measure 1 Cust5]>` / `<[Parameters].[Tactic Measure 1 Cust5]>` — matching
|
|
404
|
+
the suffixed parameter label — alongside the already-correct `fieldName` repointing.
|
|
405
|
+
|
|
406
|
+
## CONFIRMED (2026-08-17, fifth real end-to-end run, suffix `Cust6`): `--overrides` accepts a genuinely arbitrary customer-chosen measure code + display name
|
|
407
|
+
|
|
408
|
+
Every prior real run (`Cust`-`Cust5`) only exercised the *no-override* fallback path (base measure
|
|
409
|
+
code, base display name + suffix) — the `--overrides` path itself had never been exercised in a real
|
|
410
|
+
write. Ran `--overrides '{"Promotion_Measure_1":{"measureCode":"TARE","displayName":"My Custom KPI
|
|
411
|
+
Name"}}'` (an existing base allowedValues code, `TARE`/"Actual Gross Revenue", paired with a display
|
|
412
|
+
name unrelated to the suffix or the base label) end-to-end for real. Confirmed:
|
|
413
|
+
- `newParamBody.allowedValues` correctly appended `{"displayName":"My Custom KPI Name","value":"TARE"}`
|
|
414
|
+
alongside the base 13 entries, with `defaultValue: "My Custom KPI Name"`.
|
|
415
|
+
- The other 4 slots (no override supplied) fell back to the base-code + suffixed-label default, as
|
|
416
|
+
designed — overrides are per-slot, not all-or-nothing.
|
|
417
|
+
- Both dashboards, all repointed visualizations, and both parameter widgets built successfully with
|
|
418
|
+
the customer-chosen value in place. Mechanism: `slotOverride()` just substitutes the given
|
|
419
|
+
`measureCode`/`displayName` into the same `allowedValues`-append + `defaultValue` logic used for the
|
|
420
|
+
no-override case — there is no dependency anywhere on the value containing or matching the suffix.
|
|
421
|
+
|
|
422
|
+
## Added (2026-08-17): `--max-vizs-per-dashboard` debug flag
|
|
423
|
+
|
|
424
|
+
Added to `clone-tpe-dashboards.js` for faster local iteration — caps how many distinct
|
|
425
|
+
visualizations get cloned per dashboard; widgets beyond the cap are left pointing at base (same
|
|
426
|
+
code path as a genuinely-unrelated widget). Confirmed live with suffix `Dbg1` and
|
|
427
|
+
`--max-vizs-per-dashboard 1`: exactly 1 visualization cloned per dashboard (2 total, vs. the normal
|
|
428
|
+
16), every other visualization widget correctly left on base, both dashboards still created
|
|
429
|
+
successfully. Debug-only — never use on a real customer deliverable, since it leaves most charts
|
|
430
|
+
still bound to base measurements.
|
|
431
|
+
|
|
432
|
+
## CONFIRMED (2026-08-17, sixth/seventh real end-to-end runs, suffixes `Dbg1`/`Dbg2`): two overrides features added — full-list replace mode, and append-mode upsert-by-measureCode
|
|
433
|
+
|
|
434
|
+
Two follow-up feature requests after the `Cust6` override test:
|
|
435
|
+
|
|
436
|
+
1. **REPLACE mode** — a slot's `--overrides` entry can now be `{allowedValues: [{displayName, value}, ...], defaultValue?}` instead of `{measureCode, displayName}`. In this mode the customer's array *replaces* the base parameter's allowedValues wholesale (not merged) — for when the customer wants their own complete measure-code menu instead of the base 5-14-entry standard list per slot. `defaultValue` must match one of the supplied entries' `displayName` (defaults to the first entry if omitted) — validated with a thrown error if it doesn't match, not a silent fallback.
|
|
437
|
+
2. **APPEND mode now upserts by `measureCode`, not a blind array push.** `allowedValues[].value` (the measure code) is unique within a parameter's menu — the original `Cust6` test appended a *second* `{displayName: "My Custom KPI Name", value: "TARE"}` entry alongside the base `{displayName: "Actual Gross Revenue", value: "TARE"}` entry, leaving two entries sharing the same code with different labels. Fixed: append mode now does `baseParam.allowedValues.filter(v => v.value !== resolved.measureCode)` before appending the new/changed entry — so choosing an *existing* code just relabels that entry in place (list count unchanged), while a genuinely new code still appends normally (list count +1). Confirmed live (`Dbg2`, same `TARE`/"My Custom KPI Name" override as `Cust6`): the created parameter's `allowedValues` has exactly one `TARE` entry (count 13, same as base), `defaultValue: "My Custom KPI Name"` — no duplicate.
|
|
438
|
+
|
|
439
|
+
Both modes are chosen per-slot by which fields are present in that slot's override object — a single `--overrides` payload can mix append-mode slots and replace-mode slots freely.
|
|
440
|
+
|
|
441
|
+
## Open item: dashboard selection is not currently configurable
|
|
442
|
+
|
|
443
|
+
`DASHBOARDS` in `clone-tpe-dashboards.js` is a fixed constant (`Promotion` + `Tactic`) — the main
|
|
444
|
+
loop always clones both, regardless of which slots have overrides. There is no `--dashboards` flag
|
|
445
|
+
or other mechanism to clone only one. This was an intentional carryover from the original plan's
|
|
446
|
+
"always produce all 5 slots' worth of clones, never skipped" rule, extended to dashboards — but if a
|
|
447
|
+
customer only cares about one dashboard, this will still always clone both.
|