Help & Support
Elevate Your Agents with AgentExchange Solutions
Have Questions?
Crm Analytics Wave Generate
Overview
Use to build a Wave Recipe, deploy a CRM Analytics dataset or dashboard, or author the full CRMA asset stack — apps, Wave Recipes (.wdpr), datasets, and dashboards — against a connected Salesforce org via REST and Metadata APIs, without CRM Analytics Studio or Dashboard Builder. TRIGGER when: user asks to build or create a Wave Recipe, generate a .wdpr or .wdpr-meta.xml, deploy a .wdash or .wapp, run a recipe or dataflowjob programmatically, upload CSV data via InsightsExternalData, validate a dataset schema with synthetic data, fetch dataset XMD, audit dashboard SAQL re-encoding, or find unused dataset fields. TRIGGER on keywords: Wave Recipe, CRMA, CRM Analytics, wdpr, wdash, wapp, WaveRecipe, Wave dataflow, dataflowjob, InsightsExternalData, SAQL, dataset XMD, Analytics Studio, R3 format, wave/recipes, wave/dashboards, or analytics on Salesforce objects (opportunities, leads, cases, accounts, sales). DO NOT TRIGGER for standard Salesforce reports or LWC components unrelated to CRM Analytics.
Authoring CRM Analytics Assets
End-to-end workflow for authoring the full CRM Analytics asset stack — application, Wave Recipe, dataset, and Wave Dashboard — against a Salesforce org from a local SFDX project, without the UI.
Scope
- In scope: WaveApplication, WaveRecipe (R3 format), Wave datasets (recipe-driven + CSV upload via InsightsExternalData), Wave Dashboards (.wdash deploy + PATCH styling), SAQL step authoring, dataset XMD inspection, dashboard auditing.
- Out of scope: Standard Salesforce reports/dashboards, LWC analytics bridge components, Tableau CRM managed packages, org provisioning.
Decision Tree — Pick the Right Method
Before acting, read references/method-decision-tree.md to select the correct API path for the user's goal. Wrong method choices (e.g. multipart upload instead of R3 JSON body) cause silent failures that are hard to diagnose.
Required Inputs
- Authenticated org alias (verify with
sf org display -o <alias> --json) - Target CRMA app name (folder developer name)
- Recipe or dashboard name and output dataset alias
- Source sObjects to load (API names, e.g.
Opportunity,Account) - Field list per sObject
- Transformation intent: formula fields, aggregations, filter conditions
Defaults:
- Local Salesforce connector name:
SFDC_LOCAL(verify viaGET /wave/dataConnectors) - API version: use
result.apiVersionfromsf org display - Node.js binary:
/usr/local/lib/sf/bin/node(bundled with sf CLI)
Workflow
Phase Selection
Before starting, identify the user's goal and determine which phases to run:
| User goal | Phases to run |
|---|---|
| Build or update a recipe and dataset | 0 → 1 → 2 |
| Validate recipe output schema after run | 0 → 1 → 2 → 3 |
| Build or update a dashboard | 0 → 1 → 2 → 4 |
| Full end-to-end: recipe + validation + dashboard | 0 → 1 → 2 → 3 → 4 |
| Audit dataset for unused fields / stale SAQL | 0 → 1 → 5 |
Execute only the phases in the selected path. Do not revisit this table mid-workflow.
Phase 0 — Bootstrap Scripts
- Check for helper scripts — before running any script referenced in this workflow, verify the
scripts/directory exists in the skill root. All 11 scripts listed inreferences/scripts-reference.mdare bundled with this skill. If a script is somehow missing, check that the skill was installed from the full repo (not a partial clone). Do not regenerate scripts from the descriptions — the implementations are authoritative.
Phase 1 — Setup
Verify org connection — run
sf org display -o <alias> --json. CaptureaccessTokenandinstanceUrl. If EPERM error, shell is sandboxed — re-run with full permissions.Look up or create the CRMA app — run
scripts/create_app.js <alias> <appName>. The script lists existing folders and only creates if missing. Do NOT includeassetIcon.
Phase 2 — Recipe Authoring
Read the R3 node schema — load
references/r3-node-schema.mdbefore writing any recipe JSON. Every node shape, critical field rule, and formula gotcha is documented there.Author the recipe JSON — the
.wdprfile contains only therecipeDefinitionobject (thenodesmap,runMode, anduisection). Do NOT wrap it in an API envelope (fileFormat,label,name,recipe: {}) — the deploy script reads this file directly as therecipeDefinitionbody; wrapping it causes the wrong JSON structure to be POSTed at?format=R3, leavingtargetDataflowIdnull. Save toforce-app/main/default/wave/<RecipeName>.wdpr. Key rules:runModemust be"full"for generated recipes — NOT"R3"(which is only used as a query param on the API endpoint). Use"incremental"instead of"full"for large objects where reprocessing the entire source on every run is too slow; use"streaming"for near-real-time pipelines."full"is the safe default for new recipes.- Use
sources(array), neversource(singular) - One field per formula node — chain them in sequence
- Use
expressionType: "SQL"andtype: "NUMBER"ortype: "TEXT"on formula fields - For text date dimensions use
date_format(field, 'yyyy')ordate_format(field, 'MM/yyyy') - Formula node action is
"formula"— NOT"computeExpression","augmentColumns", or"transform". Fields array key is"fields"(NOT"columns"). Expression key is"formulaExpression"(NOT"expression"). Do NOT use backtick quotes around field names in expressions. - Insert an
EXTRACT0(extractGrains) node between the last formula node and aggregate - Use the Designer-native
uisection format — readreferences/ui-section-template.md
CRITICAL — top-level
.wdprfile structure.nodesis a ROOT-LEVEL object with UPPERCASE string keys. Do NOT put nodes insideui. Do NOT use an array forui.nodes. The file has exactly three top-level keys:runMode,nodes,ui. Seeexamples/wdpr-skeleton.wdprfor a minimal 5-node skeleton (load → formula → extractGrains → aggregate → save).See the wrong-variant reference table in
references/r3-node-schema.md → "Common wrong variants"for the full list of correct vs incorrect field names per node type.Write SFDX metadata wrappers — every
.wdprfile MUST be accompanied by a.wdpr-meta.xmlin the same directory. These are always created as a pair — outputting the recipe JSON without its metadata wrapper is incomplete. Use the template atassets/wdpr-meta-template.xml. Required fields:<application>,<masterLabel>,<targetDatasetAlias>,<dataflow>(set to the same value as<targetDatasetAlias>). Do NOT add<description>,<label>,<accessType>,<shareType>, or<format>— each triggers a deploy failure. Similarly, every.wapprequires a.wapp-meta.xml.Deploy recipe via REST — run
scripts/deploy_and_run_recipe.js <alias> <recipeFile.wdpr> <appName> <recipeName>. This script: looks up folder id → POSTs or PATCHes recipe at?format=R3→ assertstargetDataflowIdreturned → POSTs to/wave/dataflowjobs→ polls until terminal.Verify recipe ran — terminal statuses:
Success,Failure,Warning,Cancelled. OnFailure, runscripts/check_connector_error.sh <alias> <connectorId>— exits 0 forSystemerrors (safe to retry), exits 1 forUser/Limiterrors (do not retry; fix config first). If retries persist on aSystemerror, runPOST /wave/dataConnectors/{id}/ingestto force a connector refresh.
Phase 3 — Dataset Validation
Upload synthetic data — run
scripts/gen_synthetic_dataset.js(schema-driven, reads live XMD) thenscripts/upload_dataset.js. Readreferences/external-data-upload.mdfor InsightsExternalData rules.Verify with SAQL — run
scripts/verify_synthetic_dataset.js. A 200 onAction=Processonly means ingestion was queued — wait forStatus=Completed, then verify with a count query.
Phase 4 — Dashboard Authoring
Read dashboard rules — load
references/dashboard-authoring.mdbefore writing any.wdash. Covers the two-step deploy+PATCH workflow, accepted widget parameters per type, and the SAQL re-encoding trap.Build minimal
.wdash— useassets/dashboard-template.jsonas the starting structure. Keep widget parameters minimal at deploy time (seereferences/dashboard-authoring.md → "Deploy-time rules"). Deploy withsf project deploy start. When a listselector must drive another step's SAQL (not a global facet), put a{{column(step.selection, ["Field"]).asEquality("TargetField")}}binding in that step's query and setbroadcastFacet/useGlobaltofalseon both steps — seereferences/dashboard-authoring.md → "Query bindings".Apply styling via PATCH — after deploy,
PATCH /wave/dashboards/{id}with enriched state. AlwaysdecodeAlleverystate.steps[*].queryimmediately after GET, before any mutation + PATCH. This is the #1 silent failure mode: the API HTML-encodes SAQL on write ("→"). GET → PATCH without decoding encodes again (&quot;, then&amp;quot;) and every widget renders Error 119. The helper is inreferences/dashboard-authoring.md. Usescripts/patch_t_oppo_view.jsas the canonical GET → decodeAll → merge → PATCH template.Verify all steps render — before running, edit the
DASH_IDandDSconstants at the top ofscripts/verify_dashboard_steps.jsto the deployed dashboard's 18-char ID and the dataset's current ID/versionId. Then runscripts/verify_dashboard_steps.js <alias>. For each step: GET stored SAQL → decode → swap dataset name forid/currentVersionId→ POST to/wave/query. Confirm 200 + rows. Do not mark done until user confirms widgets render in the browser.
Phase 5 — Dataset Hygiene
- Find unused fields — run
scripts/find_unused_dataset_fields.js <alias> <DatasetName>. Readreferences/dataset-hygiene.mdfor caveats (aggregateflex steps, multi-dataset SAQL, archived apps).
Rules / Constraints
| Constraint | Rationale |
|---|---|
Every .wdpr MUST be paired with a .wdpr-meta.xml in the same directory |
Metadata wrapper is required for SFDX deploy; recipe JSON alone is an incomplete artifact |
Always POST recipes at ?format=R3 with recipeDefinition in JSON body |
Multipart upload creates recipe but never compiles it — targetDataflowId stays null |
PATCH recipe with ?format=R3 JSON body only; never multipart PATCH |
Multipart PATCH updates raw file but destroys compiled state → "tableModelInfo is null" |
On tableModelInfo is null: DELETE then re-POST |
Only re-POST triggers server-side compilation |
Use targetDataflowId (02K...) on /wave/dataflowjobs, never recipe id (05v...) |
/wave/dataflowjobs only accepts the dataflow id |
| One field per formula node | REST API accepts multiple but Recipe Designer breaks with "Can't Load the Recipe" |
Use column not col in gridLayouts widget entries |
col triggers Unrecognized field "col" on deploy |
widgetStyle.borderRadius must be integer (e.g. 8), not CSS string |
String value rejected at PATCH |
| Use name-based dataset load in stored SAQL, not id/version | id/version load requires a datasets binding the PATCH input rep rejects |
Always decode SAQL before PATCH — use decodeAll |
GET returns HTML-encoded SAQL; re-encoding on every PATCH accumulates → &amp;quot; |
valuestable widget rejects all styling at PATCH — use chart with visualizationType: "flatTable" |
chart widget accepts the full rich-styling param set |
POST /wave/dashboards only creates retired classic dashboards |
Use Metadata API deploy for modern grid dashboards |
KPI total requires group q by all before foreach |
Without it, foreach acts as a window function returning one row per source row |
Gotchas
| Issue | Resolution |
|---|---|
Recipe sits in New status, targetDataflowId null |
Re-create via POST /wave/recipes?format=R3 with recipeDefinition in JSON body |
Formula type "TEXT" but expression returns number |
Use type: "NUMBER" and expressionType: "SQL" — other type strings silently normalize to TEXT |
Aggregate fails: Can't find field _Year / _Month |
Auto-derived date helpers don't exist mid-pipeline; derive explicitly via date_format formula node |
400 errorCode 276 on POST recipe |
Name already in use — PATCH the existing recipe instead |
| All widgets show Error after PATCH | SAQL re-encoding — run decodeAll on every step query before PATCH |
listselector list text invisible on dark bg |
Set filterStyle.valueColor — it defaults to dark; always set a contrasting color |
date_format(CloseDate, 'MMM yyyy') produces numeric month |
Use 'MM/yyyy' for text dimensions — MMM silently returns integer in recipe formula context |
Output Expectations
force-app/main/default/wave/<RecipeName>.wdpr— Wave Recipe JSON (R3 format)force-app/main/default/wave/<AppName>.wapp-meta.xml— app metadata wrapperforce-app/main/default/wave/<RecipeName>.wdpr-meta.xml— recipe metadata wrapperforce-app/main/default/wave/<DashboardName>.wdash— dashboard state JSONforce-app/main/default/wave/<DashboardName>.wdash-meta.xml— dashboard metadata wrapper
Reusable Scripts
All scripts accept org alias as first positional arg. Read references/scripts-reference.md for full descriptions and usage.
| Script | Purpose |
|---|---|
scripts/create_app.js |
Idempotently create a CRMA app folder |
scripts/deploy_and_run_recipe.js |
Create + compile + run recipe end-to-end |
scripts/probe_recipe.js |
Fetch recipe compile status + targetDataflowId |
scripts/fetch_dataset_xmd.js |
Dump dataset XMD (dimensions/measures/dates) |
scripts/gen_synthetic_dataset.js |
Schema-driven synthetic data generator |
scripts/upload_dataset.js |
Upload CSV via InsightsExternalData API |
scripts/verify_synthetic_dataset.js |
Post-upload SAQL sanity checks |
scripts/find_unused_dataset_fields.js |
List XMD fields not referenced in any dashboard SAQL |
scripts/verify_dashboard_steps.js |
POST each step's SAQL to /wave/query and confirm rows |
scripts/patch_t_oppo_view.js |
Full-cycle PATCH template with dark theme and decodeAll |
scripts/check_connector_error.sh |
Check connector errorCategory; exits 0=System (retryable), 1=User/Limit (do not retry) |
Cross-Skill Integration
| Need | Delegate to |
|---|---|
| Deploying Wave metadata via SFDX project deploy | Use standard sf project deploy — no skill needed |
| LWC components that consume Wave datasets | Use an appropriate LWC skill |
Reference File Index
| File | When to read |
|---|---|
references/method-decision-tree.md |
Phase 1 — before choosing API path |
references/r3-node-schema.md |
Phase 2 — before writing any recipe node |
references/ui-section-template.md |
Phase 2 — when building the Designer-native ui section |
assets/wapp-meta-template.xml |
Phase 2 — when creating .wapp-meta.xml |
assets/wdpr-meta-template.xml |
Phase 2 — when creating .wdpr-meta.xml |
references/external-data-upload.md |
Phase 3 — InsightsExternalData rules and common errors |
references/dashboard-authoring.md |
Phase 4 — full dashboard deploy+PATCH rules and re-encoding trap |
assets/wdash-meta-template.xml |
Phase 4 — when creating .wdash-meta.xml |
assets/dashboard-template.json |
Phase 4 — minimal deploy-valid .wdash starting structure |
references/dataset-hygiene.md |
Phase 5 — unused field detection and SAQL re-encoding audit |
references/scripts-reference.md |
Any phase — full script descriptions and usage |
examples/wdpr-skeleton.wdpr |
Phase 2 — minimal 5-node skeleton (load → formula → extractGrains → aggregate → save) |
examples/opportunity-revenue-recipe.wdpr |
Phase 2 — fully annotated example recipe with all node types |
examples/pipeline-by-industry-quarter.wdpr |
Phase 2 — kitchen-sink join + formula chain + extractGrains + case/coalesce/quarter |
examples/industry-binding-dashboard.wdash |
Phase 4 — listselector driving a SAQL filter via column().asEquality() |