Salesforce · Tableau Next · SE Track

Under the Hood.

40+ platform limits, empirically documented. Every wall we hit and every workaround we shipped, tested against a live workshoplwc2 org.

Agent Astro thinking
Between July 6 and July 13, 2026, we built the largest Salesforce Tableau Next LWC library ever assembled — 99 canonical components, 47 map extensions, 27 Leaflet tiles. Along the way we hit walls. This page is what we found: every wall, every workaround, every reason it's the way it is.
The Big Five

Five walls that will bite anyone shipping on this stack

For each: the wall, the evidence, the workaround. Every citation traces back to a file in the repo.

FINDING 01 · BLOCKS

Web Workers via Blob URLs are permanently blocked by Lightning Web Security

The wall

Any LWC on Tableau Next / Salesforce Lightning that uses Web Workers via Blob URLs is permanently blocked by LWS. That includes mapbox-gl-js, maplibre-gl-js, deck.gl — every WebGL library that spawns off-main-thread workers via new Worker(URL.createObjectURL(blob)).

Evidence

Both Mapbox and MapLibre UMDs build their WebWorker as new Blob([workerBundleString], { type: 'text/javascript' }). Under LWS this throws immediately:

Lightning Web Security: Unsupported MIME type.

Because the throw happens INSIDE factory(), the entire UMD wrapper unwinds — global.mapboxgl = factory() never assigns. Swap the MIME to application/javascript and the factory completes, but new mapboxgl.Map(...) then throws TypeError: Cannot read properties of undefined (reading 'protocol') inside the URL parser. The workaround is worse than the wall.

Workaround

Rebuild as pure D3 Canvas/SVG with pre-baked GeoJSON, or use Leaflet with raster-only tiles. Leaflet does NOT use Blob workers. Avoid Leaflet.VectorGrid and vector tile plugins. Eight Mapbox LWCs (Hero, 3DExtrude, Heatmap, Traffic, Isochrone, HexAgg, TileMap, TerritoryMap) were rewritten to pure D3 in Wave S-map-D3. All 8 now render clean.

FINDING 02 · BLOCKS

The MIME-swap "fix" makes it worse — surface an unrecoverable modal

The wall

The instinct when you see Lightning Web Security: Unsupported MIME type is to patch the library's Blob type to application/javascript. Do NOT do this. Making the factory succeed only relocates the failure to a louder place.

Evidence

After patching, window.mapboxgl surfaces but every retry from the LWC's renderedCallback triggers:

WARNING: Too many active WebGL contexts. Oldest context will be lost.
TypeError: Cannot read properties of undefined (reading 'protocol')

…and eventually surfaces Tableau's "Sorry to Interrupt" modal — worse than silent Mode-B black canvas. Data-URL fallback (data:application/javascript;base64,…) for the worker doesn't help either.

Workaround

The silent Mode-B black canvas IS the preferred failure state. LWCs early-exit at if (!window.mapboxgl) return, the card chrome renders clean, and no user-visible error modal appears. The paved path is architectural: D3, Leaflet raster, or an iframe/LWR site outside the LWS boundary.

FINDING 03 · WORKS-AROUND

Data Cloud CSV ingestion is fully programmatic — the blocker was the OAuth scope

The wall

Every prior wave of this doc said CSV ingest to a DLO was blocked — Bulk API 2.0 rejects DLOs as "no create access," the streaming CSV endpoint is 404, and the Ingestion API path was called UI-only. That was wrong. The blocker was the OAuth scope on the token, not the endpoints.

Evidence

Standard sf org login web yields a token with api refresh_token only. Every 400 against the CDP tenant endpoint was the tenant refusing an under-scoped token. Live-tested 4-stage pipeline works end-to-end once the token carries cdp_ingest_api:

1. POST /services/data/v67.0/ssot/connections           → 201 {id, tenantSpecificEndpoint}
2. PUT  /services/data/v67.0/ssot/connections/{id}/schema → 200
3. POST /services/data/v67.0/ssot/data-streams          → 201 (DLO auto-materializes)
4. POST https://{tenant}.c360a.salesforce.com/api/v1/ingest/jobs → 201
   → PUT batches (text/csv, 10k/chunk) → PATCH state=UploadComplete → poll
Workaround

Single-flag change to cold-open:

sf org login web \
  --scopes "api cdp_ingest_api cdp_query_api refresh_token"

No new Connected App. No Metadata deploy. Attendee sees the same OAuth consent popup with two extra scope lines. Definitive recipe at docs/dc-ingestion-research-2026-07-13.md.

FINDING 04 · WORKS-AROUND

Dashboard extension LWCs fetch /services/data and get 401 — the origin mismatch trap

The wall

Every canvas we mined assumes an LWC can call fetch("/services/data/v67.0/semantic-engine/gateway", { credentials: "include" }) directly from the browser. That assumption is wrong for the specific runtime an analytics__Dashboard LWC lives in.

Evidence

An LWC hosted on analytics__Dashboard runs inside a document whose origin is <org>.lightning.force.com. Session cookies for /services/data/... live on <org>.my.salesforce.com. Browser-side fetch resolves against the lightning origin — no session cookie — and comes back:

HTTP 401 Unauthorized

Every canvas author "deployed but never API-PATCHed onto a dashboard." That combination is what surfaces the mismatch.

Workaround

Route every semantic-engine call through an Apex @AuraEnabled controller. All 45 gateway-using workshop LWCs have been refactored to Apex. Full reference at docs/references/official-dashboard-extension-sdk-docs.md.

FINDING 05 · WORKS-AROUND

Extension LWCs need an inner try/catch or the whole tile renders "Something Went Wrong"

The wall

Four of five Tier-1 offline map LWCs rendered "Something Went Wrong" while a fifth (ArcMap) rendered correctly — same fallback pattern, only ArcMap survived. Diagnosed 2026-07-11 on ck_map_showcase_dsh.

Evidence

All 5 share an async _runQueryPipeline() wrapped in a single outer try/catch. When fetchDataUsingQueryAndSource() throws — because the SDM doesn't ship the field being queried (State_clc, Postal_Code on Account) — the outer catch sets _phase = "error" and the tile renders the framework's error state. ArcMap wraps the query call in an inner try/catch that swallows the error, sets rowArr = [], and falls through to _phase = "ready" → seed data.

Workaround
// WRONG — an SDM-mismatch throw kills the tile
const rows = await this._runShapeBQuery(fields, modelJson, sdmName, this._rowLimit);
rowArr = Array.isArray(rows) ? rows : [];

// RIGHT — seed-data path stays reachable
try {
    const rows = await this._runShapeBQuery(fields, modelJson, sdmName, this._rowLimit);
    rowArr = Array.isArray(rows) ? rows : [];
} catch (e) {
    if (this._debug) console.warn(TAG, "Shape B query failed; using seed data", e);
    rowArr = [];
}

Corollary: the outer catch must call notifyLifecycleChange(LOADED) — never ERROR — so the dashboard shell keeps the tile visible.

The Corpus

18 more gotchas we hit and worked around

Compact table pulled from .claude/skills/tableau-next-author/references/platform-gotchas.md. Every row corresponds to a rule enforced by the authoring skill or a workaround shipped in bootstrap/lib/dashboard_builder.py.

NameOne-line descriptionSeverity
POST-vs-GET divergencev67 POST rejects widgetStyle.textColor, source.label, source.type on widgets that GET happily returns. Round-tripping fails until you strip.WORKS-AROUND
Filter widget source blockMissing source: {id, name} → widget renders but does nothing; server sets status DataSourceError and redacts field names to ACCESS_DENIED_*.WORKS-AROUND
Radial ?minorVersion=6Radial family (Radial Bar, Nightingale, Radial Donut) is not a Vizql variant. Wrong minor version returns "min version is 67.06". Only Color encoding accepted.WORKS-AROUND
Relative-date operatorsCurrentDay rejected. Fiscal / Next* variants inconsistent — sometimes 201, sometimes 400. Fall back to LastNDays.WORKS-AROUND
Total_Sales_clc is Closed-Won-onlySDM expression gates on Is_Won_Opportunity_clc, making the measure NULL for every non-Closed-Won stage. Across-stage visuals render 1 populated cell of 6.KNOWN
Palette on-write requiredEmpty style.encodings.colors = SFDC pink/magenta default. Every viz payload must ship an explicit Handoff4 palette or the dashboard looks off-brand.WORKS-AROUND
Text widget = Quill deltaText widgets MUST be added via update_dashboard, not add_widget_to_dashboard. & HTML-encodes to &amp; in labels.WORKS-AROUND
Boolean @api defaultLWC1503: @api foo = true fails compilation with "Boolean public property must default to false." Rename the semantic so the default is false.WORKS-AROUND
Grouped Table mergeRepeatedCellsMust be false. subtotals block rejected (use grandTotals). Measure keys forbidden in style.headers.fields.WORKS-AROUND
Map lat/lon aggregationLat/Lon must be Continuous. Platform applies SUM by default → summed lat/lon → world-scale drift near the antimeridian. Set Median or explicit pixel size.WORKS-AROUND
Waterfall / Nightingale flagsisRunning: true and isNightingale: true rejected in v67.06. Ship as stacked bar / Radial Bar until parser catches up.KNOWN
Required-but-undocumented viz fieldsmarks.headers.stack, full style.marks.panes block, correct size.type per chart. Missing fields → empty shell.WORKS-AROUND
Percent not a valid number typenumberFormatInfo.type: "Percent" returns 400. Use Number with % suffix, or NumberCustom.WORKS-AROUND
Probability_clc aggregation bugSDM ships aggregationType: "Sum" on a percentage. Scatter payloads joining SUM(x), SUM(y) collapse to empty. Fix in SDM, not payload.KNOWN
UserAgg CLC re-aggregationCLC measures with aggregationType: "UserAgg" already contain SUM/COUNT inside the expression. Name-based auto-inference re-aggregates a pre-aggregated field.WORKS-AROUND
Side-by-side bar MeasureNamesMeasureNames F2 must appear in BOTH columns AND style.headers.fields. Omit either → second measure column silently disappears.WORKS-AROUND
DMO ingestion binding still UI-onlyA freshly POSTed DMO comes back with isEnabled: false; SDM POST rejects it with SEMANTIC_FIELD_NOT_VALID. DMO enablement is a UI click. Bind SDMs to shipped ssot__ DMOs, or use the DLO ingest path (Finding 03).KNOWN
SDM field discovery before map LWCWorkshop SDM has geo only on CloudKicks_Locations. Account_Latitude_clc, State_clc, Primary_Industry_clc do NOT exist. Always run SDM discovery before authoring.WORKS-AROUND
The Research Tree

Eight paths tested for Data Cloud CSV ingestion

On 2026-07-13 we enumerated every documented and undocumented path to load CSV rows into a DLO from headless code. Seven paths were dead ends. One won. Path G was the answer the entire time — it was just gated behind a scope on the OAuth token.

Can we load CSV rows into a DLO headlessly? PATH A/ssot/connectionsWIN ✓ PATH BMetadata API✗ scope PATH Csfdatakit Flow✗ no data PATH DTooling Mkt*✗ metadata PATH E__dll INSERT✗ read-only PATH FSigned URL✗ 404 PATH Gcdp_ingest_apiWIN ✓ (the key) PATH H/tableau/datasets✗ 404 connections + streams OAuth scope on the token Winning pipeline: A + G POST /ssot/connections → PUT /schema → POST /data-streams → PUT /api/v1/ingest/jobs/{id}/batches (text/csv) Prereq: sf CLI token must carry the cdp_ingest_api scope
The Math

By the numbers

8candidate paths tested for Data Cloud CSV ingestion. Seven dead ends, one winner.
3map providers eliminated by Lightning Web Security: Mapbox GL, MapLibre GL, deck.gl.
1map provider survived: Leaflet with raster-only tiles. No Blob workers, no LWS conflict.
27Leaflet LWCs shipped after the Wave S rewrite. All render in Salesforce Lightning.
40+platform gotchas systematically documented in platform-gotchas.md.
175 → 0HTTP 400 errors on the LWC deploy pipeline once the strip-and-normalize helpers landed.
The Receipts

Read the source. Fork the workarounds.

This isn't a whitepaper. Every finding above resolves to a file you can open, a payload you can POST, and a live org where we tested it.

Empirically verified 2026-07-06 through 2026-07-13
Against workshoplwc2 (orgfarm-bd675751b0.my.salesforce.com) on Tableau Next v67.06. Every payload was live-POSTed; every 400 response was captured; every workaround was diff-tested against a known-good baseline. The moat isn't the vision — it's the receipts.