# Claudforce — HLS — Provider + Payer

An ordered route from an empty Salesforce org to a working Data Cloud, Marketing Cloud Next and Agentforce demo. Written from the org it was built in, verified against that org on 3 September 2026.

Liam Sheridan · Salesforce Solutions Engineer · org trailsignup-a612cacbdcac92

Connect this to your Claude

## This page is an MCP server. Point your agent at it.

Everything below is also available as tools your agent can call. Connect this server for the route, the traps and the data shapes, and connect your own Salesforce org so your agent can act on it. Neither half is enough on its own: this server knows what to build and cannot touch an org; the Salesforce MCP can touch an org and does not know this build.

1\. Connect this guide

```
claude mcp add --transport http claudforce-hls https://claudforce-hls.thelonelyagent.com/api/public/mcp
```

No auth. Twelve tools: `start_here` first, then get\_guide, get\_corrections, list\_sections, get\_section, search\_traps, verify\_step, list\_skills, get\_skill, get\_dataset\_schema, get\_manifest, list\_downloads.

2\. Connect your own Salesforce org

Point your agent at your own org with the Salesforce MCP server. That is the half that does the building. This guide never touches your org and never sees your data.

**How to connect your Salesforce org, step by step**

You need an External Client App. Connected Apps are not supported for MCP client connections. Verified against a live org — about five minutes of setup, plus up to thirty minutes of waiting that catches most people out.

1.  In Setup, go to **External Client App Manager** and choose **New External Client App**.
    
    Broken
    
    ### It is an External Client App, not a Connected App
    
    Connected Apps are not supported for MCP client connections. If you build one you will get an app that looks correct, authorises, and never connects. Salesforce's own setup guide directs you to register the client as an External Client App, and that is the only path that works.
    
2.  Give it a name. The **API Name must contain no hyphens** — the label can. A hyphen in the API name is a silent failure later.
3.  Set the **Callback URL** to `https://claude.ai/api/mcp/auth_callback`
4.  Add the OAuth scopes `mcp_api` and `refresh_token`. Both are required.
5.  Under security, enable **JWT-based access tokens for named users**, and leave **PKCE** on. Uncheck everything else that is changeable.
6.  Set **Distribution State** to **Local**.
7.  Save, then **wait**. See the trap below.
8.  In Claude, add the Salesforce MCP server using the server URL from Salesforce's documentation for your setup. Standard hosted servers ship with tools already and are the fastest route; a custom server only pays off when you need to expose specific Flows, Apex or Data Cloud queries.

Trap

### Propagation takes up to thirty minutes

The app is saved long before it is usable. First-attempt connection failures are almost always this and not a configuration mistake. Wait, then retry the same settings before you change anything. Changing settings during propagation is how people end up rebuilding a working app.

Salesforce MCP connection summary

| Setting | Value |
| --- | --- |
| Where | Setup → External Client App Manager → New External Client App |
| API Name | No hyphens. The label can keep them |
| Callback URL | https://claude.ai/api/mcp/auth\_callback |
| OAuth scopes | mcp\_api and refresh\_token |
| Security | JWT-based access tokens for named users, PKCE enabled |
| Distribution State | Local |
| Propagation | Up to 30 minutes |

Salesforce's setup guide at [developer.salesforce.com/docs/platform/hosted-mcp-servers/guide/setup-overview.html](https://developer.salesforce.com/docs/platform/hosted-mcp-servers/guide/setup-overview.html) is the source for the server URL and the current client list. Salesforce lists Claude as a supported client.

Reading instead? The whole document is below, or as one Markdown file at [/rebuild-path.md](/rebuild-path.md), plain text at [/rebuild-path.txt](/rebuild-path.txt), indexed at [/llms.txt](/llms.txt).

Get everything

## The downloads

Everything on this page is downloadable. The asset pack is the whole build: 37 Apex classes, 18 Lightning web components, pages, tabs, permission sets and the schema. The mock data is the thirteen scrubbed datasets on their own. The skills are five Claude skills written while building this, for driving Salesforce over its APIs: headless discovery, campaign build, web affinity, Tableau Next dashboards, and SE triage.

Claude skills

### Salesforce SE skills

Five skills for driving Salesforce over its APIs, written while building this and used to build it.

5 skills · 36 KB

[Download the skills](/salesforce-skills-pack.zip)

Data only

### Makana mock data

The thirteen datasets on their own, if you have an org and just need something in it. Scrubbed and synthetic.

13 datasets · 467 KB

[Download the mock data](/makana-mock-data.zip)

The build

### The asset pack

Everything behind the screenshots and videos on this page. Apex, components, page placement, data and configuration.

553 files · 1.04 MB

Full file list: [/manifests/asset-pack.txt](/manifests/asset-pack.txt)

[Download the asset pack](/rebuild-path-asset-pack.zip)

Machine-readable paths and the MCP server are covered at the top of this page.

The result

## What the build produces

Four short screen recordings from the finished org, so you can see where the week ends before it starts.

The unified profileA custom Unified Profile connected live to campaigns, segments and web affinity data, with Maps showing how close each patient is to the nearest clinic and a full engagement timeline.

The campaign pageAutomated campaign creation with mock engagement data, attribution insights, A/B path experiments, multi-content campaigns and a matching landing page.

A/B testingA standalone app inside Marketing Cloud Next that shows every live content A/B test running across the platform.

Audience overlapAdjust audience priority and see where customers overlap across segments. Identify the people tied to three, four or more segments and prioritise them directly.

Rule

### A 200 is not a result.

A `200` proves the parser accepted your request. `status: Ok` proves a source resolved. Neither proves the thing is correct. In this project, at least a dozen distinct APIs returned success while doing nothing, doing the wrong thing, or silently discarding half the payload. **Verify every write with a read-back query.** Every _How you know it worked_ line in this document exists because that rule was learned the hard way.

Note

This is a sequence of verified steps. How long it takes depends on how much of it you are deciding versus following. It does not compress if you are also making the design decisions. **The asset pack now ships the whole build:** 37 Apex classes, 18 Lightning web components, the pages, tabs and permission sets that place them, 13 scrubbed datasets, and the segment, dashboard and agent configuration as reference. What it cannot ship is the schema — custom objects have no REST create path — and the Tableau Next dashboards, which have no read endpoint to export from.

Scope

## What you are rebuilding

A single Salesforce org carrying a full healthcare marketing demo, and it is not one undifferentiated route. The difficulty sits in the data foundation: the mock data, the data streams and the DLO to DMO mappings, identity resolution, the data graphs, the engagement data connections, consent, and the segments built on top of all of it. Everything downstream of that foundation — the Tableau Next dashboards, the custom components and the Agentforce agents — is comparatively mechanical once the foundation is correct and resolving. The foundation is where the time actually goes. The figures below are the shipped state of that org.

640

unified profiles

5,828

consent rows

6

campaigns

192

dashboard widgets, all returning Ok

4

agents

26

message variants

Every figure on this page carries a verification date. Status in a catalogue decays faster than content.

Day 0

## Before you build anything

Two constraints set here shape every decision for the rest of the week.

Build

Day 0 prerequisites

| Requirement | Why |
| --- | --- |
| Data Cloud provisioned, with a data space | Everything downstream of Day 1 |
| Marketing Cloud Next enabled | Campaigns, Flows, Content tabs |
| UnifiedMarketingStarterUserPermSet assigned to yourself | Blank Campaigns/Flows/Content tabs are a permissions problem, not a setup problem. Marketing Cloud Admin configures MC Next; it does not let you _use_ it |
| Tableau Next workspace | Day 4 |
| Agentforce enabled | Day 6 |

### What the source org actually holds

Verified against the source org on 3 September 2026. It is an **Enterprise Edition trial**, non-sandbox, expiring **11 November 2026**. Every permission-set licence below expires with it. A trial gets provisioning and limits a customer org will not — treat the counts as evidence the feature was on, not as a shopping list.

Source org licence and permission inventory

| Licence or permission | Total | Used | What breaks without it |
| --- | --- | --- | --- |
| Salesforce (user licence) | 550 | 69  | Everything |
| Data Cloud `GenieDataPlatformStarterPsl` | 200,000 | 2   | Day 1 onward. No streams, no DMOs, no identity resolution |
| Customer Data Platform `CdpPsl` | 2,500 | 1   | Segment creation and the `MarketSegment` sObject |
| Customer Data Cloud for Marketing | 200,000 | 1   | Segment activation into campaigns |
| Marketing Cloud `MarketingCloudPsl` | 36,005 | 1   | Campaigns, Flows, Content — the whole of Day 3 |
| Marketing Triggers | 5,000 | 1   | Event-driven journeys |
| Marketing Intelligence Users | 10,000 | 1   | Campaign performance reporting |
| Agentforce (Default) `EinsteinGPTCopilotPsl` | 3,105 | 10  | Day 6. No agents in the picker |
| Einstein Prompt Templates | 3,110 | 15  | Prompt Builder |
| Einstein Agent (user licence) | 802 | 1   | Agent runtime |
| Tableau Next Creator | 10  | 1   | Day 4. Authoring dashboards |
| Tableau Next Consumer | 10  | 1   | Viewing them |
| CRM Analytics Plus | 131 | 3   | The analytics platform underneath |
| Data Pipelines Base User | 3   | 1   | Batch data prep |
| Guest User License | 25  | 9   | The anonymous web-visit capture only |

Verified

### The one assignment everybody misses

`UnifiedMarketingStarterUserPermSet` — the Marketing Manager permission set. Without it the Marketing app and the Marketing Cloud Next campaign builder simply do not render, and nothing tells you why. Marketing Cloud Admin configures MC Next; it does not let you _use_ it. This is the same trap as the blank-tabs row in the table above, and it is worth stating twice because it costs an afternoon.

Note

### What the build owner holds, and what that means for you

The owner is a **System Administrator** carrying **149 permission-set assignments** and **101 permission-set-licence assignments**. You do not need to reproduce that. What matters:

-   The standard sets that gate the platforms: `UnifiedMarketingStarterUserPermSet`, `StarterMarketingApp`, `MarketingCloudAdmin`, `MarketingTriggers`, `CDPAdmin`, `GenieAdmin`, `GenieMarketingAdmin`, plus the Tableau Next and CRM Analytics sets.
-   **Every custom permission set in the asset pack has a null `LicenseId`.** None of them consumes a licence. They are free metadata — recreating them is labour, not spend.
-   **`Cld_Web_Visit_Guest_Api` goes on the guest user, not on you.** It belongs to the Marketing Landing Pages Site Guest User. Assigning it to yourself does nothing and the endpoint keeps returning 403 for anonymous traffic.
-   A plain `SELECT ... FROM PermissionSetAssignment` **under-reports**. It returned 102 rows where `COUNT(Id)` returned 149 — standard-type permission sets are silently omitted from the row-returning query. Audit by count, not by eyeballing the rows.

Broken

### Two things the source org does not have

**No Health Cloud user licence exists in this org at all**, yet 146 `HealthcareProvider` records are queryable — the HLS objects ride on the industries data model available to the trial's Salesforce licence. Do not promise a customer this reproduces without a Health Cloud entitlement.

**`CarePlan` is not supported.** The Care Plans permission-set licences are provisioned (10 available) and entirely unassigned. Any demo beat that depends on care plans needs rework, not configuration.

Note

### Limits worth knowing before you generate data

Most limits in a trial are enormous — 441,450 daily API requests against 52 used, 53 GB of data storage against 539 MB. Two will bite:

-   **Single emails: 100 per day.** Mass emails: 10. The seed generators write engagement _records_, they do not send, so this only matters if you wire up real sends.
-   **Analytics dataflow executions: 60 per day.** Enough to iterate, not enough to brute-force.

Everything else has four or more orders of magnitude of headroom at this data volume.

### The two constraints that shape every later decision

-   Trap
    
    **Custom objects cannot be created over any REST surface.** Tooling `CustomObject` reports `createable: true` and rejects `FullName`. There is no `createCustomObject` action. `/headless/metadata` is not served. `deployRequest` needs multipart. Confirmed independently four times across this project. Metadata API via the `sf` CLI only.
    
-   Trap
    
    **Custom fields are fine over Tooling.** This is the standard workaround, and it shapes the architecture: where you would reach for a new object, put fields on an existing one. Five fields on `User` replaced a leaderboard object. `B2B_Mood_Board__c`, a stock SDO object, carries an entire 27-row message registry. A Tooling-created CustomField has **no FLS**, so SOQL and REST report _No such column_ while Apex reads and writes it perfectly. A seed can therefore succeed against a field you cannot query.
    

### Traps that apply all week

-   Trap
    
    **`+`, `++`, `&` and `#` in an `executeAnonymous` query string.** `+` decodes to a space; `&` truncates the body at that character and surfaces as `Missing closing quote character '` at an unrelated column. Use `String.join` and a `List` as a counter. Build the characters you need with `String.fromCharArray(new List<Integer>{38})`. This was the single most frequently recurring trap in the project.
    
-   Trap
    
    **Throwing an exception to read a value out of `executeAnonymous` rolls back every DML in that transaction.** It once reverted 2,983 updates and silently no-opped a 1,165-row seed. Read-only probes only. To prove a write: run it plain, confirm `success: true`, verify with a separate query.
    
-   Trap
    
    **`where`, `group` and `on` are reserved words in Apex.** `on` is the nastiest: `for (String on : map.keySet())` fails with `Unexpected token 'on'` and then cascades a dozen unrelated errors below it — `Extra ')'`, `Method does not exist: void get() from the type Map<String,Integer>` — none of which are real.
    
-   Trap
    
    **Apex identifiers are case-insensitive.** A parameter `src` silently shadowed the constant `SRC` and mis-grouped every seeded row.
    
-   Trap
    
    **Every out-of-the-box marketing tile defaults to Last 30 Days.** Anchor every generator to `Date.today()`, never a literal date. Hit three separate times; stale data is indistinguishable from a broken pipeline.
    

The part that takes the time

## The data foundation

Dashboards, components and apps are straightforward once this exists. The foundation is where the difficulty lives, and it is the part that has to be right before anything downstream means anything. Everything here was verified against the source org.

The order

1.  Field inventory and design, complete, named, typed
2.  Create fields and field-level security
3.  Populate and verify in CRM by SOQL
4.  Create the data stream
5.  Map DLO to DMO, and verify the mapping by reading the DLO field list
6.  FULL refresh
7.  Dry-run the match rule, and state the predicted unified count
8.  Run identity resolution, reconcile against the prediction, explain any gap
9.  Segments, Calculated Insights, data graph
10.  Dashboards last

Steps 1 to 3 before step 4 is the rule that saves the most rework.

Rule

### CRM nulls become empty strings in Data Cloud

A CRM field that is `null` arrives as `''`, not `NULL`. Verified on 2,989 rows: `IS NULL` matched **0**, `= ''` matched **2,507**. So `IS NOT NULL` is always true. A care-gap measure written as `SUM(CASE WHEN Care_Gap_Type IS NOT NULL THEN 1 ELSE 0 END)` returned 2,989 instead of 482. Always test emptiness with `<> ''`.

The data stream create contract

```
POST /services/data/v64.0/ssot/data-streams

{
  "name": "Patient_Appointments_CRM",
  "label": "Patient Appointments - CRM",
  "datastreamType": "SFDC",
  "connectorInfo": {
    "connectorType": "SalesforceDotCom",
    "connectorDetails": { "name": "SalesforceDotCom_Home", "sourceObject": "Patient_Appointment__c" }
  },
  "refreshConfig": { "refreshMode": "UPSERT", "frequency": { "frequencyType": "BATCH" } },
  "sourceFields": [ { "name": "Id", "dataType": "Text" } ],
  "dataLakeObjectInfo": {
    "name": "Patient_Appointments_CRM__dll",
    "label": "Patient Appointments - CRM",
    "category": "Engagement",
    "recordModifiedFieldName": "SystemModstamp",
    "eventDateTimeFieldName": "Appointment_DateTime_c",
    "dataspaceInfo": [ { "name": "default" } ]
  }
}
```

Data stream create traps

| Trap | Detail |
| --- | --- |
| No array wrapper | POST the object directly |
| `dataLakeObjectInfo` takes no field list | Derived from `sourceFields` |
| `dataspaceInfo`, lowercase s, **inside** `dataLakeObjectInfo` | Entries take `name` only |
| `sourceFields` uses `dataType` on create | The read returns `datatype` |
| Source field types must match CRM exactly | Email declared as Text returns `Expected EMAIL, was given Text` |
| No `isPrimaryKey`, `shouldFetchImmediately` or `type` in `connectorDetails` | Rejected on create, present on read |
| Engagement category needs `eventDateTimeFieldName` | Give it the mangled DLO name, not the CRM name |
| DELETE needs `?shouldDeleteDataLakeObject=true` | Otherwise `MALFORMED_QUERY` |

The run trigger is `POST /ssot/data-streams/{name}/actions/run` with `{"refreshType":"FULL"}`, and `POST /ssot/calculated-insights/{apiName}/actions/run` with an empty body. Both return `{"errors":[],"success":true}`. `/actions/refresh`, `/refresh` and `/data-refreshes` all 404 and are not endpoints.

Broken

### Category decides everything downstream and cannot be changed

Profile DLOs map to Profile or Other DMOs. Engagement DLOs map to Engagement DMOs only. `Profile` is the only choice for person-shaped rows, because a data graph root must be Unified Individual, and a Calculated Insight attaches only at graph **root** with the root's primary key as a **dimension** on the insight. Category cannot be changed after stream creation, only dropped and recreated. Decide it first.

DMO mapping

Rule

### DMOs union rows, they do not join columns

Mapping a second DLO into an existing DMO doubles its row count and breaks every segment counting it. To add attributes to the same people, map into the same standard DMO and let identity resolution collapse them, or build a related DMO. Never a second Profile DMO for the same population.

-   Trap
    
    **Identity resolution operates on one DMO.** "Add my object to identity resolution" always means mapping its DLO into `ssot__Individual__dlm` plus `ssot__ContactPointEmail__dlm`, not creating a parallel custom Profile DMO.
    
-   Trap
    
    **`auto-map-dlos` omits `ParentId` to `ssot__PartyId__c` on contact points.** 887 email and 836 phone rows landed with a null party, orphaned, and the match rule needs that link. Always assert `COUNT(ssot__PartyId__c) = COUNT(*)` after auto-map, before running identity resolution. Fix with the Aura `deploy` action and re-run a FULL refresh.
    
-   Trap
    
    **`auto-map-dlos` 500s while the DLO is still `PROCESSING`.** Wait about 60 seconds and retry. The 500 is a timing artefact, not a failure. It also only works for opinionated objects, Individual, ContactPointEmail, ContactPointPhone, Contact, Lead, Account, Order, and returns `mappedDloNames: []` for anything else.
    
-   Trap
    
    **There is no served DLO to DMO mapping API.** The only route is the Aura `DataModelingController/ACTION$deploy` action, which needs a live session.
    
-   Trap
    
    **Field names mangle across three hops.** CRM `Party_Title__c` becomes DLO `Party_Title_c` becomes SQL and deploy source `Party_Title_c__c`. Run `SELECT * … LIMIT 1` once and read the `metadata` block. Never guess.
    
-   Trap
    
    **Always FULL refresh** after a bulk source update or a new mapping. Incremental appends modified rows as new versions and silently duplicates. The tell is `COUNT(*) > COUNT(DISTINCT Id__c)`.
    

Identity resolution

Broken

### The source profile filter is inverted relative to its own UI text

The UI describes an exclude filter. The behaviour is an include filter. `Title Not Equals Physician` produced 743 unified profiles containing blanks, Customer, CFO and CEO, while `WHERE ssot__TitleName__c = 'Physician'` returned **0**.

It also silently drops NULL-valued profiles, because `NULL != 'Physician'` is UNKNOWN, not TRUE. Contacts arrive with empty-string titles so they process; every other source has a genuine NULL and is dropped. The result was 743 unified, which is exactly 889 Contacts minus 146 physicians. **The advertised 62% consolidation rate represented zero merging.**

There is no null-safe operator, the builder offers exactly Equals, In, Not Equals and Not In. The fix is OR'd filter groups keyed on a field every source populates:

```
Group 1:  Individual · Title              · Not Equals · Physician   (Trim Spaces on)
   OR
Group 2:  Individual · Data Source Object · Not Equals · Contact
```

`Data Source Object` is populated for every source and is never NULL. Filter groups are OR'd. This is also future-proof: adding a new source later needed no ruleset change at all.

Rule

### Dry-run the match rule, not a proxy for it

Before adding a source, reproduce the rule's criteria as a self-join and count how many rows will merge versus pile up. Doing that predicted 640 of 640 and correctly forecast unified staying flat. An earlier estimate based on email overlap alone predicted a jump of 120 and was wrong. **State the expected unified-profile count as an acceptance test before running the job**, then explain any gap. One pass predicted 784 and got 930, and the 146-record gap was the finding. A pass that only checks "did it succeed" ships that defect.

-   Trap
    
    **Read the rule, do not trust its label.** The reference ruleset is called "Fuzzy Name and Normalized Email" and contains no fuzzy matching. It is an AND of three exact criteria: first name exact, last name exact, email exactnormalized. Email alone will not resolve. A synthetic identity needs an Individual row with first and last name AND a linked ContactPointEmail row.
    

`PATCH /ssot/identity-resolutions/{id}` writes filters over REST:

Identity resolution PATCH contract

| Constraint | Detail |
| --- | --- |
| Required | `label`, `matchRules`, `reconciliationRules`. Omitting any returns a named `INVALID_INPUT` |
| Rejected | `objectApiName`, `dataSpaceName`, output-only, `JSON_PARSER_ERROR` |
| Not allowed | Two definitions on the same field with the same operator inside one group, which returns "The filter contains a duplicate entry" |
| Use instead | `NOT_IN` with a comma-joined value, for example `"Contact,Individual"` |
| Reconciliation | Send `entityName`, `fields`, `ruleType` and `sources` only. Drop the `linkDmoName` and `unifiedDmoName` the GET returns |
| After PATCH | `rulesetStatus` goes `PUBLISHING` to `PUBLISHED` in about two minutes. Wait for `PUBLISHED` before running |

Broken

### There is no REST endpoint to run a ruleset

`/actions/run` and `/jobs` both 404. Two working routes: the `Run Ruleset` button on the record page, or the Aura `IdentityResolutionController/ACTION$runNow`. Find the button without guessing, `GET /ui-api/actions/record/{id}` lists it as `IdentityResolutionRunNowAction`.

The data graph

The data graph is the grounding object. One retrieval answers who this member is, what their plan and risk look like, what their clinical picture is, and whether you may contact them. Nothing in the agent layer works properly without it.

```
Unified Individual HLS1 (Primary)     5 fields
└── Unified Link Individual HLS1      5
    └── Individual                    3   (+ Title, so role is visible on the grounding object)
        ├── Payer Profile            16
        ├── Patient Profile          14
        └── Communication Consent     6   (Id, PartyId, ChannelTypeId, ConsentStatus,
                                           ContactPointValueText, ConsentCapturedSourceName)
```

Broken

### A node's join key must also be a projected field on that node

The first build returned `status: ERROR` with no message anywhere. The cause was visible only by reading the stored graph back: the consent node's `path` had lost its `fieldName`.

```
"path":[{"devName":"DGRelationships_252_…","parentFieldName":"ssot__Id__c"}]     // broken
"path":[{"devName":"…","fieldName":"Id__c","parentFieldName":"ssot__Id__c"}]   // every good node
```

The join was on `ssot__PartyId__c` without projecting it. The platform silently dropped the join key, accepted the create, and failed the build. Adding `ssot__PartyId__c` to the node's `fields` fixed it on the first retry. **GET the graph and assert that `path[0].fieldName` is present on every non-root node before waiting five minutes on a build.**

-   Trap
    
    **Verify counts on the ID DMO, not the fragments.** `*_FRAGMENT__dlm` DMOs are empty, zero rows, on every graph checked. The `*_ID__dlm` object carries one row per node and record and answers everything:
    
    ```
    SELECT object_name__c AS node,
           COUNT(*) AS rows_,
           COUNT(DISTINCT root__c) AS members
    FROM   <Graph>_<epoch>_ID__dlm
    GROUP BY 1
    ORDER BY 2 DESC
    ```
    
-   Trap
    
    **The ID DMO's name carries a creation epoch you cannot derive.** Find it with `GET /ssot/metadata?entityType=DataModelObject&entityCategory=Related` and match on the display name.
    
-   Trap
    
    **Orgs have a data graph cap.** A failed build still occupies a slot, so delete it before retrying rather than creating a second.
    
-   Trap
    
    **`POST` creates on a 500 and `DELETE` deletes on a 500.** Read the state back rather than trusting the status code.
    
-   Trap
    
    **A standard DMO works as a graph node,** joined on its own foreign key to the parent. `dataType: "DATE_TIME"` is valid input casing, and the output renders as `DateTime`.
    

Note

### Consent inside the graph is not self-describing

`ssot__CommunicationSubscriptionChannelTypeId__c` is an opaque id, and the channel type DMO does not carry the ids consent actually uses. Streaming `CommSubscriptionChannelType` and `CommSubscription` from CRM is what makes the channel nameable from the graph.

Resolve, do not hardcode

## Making it portable

This build references 49 distinct record ids across 22 key prefixes, plus about 21 platform-generated API names. All of them are org-specific. A rebuild in another org gets none of them. The rule: hardcode only names you create yourself, resolve everything else, and make every resolve a named call rather than a guess.

Org-specific anchors and how to resolve them

| Anchor | Resolver | Read |
| --- | --- | --- |
| Identity ruleset id | `GET /ssot/identity-resolutions` | match `label` |
| Ruleset short id | same call | `rulesetId` |
| Unified DMO name | same call | `reconciliationRules[].unifiedDmoName` where `entityName` matches |
| Link DMO name | same call | `reconciliationRules[].linkDmoName` |
| Ruleset object API name | same call | `objectApiName` |
| Data stream record id | `GET /ssot/data-streams/{name}` | `recordId` |
| dseId | same call | `dataLakeObjectInfo.id`, **truncated to 15 characters** |
| DLO API name | same call | `dataLakeObjectInfo.name` |
| DMO relationship id | `GET /ssot/data-model-objects/{dmo}/relationships` | match `sourceField.name` plus `targetObject.name`, check `status: ACTIVE` |
| Data graph | `SELECT Id, Name, DataGraphApiName, Status, LastRunStatus, RecordCount FROM DataGraph` | match `DataGraphApiName` |
| Data graph ID DMO | `GET /ssot/metadata?entityType=DataModelObject&entityCategory=Related` | match `displayName` equal to `<Label> Id` |
| Segment | `GET /ssot/segments` | match `displayName` |
| Segment root DMO id | same call | `segmentOnId` |
| Data space | `SELECT Id, Name FROM DataSpace` | match `Name` |

Rule

### The ruleset call is the single highest-leverage resolver in the build

One `GET /ssot/identity-resolutions` eliminates every unified-DMO literal in every segment, every Apex 360 join, the data graph root and the semantic model, because the names come back per entity in `reconciliationRules`.

Day 1

## The identity spine

Nothing else works until this does. Every segment, dashboard and agent resolves through it.

Build

1.  **Patient spine** → `Patient_Spine_CRM` data stream → Individual + ContactPointEmail. There is no 520-row source object: 520 is the _overlap_ between the payer and patient enrichment sets on `MemberID__c`. The remaining 120 rows on each side are sample-data leftovers. Plan for the overlap, not the total.
2.  **Role attributes**, two CRM data streams: `Individual_Enrichment_Payer__c` → `Payer_Profile__dlm` (640 rows, 42 fields 1:1) and `Patients__c` → `Patient_Profile__dlm` (640).
3.  **Appointment enrichment** → `Patient_Appointment__c` → `Individual_Enrichment_Data_Patient__dlm`. **2,989 rows, verified 3 September 2026.** Earlier versions of this material said 3,725; `queryAll` confirms 2,989 and the discrepancy is unexplained. `Individual_Enrichment_Data_Patient__c` does not exist in the org — the CRM-side object is `Patient_Appointment__c`.
4.  **Contact points**: Individual / ContactPointEmail / ContactPointPhone streams. 887 email, 836 phone.
5.  **Identity ruleset** → `UnifiedssotIndividualHls1__dlm`. Verified live: **784 unified profiles** (759 known, 25 anonymous) from 3,482 source records, 77% consolidation.

Traps at this step

-   Trap
    
    **Corrected: DLO schema is not frozen at stream creation.** This was published as a trap and it is wrong. `PATCH /ssot/data-streams/{name}` with an extended `sourceFields` list adds a column to a live DLO, `PROCESSING` for about 60 seconds, then `ACTIVE`. The dead end that produced the myth was `DELETE …?shouldDeleteDataLakeObject=true` returning 412 while identity resolution holds the DLO. Designing the full field set up front is still cheaper, but it is no longer mandatory.
    
-   Trap
    
    **auto-map-dlos omits `ParentId → ssot__PartyId__c` on contact points.** 887 email and 836 phone rows land with a null party, and the match rule needs that link. The mapping call 500s while the DLO is still PROCESSING — retry after 60 seconds. Einstein auto-map also omits the primary key and saves happily. Fix: assert `COUNT(ssot__PartyId__c) = COUNT(*)` after auto-map, then fix with the Aura deploy action and a FULL refresh.
    
-   Trap
    
    **DMOs union rows, they do not join columns.** Mapping a second DLO into an existing DMO doubles its row count and breaks every segment built on it. Fix: map into the same standard DMO and let identity resolution collapse them, or build a related DMO.
    
-   Trap
    
    **Field name double-mangling.** `X__c` in CRM becomes `X_c` in the DLO becomes `X_c__c` in SQL.
    
-   Trap
    
    **CRM nulls arrive as empty strings.** `IS NOT NULL` is therefore always true — a care-gap measure returned 2,989 instead of 482. Always test `<> ''`.
    
-   Trap
    
    **Incremental refresh appends modified rows as new versions.** 9,000 duplicates from one refresh. A mapping change needs a Full refresh; new mappings never backfill. Corrected: a data-stream refresh endpoint does exist, `POST /ssot/data-streams/{name}/actions/run` with `{"refreshType":"FULL"}`.
    
-   Trap
    
    **Identity resolution's Source Profile Filter is inverted relative to its own UI text** and silently drops NULL-valued profiles. A filter keying on a field one source did not populate admitted 146 physicians, and a reported _62% consolidation rate_ represented zero actual merging. Corrected: these filters are not UI-only, `PATCH /ssot/identity-resolutions/{id}` writes them. Fix: OR'd filter groups keyed on `Data Source Object`, which every source populates.
    
-   Trap
    
    **Reconcile at unified grain, never row grain.** 640 payer rows resolve to 619 unified individuals; row-grain counts overstate by up to 3.
    

How you know it worked

Verified

Unified count matches expectation and the consolidation rate reflects real merging, not a filter artifact. Then check the thing everyone forgets: `doesRunAutomatically` is `false` by default. In this org it still is, and identity resolution last ran 2026-08-29 — five days before twenty-two segments republished against it. Read `lastJobCompleted` before trusting anything downstream.

20-unified-profile.png

![One person, resolved from five source systems. The counter in the header is the whole argument for Day 1.](/screenshots/20-unified-profile.png)

One person, resolved from five source systems. The counter in the header is the whole argument for Day 1.Contact record · HLS\_Unified\_Person\_Page

Day 2

## Consent, then segments

Consent first. It gates every campaign number you will later quote, and retrofitting it invalidates every count you have already shown someone.

Build

-   Individuals, contact points, per-subscription consent: 891 Individuals, 1,723 contact point type consents, **5,828 consent rows** (verified). Generator: `SEEConsentSeed`.
-   Mobile numbers: 836 of 889 Contacts, area code derived from MailingState, line number hashed. Generator: `SEEMobileSeed`.
-   Four CommSubscriptions × Email and SMS channel types = 8 combinations.
-   Segments: six `cld_` campaign audiences over the `MarketSegment` sObject.

Verified

### Why consent is the point, not the paperwork.

426 members carry _Not Renewed_. Only **268 are actually reachable**. That single number is why the campaign reach figures in this demo are defensible, and why a reachability waterfall is worth building.

### Segment creation, the working route

`PATCH /services/data/vXX/sobjects/MarketSegment/{1sg...}` with `IncludeCriteria` as a **string**. Returns 204, the post-commit hook fires on plain DML, `SegmentStatus` goes to `COUNTING` within seconds and `LastSegmentMemberCount` lands in 30–60 seconds. This supersedes every earlier note saying segment criteria need a browser.

`PATCH /ssot/segments/{apiName}` is dead — it fails on `referredColumns`, a server-derived value the Connect path cannot compute and which is never a valid input. `POST /ssot/segments` does accept `includeCriteria` on create.

Traps at this step

-   Trap
    
    **One headless segment = one predicate.** Compound criteria save with SUCCESS and are silently ignored.
    
-   Trap
    
    **`NO_REFRESH` is the default and it is permanent.** Frozen membership behind a green status.
    
-   Trap
    
    **A segment can report `lastSegmentMemberCount` while `segmentMembershipTable` is null and have never materialised.** Reading the count alone injects phantom people. This project documented that trap and then committed it: `cld_2027 Win-Back Eligible` was catalogued as a real audience on the strength of a 784 count that was the entire population. A full sweep on 3 September found it is **four segments, not one** — `cld_2027 Win-Back Eligible` (784), `Members - No Upcoming Appointments` (479 of 779), and two `zz_` probes. All four report a count with `SegmentMembershipTable`, `PublishStatus` and `LastPublishedEndDateTime` all null. Check all three fields, never the count.
    
-   Trap
    
    **A segment can have working criteria that no API will show you.** `HLS - No Upcoming Appointments` has a materialised membership table and 366 real members, so it demonstrably has criteria — yet `IncludeCriteria` is null on the sObject and the segment is absent from `/ssot/segments`. Five segments in this org are in that state. A REST-only audit will report them as criteria-free, and they are not.
    
-   Trap
    
    **Segments write into a shared membership table** — four of them in this org, not one. `Delta_Type__c` carries `new` and `existing` for the same person, so any count not using `COUNT(DISTINCT Id__c)` is silently doubled.
    
-   Trap
    
    **`MarketSegment` has no `DeveloperName` or `ApiName` column.** The API name lives on `MarketSegmentDefinition`, which neither the data API, the Tooling API nor Apex SOQL will query. Use `ConnectApi.CdpSegment.getSegmentsPaginated(batchSize, offset, orderBy)` — batch size first, and an offset past the end returns an empty list rather than an error.
    
-   Trap
    
    **Segments cannot be deleted headlessly,** and not at all once an Activation references them. Never create probe segments.
    
-   Trap
    
    **`excludeCriteria` is not exposed by the REST read.** A REST-only audit shows a filtered segment as criteria-free.
    
-   Trap
    
    **Consent specifics.** `CommSubscriptionConsent.ContactPointValue` caps at 40 characters, so 216 of this org's email addresses cannot be keyed on it — join on PartyId instead. Consent value casing here is `OptIn`, not `OPT_IN`. Every create of a Custom Preference Page also sets it as that channel's default, displacing the existing one.
    

12-campaign-audience.png

![185 of 230 members will actually receive this send. Two opted out; forty-three were already over the eight-in-thirty-days cap.](/screenshots/12-campaign-audience.png)

185 of 230 members will actually receive this send. Two opted out; forty-three were already over the eight-in-thirty-days cap.Campaign record · Audience tab

05-apply-blocked.png

![The Apply button refuses to write an order Data Cloud would reject. The blocked state is the honest screenshot.](/screenshots/05-apply-blocked.png)

The Apply button refuses to write an order Data Cloud would reject. The blocked state is the honest screenshot./lightning/n/cld\_Audience\_Overlap · tab 5

### Waterfall segments — the contract

A waterfall is an ordered list of up to 20 segments sharing one Segment On entity, evaluated top down, first match wins, every person in exactly one bucket.

An earlier version of this material said _MarketSegment exposes no SegmentType field, so a waterfall cannot be created headlessly — it is UI-only._ **That was wrong on both counts.** The field is `MarketSegmentType` (values `UI`, `DBT`, `Lookalike`, `EinsteinGptSegmentsUI`, `Waterfall`, `Realtime`, `Dynamic`). It is `createable: false` on the sObject, so the sObject cannot _create_ one — but the ordered child list is not a child object at all. It is the waterfall's own `IncludeCriteria`, and `IncludeCriteria` is plainly updateable:

```
{"type":"Waterfall","segments":[
  {"type":"NestedSegment","nestingType":"definition","filterId":null,
   "segmentId":"<child apiName>",
   "subject":{"segmentDevName":"<child apiName>",
              "fieldApiName":"<child displayName>",
              "nestedPublishBehavior":"useSegmentCriteria"}}
]}
```

**Array position is the priority.** There is no rank or sequence field. Reordering a waterfall is one write to one field, proven live. `MarketSegmentDefMapping` looks like the obvious home for the child list and is a red herring.

Eligibility is the real constraint: children must share the Segment On entity, not already be in another segment, and carry no publish schedule. `PublishScheduleInterval` is updateable, so clearing it is also one write — but it is destructive if you do not record the old schedule first.

**Coverage does not change with priority order.** The union is the union; only the distribution changes. The real decision is protecting small specialised audiences from large generic ones, which is why _specific first_ is the working default.

Day 3

## Engagement, campaigns, content

This is what makes every dashboard non-empty. Skip it and Day 4 shows you zeros you cannot diagnose.

Build

Day 3 datasets and generators

| Dataset | Volume (verified) | Generator |
| --- | --- | --- |
| Six-campaign email and SMS | 4,312 rows | `cld_SixEngagementSeed` |
| Six-campaign web | 240 rows — 192 pageview, 48 conversion | `web()` builder, UTM in the page URL |
| Win-back engagement | 666 rows reaching 235 of 268 members | `cld_WinbackEngagementSeed` |
| Web category affinity | 5,600 rows / 344 individuals / 8 categories | `cld_WebAffinitySeed` |
| Campaign members | 921 across 6 campaigns | `cld_SixAudienceSeed`, idempotent |
| Encounters | 1,838 — 1,330 ED, 399 IP, 109 UC, 38 readmits | `SEEEncounterSeed`, FNV-1a deterministic |
| Message registry | 26 usable variants on `B2B_Mood_Board__c`, plus one blank stock record | upsert on `cld_Message_Id__c` |
| CMS content | 25 pieces, 6 campaigns, all Published | `build.py` from `specs/*.json` |

### CMS content — the write and read contracts

`PATCH /connect/cms/contents/{id}` returns **405**. The write path is `PUT .../variants/{variantId}` on the `9Ps` id and it replaces the whole variant — a partial body returns a bare 500. The API wants `contentBody`, not the catalogued `contentData`. Every block `id` must be a real UUID. Send raw HTML; the API escapes on write.

**The escaping pipeline, both directions.** On write: a URL sanitizer rewrites `=` to `&#61;` and `&` to `&amp;` inside quoted `src` and `href` in `rawHtml`, then one HTML escape runs over the whole string. The inverse is one unescape pass, then a targeted URL fix — never _unescape until stable_, which is destructive and silent. `<p>Base hero &amp; more</p>` is stored double-escaped, so one pass restores it and a second turns the intended entity into a bare ampersand, with a 200 and no warning.

Reading content back out into flat fields an LWC can bind to: headline is the first `lightning/heading`, body the first `lightning/paragraph`, CTA the first `lightning/button` (`text` plus `uri`), bullets the `<li>` items of the first `lightning/html` containing `<ul`. Identify the hero image by `width="600"`, never by position — every email has a logo image first, sometimes `lightning/html` at 200px, sometimes a native `lightning/image` at 210. And harvest only from `contentBody.sfdc_cms:block`: `sfdc_cms:variants[]` carries alternate heroes on the same block id, so a whole-document scan picks the personalization image over the base one in most cases.

`attributes.uri` on a button is a plain JSON string, not escaped HTML, and still arrives with a literal `&amp;` between UTM parameters. Written through unchanged, it stores a broken link.

Broken

### Images: the trap that has two right answers

CMS images cannot be created from inline base64. Use Files → `ContentVersion` → `ContentDistribution`.

Earlier versions of this material said _take `ContentDownloadUrl`, not `DistributionPublicUrl`_ and left it there. That is right for an email client and **wrong for an `<img>` tag in a Lightning component**. `ContentDownloadUrl` points at `/sfc/dist/version/download/`, which is an attachment endpoint: the browser downloads the file and renders nothing. For anything that has to display in-page, use the rendition endpoint instead:

`/sfc/servlet.shepherd/version/renditionDownload?rendition=ORIGINAL_Jpg&versionId=068...`

A `ContentVersion` without a `ContentDistribution` has no public URL at all. This org has one, and it silently breaks the email that references it.

Traps at this step

-   Trap
    
    **Modulo rate allocation only hits its target at multiples of 100.** At n=66, a 38% target produced 58%. Use Bresenham.
    
-   Trap
    
    **`String.hashCode()` changes by exactly 1 when the last character does.** `id + '#' + salt` yields correlated draws and a bimodal distribution. Salt first, then mix, in Long space.
    
-   Trap
    
    **Unordered SOQL breaks first-wins de-duplication.** Generator and verifier picked different winners and produced 13 phantom mismatches. `ORDER BY Id` in both.
    
-   Trap
    
    **MC Next dashboard tiles need exact vocabulary:** `SENT` / `NOT_SENT`, `HARD_BOUNCE` / `SOFT_BOUNCE`. Not _uppercase everything_ — `Bounced` must become `NOT_SENT`. A missing `SEND` zeroes six tiles at once, and the diagnostic signature is unmistakable: CTR works while everything else reads zero.
    
-   Trap
    
    **The column names are not what you would guess.** The action column is `ssot__EngagementChannelActionId__c`, not `...ActionTypeId__c`. `ssot__UtmCampaignName__c` holds the _subject line_; the campaign id is in `ssot__UtmId__c`. The org field is literally misspelled `emailConversionGaolAchieved__c`.
    
-   Trap
    
    **Two contaminating sources must be excluded inside every measure expression, never as a dashboard filter.** `Marketing_Performance_Mock_Data` (19,529 rows, correct attribution, broken identity) and legacy web ingestion (20,033 rows, stale since 2026-02-18, zero UTM). Filter on `ssot__DataSourceId__c = 'Salesforce_Home'`.
    

How you know it worked

Verified

Dashboard tiles are non-zero _and_ CTR is not the only one that works. Campaign member counts reconcile against engagement rows — in this org they do not: 33 of 268 Win-Back members have no seeded activity, and the parent campaign holds zero members while all 268 sit on three children.

13-campaign-content.png

![Both email arms and the SMS, rendering from CMS. Getting the hero images to appear took the rendition endpoint, not the download endpoint.](/screenshots/13-campaign-content.png)

Both email arms and the SMS, rendering from CMS. Getting the hero images to appear took the rendition endpoint, not the download endpoint.Campaign record · Content tab

11-campaign-outcomes.png

![Five attribution models over the same campaign, so the number has a stated basis. The footnotes say what the model cannot see.](/screenshots/11-campaign-outcomes.png)

Five attribution models over the same campaign, so the number has a stated basis. The footnotes say what the model cannot see.Campaign record · Campaign Outcomes tab

Day 4

## Dashboards

Every widget can return Ok and the story on the page can still be wrong. Those are two separate checks.

Build

Two semantic models, then vizzes, then dashboards. `HLS_Member_360_SDM` (73 fields, 16 metrics, 4 HEDIS calculated measurements) and `Marketing_Performance_Semantic_Mod_f001` (278 fields, 52 metrics, 99 calculated measurements). Then six dashboards: Population Health (43 widgets, 2 Sankeys, 2 condition-by-tier heat maps), Membership Overview (40), MI Home (30), MI Segment Overview (28), MI A/B Testing (22), Campaign Deliverability (29, app-installed).

Traps at this step

-   Trap
    
    **The v65 / v66 / v67 split is a per-asset property, not a required API version.** Each dashboard and visualization carries its own `Version` column — in this org, five dashboards at 67 and one at 65; visualizations split 5 / 30 / 3 across 65, 66 and 67. Every one of them reads cleanly over the **v66.0** endpoint regardless of its own value. A re-test on 3 September 2026 could not reproduce a `DOWNGRADE_VERSION_ERROR` on any read at either version.  
      
    What does happen, and reads exactly like a permissions fault: `GET /analytics/dashboards/{0Tr…}` returns **400 `INSUFFICIENT_ACCESS` identically at v66.0 and v67.0**. That is the CRM Analytics route refusing a Tableau Next asset — wrong endpoint family, not wrong version and not wrong permissions. `/wave/dashboards/{id}` returns 400, `/connect/analytics/dashboards/{id}` and `/analytics/visualizations/{id}` return 404, at both versions. Read these objects over SOQL instead.  
      
    **Scope of this correction:** the retest covered reads only. The original claim may still hold on write paths, which were not re-exercised.
    
-   Trap
    
    **Most of a Tableau Next dashboard cannot be exported.** Of 195 widgets across six dashboards, 78 have a retrievable definition and **117 do not**: all 81 text widgets return only an id and a parent id, so every heading and caption has to be retyped; all 33 filter widgets carry no field reference; and `AnalyticsDashboardViewDef` returns zero rows, so no per-widget placement survives — only the grid (72 columns, 12px rows, 1150px max width). **Semantic models have no read endpoint at all** — `AnalyticsSemanticModel` and `SemanticModel` are `INVALID_TYPE` on both APIs, and `/ssot/semantic-models` 404s. Only the semantic object names each visualization binds to survive. This is an export limit, not a build limit: the self-service authoring API writes visualizations, widget placement and global filters, so the dashboards are a spec-and-replay problem.
    
-   Trap
    
    **`PATCH` is full replace** on both vizzes and dashboards. A partial PATCH once wiped a dashboard to a single widget. GET first, send the whole document. Widget `source` must be stripped to `{id, name}`.
    
-   Trap
    
    **A reference line needs both `visualSpecification.referenceLines[key]` and `style.referenceLines[key]`.** PATCH returns 200 with only the first and the chart renders _Can't show visualization_.
    
-   Trap
    
    **A field carrying a filter cannot also carry a sort.** Use `style.allHeaders.fields.<key>.hiddenValues` so the sort survives. Mandatory for funnels.
    
-   Trap
    
    **Aggregation vocabulary differs by API on the same platform:** `Avg` in visualizations, `Average` in metrics.
    
-   Trap
    
    **A dashboard filter silently ignores any tile whose metric `additionalDimensions` lacks the field.** Empty `widgetIds` under `filterMode: include` means the widget answers to nothing. And a filter widget binds to exactly one semantic model — plan mixed-model dashboards rather than discovering them.
    
-   Trap
    
    **The semantic layer renames colliding fields with a numeric suffix** — `Age` becomes `Age1`. Read the real names off the create response.
    
-   Trap
    
    **Per-category colour mapping, gradients and the entire radial family are not available over REST.** Build in the UI, then place by `source: {id, name}`.
    
-   Trap
    
    **`analytics_embedding:dashboard` with `dashboardId = INVALID_DASHBOARD` makes a FlexiPage permanently unsaveable over the API.** Build a new page; never try to repair in place. Three pages in this org carry it.
    

How you know it worked

Verified

Every widget returns `status: Ok` — in this org all 192 do. Then reconcile the narrative against the SQL, because that is a separate failure: one dashboard hero read _56,071 / 11,105_ (email only) against all-channel tiles.

14-population-health.png

![Forty-three widgets, all returning Ok. Cost against risk tier, with the book average as a reference line.](/screenshots/14-population-health.png)

Forty-three widgets, all returning Ok. Cost against risk tier, with the book average as a reference line./lightning/n/Population\_Health

Day 5

## Custom components

Build the capability layer before anything that binds to data. It was missing from the original catalogue entirely, and that is the most consequential omission in it.

Build

`cld_OrgCapability` and `cld_EngagementSchema` are the reason every controller in this pack deploys and runs in an org that has none of the objects it expects. They resolve object and field names at runtime, return empty rather than throwing when something is absent, and let a component degrade instead of failing.

Then the components, in rough order of dependency: campaign record page tabs (Audience, Members, Content, Outcomes, A/B), the 360 pages, then the standalone app pages.

### The LWC deployment sequence

Three project documents disagreed about this. Resolved:

1.  `POST LightningComponentBundle` — accepts scalars only. `Metadata.targets` does not stick and no `.js-meta.xml` is auto-created.
2.  `POST` the `.js` resource. **Before the `.html`**, or you get `No base file for markup://c:<name>`.
3.  `POST` the `.html`.
4.  `POST` the `.js-meta.xml` explicitly, `Format: "xml"`. It is a normal writable resource and the only way to set `targets`.
5.  `POST` the `.css`.
6.  **Reference the component from a FlexiPage. This is the compile check.** A resource POST succeeds even when the template will not compile; the FlexiPage write is what forces evaluation. Treat its 204 as the green light, not the 201 on the bundle.

`LightningComponentResource.Source` is plain text, not base64 — the bundle create's `lwcResources` _is_ base64, and two documents recorded this backwards. Payloads truncate above roughly 8 KB. `LightningComponentBundleId` is insert-only.

### Updating Apex that an LWC already binds to

`ApexClass.Body` cannot be PATCHed, and delete-and-repost fails with `DEPENDENCY_EXISTS` once a component binds. Use: `POST MetadataContainer` (name caps at 32 characters) → `POST ApexClassMember {MetadataContainerId, ContentEntityId, Body}` → `POST ContainerAsyncRequest {IsCheckOnly:false}` → poll `State`. A container is single-use after success, and a failed container rolls back everything including members that reported success. On failure `ErrorMsg` is null — the real messages are in `DeployDetails.componentFailures`, fetched by record id.

Traps at this step

-   Trap
    
    **`:root` is rejected in LWC scoped CSS** (`LWC1535`). Put design tokens on `:host` or the component's own root class; dark mode via a plain `@media (prefers-color-scheme: dark)` block. The `:root[data-theme="dark"]` pattern that works in a hosted page does not compile here.
    
-   Trap
    
    **`fetch('/services/data/...')` from an LWC returns 401.** A Lightning session cookie is not an API session, and no header or `credentials` option changes that. Before designing around a browser fetch, look for a `ConnectApi` Apex equivalent — many Connect resources have one, and it needs no callout, no named credential and no connected app.
    
-   Trap
    
    **A wire parameter bound to an undefined `$recordId` never fires** and renders an empty state with no error. Bind to a field set in `connectedCallback`.
    
-   Trap
    
    **A getter that reads selection state must not be read by the model that selection depends on.** Stamping a `rowClass` onto each row by comparing against `selectedKey` made the model depend on the selection while the selection resolved back through the model — an infinite getter cycle. Selection-dependent decoration belongs in per-view getters.
    
-   Trap
    
    **A LongTextArea cannot be filtered in SOQL at all,** not merely aggregated. `WHERE cld_Preview_Body__c != null` returns `INVALID_FIELD ... can not be filtered in a query call`. Select it and test in memory.
    
-   Trap
    
    **`FlexiPage.Metadata.description` caps at 255** and fails with no field named in the error, so it reads as a body-shape problem rather than a length one. `itemInstances` require a unique `identifier` (undocumented). `CustomTab.Metadata.available` breaks the JSON parser — omit it.
    
-   Trap
    
    **Canvas 2D and WebGL do work in LWC under Lightning Web Security.** No iframe needed.
    

### Tabs, and why eight successful steps still show nothing

`PermissionSetTabSetting.Visibility` is `DefaultOn`, not `Visible`, and `.Name` takes the bare developer name — the `standard-` prefix is for standard tabs only. `SetupEntityAccess.SetupEntityType` is not writable on insert; send only `ParentId` and `SetupEntityId`. A tab listed in `CustomApplication.Metadata.tabs` is invisible without a `PermissionSetTabSetting`. `CustomApplication` PATCH needs the full envelope — label, navType, uiType, tabs, formFactors, brand, utilityBar — or it fails with a misleading _Lightning apps only_.

And then: all eight steps can succeed and the tab still not appear, because a user's personalized app navigation is authoritative and does not absorb newly added tabs. Diagnose by comparing the rendered nav against `Metadata.tabs`, and check `/lightning/n/<tab>` directly first. Querying `PermissionSetTabSetting` without a `ParentId` filter returns zero rows with HTTP 200, which reads exactly like a failed write and is not.

How you know it worked

Verified

The FlexiPage write returns 204 and the component renders on the page for a user who is not you. A resource POST returning 201 proves nothing about whether the template compiles.

08-ab-confidence.png

![While the two intervals overlap, the versions are statistically the same thing. This test needs 735 opens per arm and has 22.](/screenshots/08-ab-confidence.png)

While the two intervals overlap, the versions are statistically the same thing. This test needs 735 opens per arm and has 22./lightning/n/cld\_AB\_Test\_Console · tab 4

19-marketing-nav.png

![Custom tabs, deployed headlessly. Eight API calls can all succeed and the tab still not appear.](/screenshots/19-marketing-nav.png)

Custom tabs, deployed headlessly. Eight API calls can all succeed and the tab still not appear.Marketing app · navigation

Day 6

## Agents

One irreversible field decides whether anyone can ever see the agent.

Build

Four agents, not five: `Makana_Data_Agent` (InternalCopilot, v3 Active, picker-visible), `Makana_Segmentation_Assistant` (InternalCopilot, v1 Active, picker-visible), `HLS_Member_360_Agent` (ExternalCopilot, v2 Active, **not** picker-visible), and `Makana_Analytics_Agent` (ExternalCopilot, v1 Inactive — a complete, never-activated duplicate). Four Apex actions: `HLSAnalyticsQueryAction`, `HLSMemberProfileAction`, `HLSCareGapCohortAction`, `HLSSegmentCatalogAction`.

Traps at this step

-   Trap
    
    **Without `agent_type: "AgentforceEmployeeAgent"` the compiler defaults to a service agent that never appears in the in-org picker.** `config.agent_type` is immutable after first publish, and publish is not reversible over the API — a wrong publish leaves an undeletable orphan. This org has one.
    
-   Trap
    
    **Picker visibility additionally requires a `SetupEntityAccess` row of type `BotDefinition`.** Active is necessary and not sufficient, and System Administrator is not exempt. `GenAiPlanner` is not a valid `SetupEntityType`. Confirmed by extraction: `SetupEntityAccess` returns exactly **two** rows across all four agents and all seven planners. The two picker-visible agents have one each. The two that are not visible fail _both_ tests at once — wrong immutable `agent_type` **and** no `SetupEntityAccess` row — so fixing either one alone would not have surfaced them.
    
-   Trap
    
    **Publish returns an `errors` array even on success.** Read `severity`, not emptiness.
    
-   Trap
    
    **Numeric Apex outputs need `type: object` plus `complex_data_type_name: lightning__numberType`.** Compile passes; publish fails.
    
-   Trap
    
    **Skip `/nextgen-authoring/.../validate`.** It reads the request body, not the saved asset. `publish` is the real validation.
    
-   Trap
    
    **A Decision element in a Flow cannot see profile attributes unless a Data Graph exposes them.**
    

How you know it worked

Verified

The agent appears in the picker for a non-admin user. Not for you — you are not exempt, but you may already carry the permission set from something else.

Section 5

## Customer evidence

Every quote below is verbatim from a meeting transcript, anonymised to a segment descriptor. 205 meetings across five months, all first-party. Where a theme has no evidence, it says so.

The themes are ordered by **evidence strength, not by build effort**. Two of them disagree with where the work actually went. That disagreement is the point of doing the pass.

Note

### A note on method

An earlier version of this analysis carried eight _confirmed_ customer quotes drawn from a meeting-search tool’s synthesised answer layer. **None survived verification.** Several were attributed to organisations that do not appear anywhere in the 205-meeting pool — they were other people’s notes, visible in a shared workspace. Every quote here was read from a verbatim transcript of a meeting the author attended. That distinction is the difference between evidence and a plausible sentence.

Themes ranked by evidence strength

| Rank | Theme | Evidence | What it leads with |
| --- | --- | --- | --- |
| 1   | Identity resolution and duplicates | 6 quotes, 2 accounts | Unified profile, identity ruleset |
| 2   | Consent and true reachability | 5 quotes, 4 accounts | Consent model, reachability waterfall |
| 3   | Segmentation self-service | 6 quotes, 5 accounts | Segment API over REST, segmentation agent |
| 4   | Campaign build speed | 6 quotes, 3 accounts | Six-campaign pack, the rebuild path itself |
| 5   | Trust in the numbers | 4 quotes, 3 accounts | Engagement vocabulary, contamination filtering |
| 6   | Compliance and legal exposure | 5 quotes, 3 accounts | Regulatory Watch |
| 7   | Consumption cost predictability | 4 quotes, 4 accounts | Consumption rate card |
| 8   | Anonymous-to-known web behaviour | 2 quotes | Web affinity wheel |
| 9   | Adoption | 3 quotes | Trailblazer Tank, Plan Year |
| 10  | Care gaps and Star Ratings | 3 quotes, quantified | HEDIS measures, Population Health |
| 11  | Proving marketing worked | aspiration only | Campaign Outcomes, attribution models |
| 12  | Dashboards | reframed — see below | Dataviz standard |
| 13  | Portability | thin | This guide |
| 14  | Audience overlap | no demand signal — see below | Audience Priority |
| —   | Network adequacy | absent — cut from the page | —   |

### 1 · Identity resolution and duplicates

> when we have a patient subscriber who does not have an email address … data cloud is somehow pulling in teammate email addresses and merging it into those subscriber records. And what we’ve seen is, at least for one poor teammate, she’s tagged on about 15 different subscriber records. And so anytime we try to send a message to one of those patients, she’s getting the email.

— Director of technical delivery, a national dialysis provider

> Somebody keyed in the wrong phone number into our patient registration system … Turns out it’s not the patient’s phone number. It belongs to rando dude. You know, three houses away.

— Architecture lead, a national dialysis provider

> in these five systems you’ve got Bob Smith here and Bob Smith here — are they the same Bob Smith?

— VP of IT, a nonprofit senior living organisation

### 2 · Consent and true reachability

> A what in place? Sorry. … I don’t know what that is. … Oh, like, I don’t know. I didn’t set any of that up.

— Marketing lead at a national diagnostic imaging network, asked whether they have a preference centre

> We have had tremendous challenge with opt-in. Just we send a campaign and then it goes to 50% of the audience because the other 50% has not been opted in.

— Marketing manager, a large Southwest health system

> SMS is kind of an embattled channel right now … we just don’t have critical mass in the channel at all right now.

— Marketing lead, a regional health plan

### 3 · Segmentation self-service

> \[a\] certified Salesforce admin, platform app builder, query guru … is working with MDM teams and database teams and everybody for weeks to try to figure out how do I structure this segment so that I get all of the people that I need?

— Architecture lead, a national dialysis provider

> we send a campaign, look at the insights and deliverability. See that a large percentage did not receive it. We build a report for that audience who didn’t get it, download the report, upload it to consent, opt them in manually and then resend the campaign.

— Marketing manager, a large Southwest health system

> we’re generating these lists twice a week. It’s just not real time, and anything short of real time is not timely in the modern world.

— Marketing lead, a regional health plan

### 4 · Campaign build speed

> Launching from the segment, we would typically take a sprint which is two weeks … pretty much we take about three weeks to four weeks from end to end.

— Marketing developer, a national dialysis provider

> we are hoping that within one or two years we will be in a better place. But for now we don’t have any ETA on when this will all be resolved. But every campaign is a struggle.

— Marketing developer, a national dialysis provider

> \[they\] want to turn this around in three weeks when this new legislation launches. And I’m like … I can’t help you in three weeks.

— Architecture lead, a national dialysis provider

### 5 · Trust in the numbers

> we sent the summer newsletter in the middle of July … and we realized that the deliverability rate had dropped to 72%, which was really alarming because the data does not change drastically from one newsletter to the other.

— Marketing manager, a large West Coast academic health system

> when I do a count of accounts where the person email is null and the status is active … I get 113,000.

— A national dialysis provider. A colleague had answered the same question as 11,000 moments earlier

> if this message does not go to some patient — how do we know which message? Because there’s so many messages going in a day.

— Technical lead, a national diagnostic imaging network

### 6 · Compliance and legal exposure

> our legal team did not allow us to use anything beyond a patient identifier to unify patients … the only thing that was legally blessed to bring patient data together.

— Architecture lead, a national dialysis provider

> If you’re the same human going to \[the .com\] or having a registration on \[the portal\], I actually do not link that together, even if I know that you’re the same human. And that’s all because of just legal guidance … it is slow. And it is self inflicted.

— Architecture lead, a national dialysis provider

> Everyone’s so scared of TCPA compliance as well that it’s like, hesitant to make any big decisions there.

— Marketing lead, a regional health plan

### 7 · Consumption cost predictability

> \[other cloud vendors\] are all giving us sandbox for free right now … So it’s like why would we do it … and pay a premium cost just to even prove it out that it works? We are not going to get that approved, period.

— Platform leader, a large multi-state health system

> can it be pulled in and referenced by these agents without incurring that data cloud cost?

— Marketing lead, a regional health plan

> we don’t want to over-engineer, and that’s probably more from a cost perspective than anything else.

— Data systems engineer, a nonprofit senior living organisation

### 8 · Anonymous-to-known web behaviour

> someone could have been browsing the site for kidney-related resources for months, maybe even years, and then ultimately they become a patient … we want to understand that journey long term. What content were they consuming? When did they turn from prospect to patient? That has vast implications for our business.

— Digital lead, a national dialysis provider

21-web-affinity.png

![What one person has been reading, built from 21 web interactions across 19 active days.](/screenshots/21-web-affinity.png)

What one person has been reading, built from 21 web interactions across 19 active days.Contact record · Web Affinity tab

### 9 · Adoption

> I’ve tried. I have tried. I’ve clawed my way and I’ve gotten almost, but then I get stuck. It’s too techy.

— Marketing manager, a large Southwest health system, on a query tool she is licensed for

> we just don’t know what we don’t know.

— Marketing director, a large Southwest health system

### 10 · Care gaps and Star Ratings

> if you were to look at \[us\] on the whole, you’d say that our goals were to improve our stars measures … improving med adherence, improving the CAHPS survey responses.

— Marketing lead, a regional health plan

> the people involved in the med adherence journeys are becoming compliant on their med schedules at like an 88 rate. Now, we don’t have a lift number on that because out of an abundance of caution, we didn’t have a control group.

— Marketing lead, a regional health plan

### 12 · Dashboards — the theme was framed wrong

The original framing was _dashboards are inventories nobody can act on_. **No customer said that.** Nobody rejected a dashboard, called reports unread, or asked _so what_. The evidence says something more uncomfortable: they never got as far as a dashboard.

> We actually have the engagement metrics going into the data cloud but we are not utilising them.

— Marketing developer, a national dialysis provider

> A year and 27 million records later and zero campaigns. We turned off the integration and we purged the data. Because it wasn’t fulfilling a purpose.

— Architecture lead, a national dialysis provider

That second line is the strongest in the entire corpus, and it argues for measuring fewer things and acting on them.

### 14 · Audience overlap — the honest negative result

Broken

Across 205 meetings, **no customer asks how many outreaches is too many**, names a communication cap, or describes a person sitting in several campaigns at once. Asked directly about duplicates and over-contact, one large health system answered _not really_ and _No, I’m not super aware._

But the problem is everywhere — solved badly, by hand, without a name:

> part of the criteria for whether you’re eligible to receive \[next best action\] 140 is: have you received 140 in the last X number of months? And if you have, then you’re ineligible as part of that query.

— A regional health plan. A real frequency ceiling, hand-written in Databricks and re-implemented for every next-best-action

> we made sure to suppress any email address ending with \[our domain\] … But a teammate who works here can also be a patient.

— A national dialysis provider. Suppression with no precision

> the other part is to have these two systems sending simultaneously to marketing cloud and not tripping over each other, which I think is what would happen if I just continued to send things out through the transactional API.

— A national diagnostic imaging network. Competing campaigns as a structural fact

**Customers do not ask for frequency capping, because they do not know it is something you can ask for. They build cooldowns in a data warehouse, block an entire email domain, or hope two systems do not collide. A demo that hands them the ceiling directly is answering a question they have not learned to phrase.**

10-regulatory-watch.png

![The regulation already has a word for the problem the marketers could not name.](/screenshots/10-regulatory-watch.png)

The regulation already has a word for the problem the marketers could not name./lightning/n/cld\_Regulatory\_Watch

The compliance layer says the same thing from the other direction. Regulatory Watch’s top open finding in this org reads: _47 CFR 64.1200(a)(2) — the healthcare exemption permits 1 message per day and 3 per week per provider. This org has zero `DataCommunicationCap` records, so nothing enforces any ceiling._ The marketers have no word for the problem. The regulation already does.

**Cut from the page: network adequacy and access.** No transcript evidence across all 205 meetings. These accounts engage on marketing and CDP, not network operations. Care Access maps and drive-time matrices stay in the build inventory; they do not get a customer-problem slide.

Section 6

## Verification

Five read-only sweeps, reproducible in about four minutes, run against the live org before any demo. Everything in this document was checked against them on 3 September 2026.

Verified

### What one pass caught

Independently, in a single run: a component that was never built; two agents that turned out to be one record; a segment reporting 784 phantom members; a 3× overstatement of brand assets; an identity ruleset five days stale; a Calculated Insight failing for thirteen days; and two campaigns with no hero imagery.

**Status in a catalogue decays faster than content.** Every drifted number in that report was correct when written, some within the same week. Carry a verification date per section and a way to re-run the check — never a static badge.

### What you inherit as broken

Honest list. These are real in the source org today and a rebuild will reproduce them unless you fix them deliberately.

Broken

Known broken state inherited from the source org

| Gap | Detail |
| --- | --- |
| Identity resolution stale | Last ran 2026-08-29, `doesRunAutomatically: false` |
| `Decision_Authority__cio` | `PROCESSING_FAILED` since 2026-08-21 on an hourly schedule |
| Seven Calculated Insights | Never executed. Return 0 rows with no error |
| Hero imagery | None for Flu Shot or Primary Care — the two largest audiences. One asset has a `ContentVersion` with no `ContentDistribution` |
| Female member portraits | 340 uncovered of 640 |
| Volume | Target architecture calls for ~9,500 in Tier 2; 520–640 exist. Every dashboard is built on a hero-sized dataset |
| Six orphaned LWC bundles | `cldAbVariantCompare`, `cldAudienceOverlap`, `cldAudiencePriority`, `cldAudienceWarnings`, `cldPreviewHarvest`, and `cldPopulationHealth` — the last was previously miscounted as live. `Population_Health_Page` hosts the Tableau dashboard via `analytics_embedding:dashboard`, not the component |
| Six compliance objects | Designed, packaged, never deployed — they need the Metadata API |
| `cld_Affinity Womens Health Interest` | Publishes SUCCESS, resolves to 0 members |
| Win-Back campaign structure | Parent holds 0 members; 33 of 268 children have no engagement |
| A/B console segment link | `cld_AbTestConsoleController` hardcodes one segment id, so the _open segment_ link shows the Win-Back audience on every campaign. Wrong for six of seven campaign families |
| Harmonized attribute escaping | The unified profile renders `Surgical &amp; Procedural` as literal text — one unescape pass missing on the EHR service-line tile |
| Two identity rulesets consolidate nothing | `HLS Physicians` reports a 93% consolidation rate against **zero** matched source profiles over 1,951 records. `ACCT - 001` resolves 91 accounts to 91 |
| `Closes_Care_Gap__c` | All 2,989 appointments. The care-gap flag the campaign story is built on is never set |
| Five segments have unreadable criteria | Including one with 366 materialised members |
| Consent could not be joined to contacts | `Contact.IndividualId` was missing from the original export; consent hangs off `Individual.PartyId`. Fixed in the asset pack, but any export that omits that one column produces consent rows that join to nothing |

### Deliberate defects, which are not gaps

79 + 11 corrupted state codes, 53 stripped-leading-zero zips, 123 of 130 Payer Age versus Contact Birthdate conflicts over three years, 9,000 duplicate engagement rows from an incremental refresh. **These exist on purpose**, so that harmonization has something to fix and the _before_ state is real. Do not clean them without deciding to.

Section 7

## Traps index

Every trap in this document, in one place. Each one cost at least an hour. Filter by the day you are on or the surface you are fighting.

Search traps

Day

Surface

62 of 62 traps

-   Trap
    
    **`d0-custom-objects`[Day 0](#day-0)MetadataCustom objects cannot be created over any REST surface.** Tooling `CustomObject` reports `createable: true` and rejects `FullName`. There is no `createCustomObject` action. `/headless/metadata` is not served. `deployRequest` needs multipart. Confirmed independently four times across this project. Metadata API via the `sf` CLI only.
    
-   Trap
    
    **`d0-custom-fields`[Day 0](#day-0)Tooling APICustom fields are fine over Tooling.** This is the standard workaround, and it shapes the architecture: where you would reach for a new object, put fields on an existing one. Five fields on `User` replaced a leaderboard object. `B2B_Mood_Board__c`, a stock SDO object, carries an entire 27-row message registry. A Tooling-created CustomField has **no FLS**, so SOQL and REST report _No such column_ while Apex reads and writes it perfectly. A seed can therefore succeed against a field you cannot query.
    
-   Trap
    
    **`d0-execanon-encoding`[Day 0](#day-0)Apex`+`, `++`, `&` and `#` in an `executeAnonymous` query string.** `+` decodes to a space; `&` truncates the body at that character and surfaces as `Missing closing quote character '` at an unrelated column. Use `String.join` and a `List` as a counter. Build the characters you need with `String.fromCharArray(new List<Integer>{38})`. This was the single most frequently recurring trap in the project.
    
-   Trap
    
    **`d0-exception-rollback`[Day 0](#day-0)ApexThrowing an exception to read a value out of `executeAnonymous` rolls back every DML in that transaction.** It once reverted 2,983 updates and silently no-opped a 1,165-row seed. Read-only probes only. To prove a write: run it plain, confirm `success: true`, verify with a separate query.
    
-   Trap
    
    **`d0-reserved-words`[Day 0](#day-0)Apex`where`, `group` and `on` are reserved words in Apex.** `on` is the nastiest: `for (String on : map.keySet())` fails with `Unexpected token 'on'` and then cascades a dozen unrelated errors below it — `Extra ')'`, `Method does not exist: void get() from the type Map<String,Integer>` — none of which are real.
    
-   Trap
    
    **`d0-case-insensitive`[Day 0](#day-0)ApexApex identifiers are case-insensitive.** A parameter `src` silently shadowed the constant `SRC` and mis-grouped every seeded row.
    
-   Trap
    
    **`d0-last-30-days`[Day 0](#day-0)Engagement dataEvery out-of-the-box marketing tile defaults to Last 30 Days.** Anchor every generator to `Date.today()`, never a literal date. Hit three separate times; stale data is indistinguishable from a broken pipeline.
    
-   Trap
    
    **`d1-dlo-frozen`[Day 1](#day-1)Data CloudCorrected: DLO schema is not frozen at stream creation.** This was published as a trap and it is wrong. `PATCH /ssot/data-streams/{name}` with an extended `sourceFields` list adds a column to a live DLO, `PROCESSING` for about 60 seconds, then `ACTIVE`. The dead end that produced the myth was `DELETE …?shouldDeleteDataLakeObject=true` returning 412 while identity resolution holds the DLO. Designing the full field set up front is still cheaper, but it is no longer mandatory.
    
-   Trap
    
    **`d1-automap`[Day 1](#day-1)Data Cloudauto-map-dlos omits `ParentId → ssot__PartyId__c` on contact points.** 887 email and 836 phone rows land with a null party, and the match rule needs that link. The mapping call 500s while the DLO is still PROCESSING — retry after 60 seconds. Einstein auto-map also omits the primary key and saves happily. Fix: assert `COUNT(ssot__PartyId__c) = COUNT(*)` after auto-map, then fix with the Aura deploy action and a FULL refresh.
    
-   Trap
    
    **`d1-dmo-union`[Day 1](#day-1)Data CloudDMOs union rows, they do not join columns.** Mapping a second DLO into an existing DMO doubles its row count and breaks every segment built on it. Fix: map into the same standard DMO and let identity resolution collapse them, or build a related DMO.
    
-   Trap
    
    **`d1-double-mangling`[Day 1](#day-1)Data CloudField name double-mangling.** `X__c` in CRM becomes `X_c` in the DLO becomes `X_c__c` in SQL.
    
-   Trap
    
    **`d1-null-empty-string`[Day 1](#day-1)Data CloudCRM nulls arrive as empty strings.** `IS NOT NULL` is therefore always true — a care-gap measure returned 2,989 instead of 482. Always test `<> ''`.
    
-   Trap
    
    **`d1-incremental-refresh`[Day 1](#day-1)Data CloudIncremental refresh appends modified rows as new versions.** 9,000 duplicates from one refresh. A mapping change needs a Full refresh; new mappings never backfill. Corrected: a data-stream refresh endpoint does exist, `POST /ssot/data-streams/{name}/actions/run` with `{"refreshType":"FULL"}`.
    
-   Trap
    
    **`d1-source-profile-filter`[Day 1](#day-1)Data CloudIdentity resolution's Source Profile Filter is inverted relative to its own UI text** and silently drops NULL-valued profiles. A filter keying on a field one source did not populate admitted 146 physicians, and a reported _62% consolidation rate_ represented zero actual merging. Corrected: these filters are not UI-only, `PATCH /ssot/identity-resolutions/{id}` writes them. Fix: OR'd filter groups keyed on `Data Source Object`, which every source populates.
    
-   Trap
    
    **`d1-unified-grain`[Day 1](#day-1)Data CloudReconcile at unified grain, never row grain.** 640 payer rows resolve to 619 unified individuals; row-grain counts overstate by up to 3.
    
-   Trap
    
    **`d1-graph-join-key`[Day 1](#day-1)Data CloudA data graph node's join key must also be a projected field on that node.** The build returns `status: ERROR` with no message. Fix: GET the graph and assert `path[0].fieldName` is present on every non-root node before waiting on a build.
    
-   Trap
    
    **`d1-stream-category`[Day 1](#day-1)Data CloudCategory cannot be changed after stream creation.** Fix: decide it before you create the stream, because a data graph root must be Unified Individual.
    
-   Trap
    
    **`d1-run-ruleset`[Day 1](#day-1)Data CloudThere is no REST endpoint to run an identity resolution ruleset.** `/actions/run` and `/jobs` both 404. Fix: the Run Ruleset button, found via `GET /ui-api/actions/record/{id}` as `IdentityResolutionRunNowAction`, or the Aura `IdentityResolutionController/ACTION$runNow`.
    
-   Trap
    
    **`d1-identity-one-dmo`[Day 1](#day-1)Data CloudIdentity resolution operates on one DMO.** "Add my object to identity resolution" always means mapping its DLO into `ssot__Individual__dlm` plus `ssot__ContactPointEmail__dlm`, not creating a parallel custom Profile DMO.
    
-   Trap
    
    **`d1-no-mapping-api`[Day 1](#day-1)Data CloudThere is no served DLO to DMO mapping API.** The only route is the Aura `DataModelingController/ACTION$deploy` action, which needs a live session.
    
-   Trap
    
    **`d1-ruleset-label`[Day 1](#day-1)Data CloudRead the rule, do not trust its label.** The reference ruleset is called "Fuzzy Name and Normalized Email" and contains no fuzzy matching. It is an AND of three exact criteria. Email alone will not resolve: a synthetic identity needs an Individual row with first and last name AND a linked ContactPointEmail row.
    
-   Trap
    
    **`d1-fragment-dmo-empty`[Day 1](#day-1)Data CloudVerify data graph counts on the ID DMO, not the fragments.** `*_FRAGMENT__dlm` DMOs are empty, zero rows, on every graph checked. The `*_ID__dlm` object carries one row per node and record and answers everything. Its name carries a creation epoch you cannot derive, so find it with `GET /ssot/metadata?entityType=DataModelObject&entityCategory=Related` and match on the display name.
    
-   Trap
    
    **`d1-graph-cap`[Day 1](#day-1)Data CloudOrgs have a data graph cap, and a failed build still occupies a slot.** Delete the failed graph before retrying rather than creating a second.
    
-   Trap
    
    **`d1-graph-500`[Day 1](#day-1)Data CloudOn data graphs, `POST` creates on a 500 and `DELETE` deletes on a 500.** Read the state back rather than trusting the status code.
    
-   Trap
    
    **`d2-one-predicate`[Day 2](#day-2)SegmentsOne headless segment = one predicate.** Compound criteria save with SUCCESS and are silently ignored.
    
-   Trap
    
    **`d2-no-refresh`[Day 2](#day-2)Segments`NO_REFRESH` is the default and it is permanent.** Frozen membership behind a green status.
    
-   Trap
    
    **`d2-phantom-count`[Day 2](#day-2)SegmentsA segment can report `lastSegmentMemberCount` while `segmentMembershipTable` is null and have never materialised.** Reading the count alone injects phantom people. This project documented that trap and then committed it: `cld_2027 Win-Back Eligible` was catalogued as a real audience on the strength of a 784 count that was the entire population. A full sweep on 3 September found it is **four segments, not one** — `cld_2027 Win-Back Eligible` (784), `Members - No Upcoming Appointments` (479 of 779), and two `zz_` probes. All four report a count with `SegmentMembershipTable`, `PublishStatus` and `LastPublishedEndDateTime` all null. Check all three fields, never the count.
    
-   Trap
    
    **`d2-hidden-criteria`[Day 2](#day-2)SegmentsA segment can have working criteria that no API will show you.** `HLS - No Upcoming Appointments` has a materialised membership table and 366 real members, so it demonstrably has criteria — yet `IncludeCriteria` is null on the sObject and the segment is absent from `/ssot/segments`. Five segments in this org are in that state. A REST-only audit will report them as criteria-free, and they are not.
    
-   Trap
    
    **`d2-shared-membership-table`[Day 2](#day-2)SegmentsSegments write into a shared membership table** — four of them in this org, not one. `Delta_Type__c` carries `new` and `existing` for the same person, so any count not using `COUNT(DISTINCT Id__c)` is silently doubled.
    
-   Trap
    
    **`d2-no-developername`[Day 2](#day-2)Segments`MarketSegment` has no `DeveloperName` or `ApiName` column.** The API name lives on `MarketSegmentDefinition`, which neither the data API, the Tooling API nor Apex SOQL will query. Use `ConnectApi.CdpSegment.getSegmentsPaginated(batchSize, offset, orderBy)` — batch size first, and an offset past the end returns an empty list rather than an error.
    
-   Trap
    
    **`d2-no-headless-delete`[Day 2](#day-2)SegmentsSegments cannot be deleted headlessly,** and not at all once an Activation references them. Never create probe segments.
    
-   Trap
    
    **`d2-exclude-criteria`[Day 2](#day-2)Segments`excludeCriteria` is not exposed by the REST read.** A REST-only audit shows a filtered segment as criteria-free.
    
-   Trap
    
    **`d2-consent-specifics`[Day 2](#day-2)ConsentConsent specifics.** `CommSubscriptionConsent.ContactPointValue` caps at 40 characters, so 216 of this org's email addresses cannot be keyed on it — join on PartyId instead. Consent value casing here is `OptIn`, not `OPT_IN`. Every create of a Custom Preference Page also sets it as that channel's default, displacing the existing one.
    
-   Trap
    
    **`d3-modulo-rate`[Day 3](#day-3)Engagement dataModulo rate allocation only hits its target at multiples of 100.** At n=66, a 38% target produced 58%. Use Bresenham.
    
-   Trap
    
    **`d3-hashcode`[Day 3](#day-3)Apex`String.hashCode()` changes by exactly 1 when the last character does.** `id + '#' + salt` yields correlated draws and a bimodal distribution. Salt first, then mix, in Long space.
    
-   Trap
    
    **`d3-unordered-soql`[Day 3](#day-3)ApexUnordered SOQL breaks first-wins de-duplication.** Generator and verifier picked different winners and produced 13 phantom mismatches. `ORDER BY Id` in both.
    
-   Trap
    
    **`d3-tile-vocabulary`[Day 3](#day-3)Engagement dataMC Next dashboard tiles need exact vocabulary:** `SENT` / `NOT_SENT`, `HARD_BOUNCE` / `SOFT_BOUNCE`. Not _uppercase everything_ — `Bounced` must become `NOT_SENT`. A missing `SEND` zeroes six tiles at once, and the diagnostic signature is unmistakable: CTR works while everything else reads zero.
    
-   Trap
    
    **`d3-column-names`[Day 3](#day-3)Engagement dataThe column names are not what you would guess.** The action column is `ssot__EngagementChannelActionId__c`, not `...ActionTypeId__c`. `ssot__UtmCampaignName__c` holds the _subject line_; the campaign id is in `ssot__UtmId__c`. The org field is literally misspelled `emailConversionGaolAchieved__c`.
    
-   Trap
    
    **`d3-contamination`[Day 3](#day-3)Engagement dataTwo contaminating sources must be excluded inside every measure expression, never as a dashboard filter.** `Marketing_Performance_Mock_Data` (19,529 rows, correct attribution, broken identity) and legacy web ingestion (20,033 rows, stale since 2026-02-18, zero UTM). Filter on `ssot__DataSourceId__c = 'Salesforce_Home'`.
    
-   Trap
    
    **`d4-api-version`[Day 4](#day-4)Tableau NextThe v65 / v66 / v67 split is a per-asset property, not a required API version.** Each dashboard and visualization carries its own `Version` column — in this org, five dashboards at 67 and one at 65; visualizations split 5 / 30 / 3 across 65, 66 and 67. Every one of them reads cleanly over the **v66.0** endpoint regardless of its own value. A re-test on 3 September 2026 could not reproduce a `DOWNGRADE_VERSION_ERROR` on any read at either version.  
      
    What does happen, and reads exactly like a permissions fault: `GET /analytics/dashboards/{0Tr…}` returns **400 `INSUFFICIENT_ACCESS` identically at v66.0 and v67.0**. That is the CRM Analytics route refusing a Tableau Next asset — wrong endpoint family, not wrong version and not wrong permissions. `/wave/dashboards/{id}` returns 400, `/connect/analytics/dashboards/{id}` and `/analytics/visualizations/{id}` return 404, at both versions. Read these objects over SOQL instead.  
      
    **Scope of this correction:** the retest covered reads only. The original claim may still hold on write paths, which were not re-exercised.
    
-   Trap
    
    **`d4-no-export`[Day 4](#day-4)Tableau NextMost of a Tableau Next dashboard cannot be exported.** Of 195 widgets across six dashboards, 78 have a retrievable definition and **117 do not**: all 81 text widgets return only an id and a parent id, so every heading and caption has to be retyped; all 33 filter widgets carry no field reference; and `AnalyticsDashboardViewDef` returns zero rows, so no per-widget placement survives — only the grid (72 columns, 12px rows, 1150px max width). **Semantic models have no read endpoint at all** — `AnalyticsSemanticModel` and `SemanticModel` are `INVALID_TYPE` on both APIs, and `/ssot/semantic-models` 404s. Only the semantic object names each visualization binds to survive. This is an export limit, not a build limit: the self-service authoring API writes visualizations, widget placement and global filters, so the dashboards are a spec-and-replay problem.
    
-   Trap
    
    **`d4-patch-replace`[Day 4](#day-4)Tableau Next`PATCH` is full replace** on both vizzes and dashboards. A partial PATCH once wiped a dashboard to a single widget. GET first, send the whole document. Widget `source` must be stripped to `{id, name}`.
    
-   Trap
    
    **`d4-reference-line`[Day 4](#day-4)Tableau NextA reference line needs both `visualSpecification.referenceLines[key]` and `style.referenceLines[key]`.** PATCH returns 200 with only the first and the chart renders _Can't show visualization_.
    
-   Trap
    
    **`d4-filter-sort`[Day 4](#day-4)Tableau NextA field carrying a filter cannot also carry a sort.** Use `style.allHeaders.fields.<key>.hiddenValues` so the sort survives. Mandatory for funnels.
    
-   Trap
    
    **`d4-aggregation-vocab`[Day 4](#day-4)Tableau NextAggregation vocabulary differs by API on the same platform:** `Avg` in visualizations, `Average` in metrics.
    
-   Trap
    
    **`d4-dashboard-filter`[Day 4](#day-4)Tableau NextA dashboard filter silently ignores any tile whose metric `additionalDimensions` lacks the field.** Empty `widgetIds` under `filterMode: include` means the widget answers to nothing. And a filter widget binds to exactly one semantic model — plan mixed-model dashboards rather than discovering them.
    
-   Trap
    
    **`d4-semantic-rename`[Day 4](#day-4)Tableau NextThe semantic layer renames colliding fields with a numeric suffix** — `Age` becomes `Age1`. Read the real names off the create response.
    
-   Trap
    
    **`d4-colour-rest`[Day 4](#day-4)Tableau NextPer-category colour mapping, gradients and the entire radial family are not available over REST.** Build in the UI, then place by `source: {id, name}`.
    
-   Trap
    
    **`d4-invalid-dashboard`[Day 4](#day-4)Metadata`analytics_embedding:dashboard` with `dashboardId = INVALID_DASHBOARD` makes a FlexiPage permanently unsaveable over the API.** Build a new page; never try to repair in place. Three pages in this org carry it.
    
-   Trap
    
    **`d5-root-css`[Day 5](#day-5)LWC`:root` is rejected in LWC scoped CSS** (`LWC1535`). Put design tokens on `:host` or the component's own root class; dark mode via a plain `@media (prefers-color-scheme: dark)` block. The `:root[data-theme="dark"]` pattern that works in a hosted page does not compile here.
    
-   Trap
    
    **`d5-fetch-401`[Day 5](#day-5)LWC`fetch('/services/data/...')` from an LWC returns 401.** A Lightning session cookie is not an API session, and no header or `credentials` option changes that. Before designing around a browser fetch, look for a `ConnectApi` Apex equivalent — many Connect resources have one, and it needs no callout, no named credential and no connected app.
    
-   Trap
    
    **`d5-wire-recordid`[Day 5](#day-5)LWCA wire parameter bound to an undefined `$recordId` never fires** and renders an empty state with no error. Bind to a field set in `connectedCallback`.
    
-   Trap
    
    **`d5-getter-cycle`[Day 5](#day-5)LWCA getter that reads selection state must not be read by the model that selection depends on.** Stamping a `rowClass` onto each row by comparing against `selectedKey` made the model depend on the selection while the selection resolved back through the model — an infinite getter cycle. Selection-dependent decoration belongs in per-view getters.
    
-   Trap
    
    **`d5-longtextarea`[Day 5](#day-5)ApexA LongTextArea cannot be filtered in SOQL at all,** not merely aggregated. `WHERE cld_Preview_Body__c != null` returns `INVALID_FIELD ... can not be filtered in a query call`. Select it and test in memory.
    
-   Trap
    
    **`d5-flexipage-description`[Day 5](#day-5)Metadata`FlexiPage.Metadata.description` caps at 255** and fails with no field named in the error, so it reads as a body-shape problem rather than a length one. `itemInstances` require a unique `identifier` (undocumented). `CustomTab.Metadata.available` breaks the JSON parser — omit it.
    
-   Trap
    
    **`d5-canvas-webgl`[Day 5](#day-5)LWCCanvas 2D and WebGL do work in LWC under Lightning Web Security.** No iframe needed.
    
-   Trap
    
    **`d6-agent-type`[Day 6](#day-6)AgentforceWithout `agent_type: "AgentforceEmployeeAgent"` the compiler defaults to a service agent that never appears in the in-org picker.** `config.agent_type` is immutable after first publish, and publish is not reversible over the API — a wrong publish leaves an undeletable orphan. This org has one.
    
-   Trap
    
    **`d6-setupentityaccess`[Day 6](#day-6)AgentforcePicker visibility additionally requires a `SetupEntityAccess` row of type `BotDefinition`.** Active is necessary and not sufficient, and System Administrator is not exempt. `GenAiPlanner` is not a valid `SetupEntityType`. Confirmed by extraction: `SetupEntityAccess` returns exactly **two** rows across all four agents and all seven planners. The two picker-visible agents have one each. The two that are not visible fail _both_ tests at once — wrong immutable `agent_type` **and** no `SetupEntityAccess` row — so fixing either one alone would not have surfaced them.
    
-   Trap
    
    **`d6-publish-errors`[Day 6](#day-6)AgentforcePublish returns an `errors` array even on success.** Read `severity`, not emptiness.
    
-   Trap
    
    **`d6-numeric-outputs`[Day 6](#day-6)AgentforceNumeric Apex outputs need `type: object` plus `complex_data_type_name: lightning__numberType`.** Compile passes; publish fails.
    
-   Trap
    
    **`d6-skip-validate`[Day 6](#day-6)AgentforceSkip `/nextgen-authoring/.../validate`.** It reads the request body, not the saved asset. `publish` is the real validation.
    
-   Trap
    
    **`d6-flow-decision`[Day 6](#day-6)AgentforceA Decision element in a Flow cannot see profile attributes unless a Data Graph exposes them.**
    

Published changes

## Corrections

Everything here was verified. When something turns out to be wrong, it gets corrected in public with a date rather than quietly edited.

Corrections log

| Date | What changed | Why |
| --- | --- | --- |
| 4 September 2026 | `DLO schema is frozen at stream creation` was published as a trap | Wrong. `PATCH /ssot/data-streams/{name}` with an extended `sourceFields` list adds a column to a live DLO, `PROCESSING` for about 60 seconds, then `ACTIVE`. The dead end that produced the myth was `DELETE …?shouldDeleteDataLakeObject=true` returning 412 while identity resolution holds the DLO. Designing the full field set up front is still cheaper, but it is no longer mandatory |
| 4 September 2026 | Source profile filters are UI-only | Wrong. `PATCH /ssot/identity-resolutions/{id}` writes them |
| 4 September 2026 | There is no data-stream refresh endpoint | Wrong. `POST /ssot/data-streams/{name}/actions/run` with `{"refreshType":"FULL"}` |
| 4 September 2026 | Tableau Next dashboards were described as unrebuildable and requiring manual redrawing | Wrong. The authoring API exposes full widget creation with placement and filter field references, plus a promotion-package export. Reading an existing dashboard back is still not possible; building one always was |
| 4 September 2026 | `Fake_Report_Data_Object__c` field count corrected from 7, then 31, to 36 | The first number was the count granted by one permission set, not the object's shape |
| 3 September 2026 | An API-version trap was qualified and scoped to read operations only | It could not be reproduced on a second attempt |

Section 8

## Downloads

The build

### The asset pack

Everything behind the screenshots on this page. Apex, components, page placement, data and configuration.

553 files · 1.04 MB

Full file list: [/manifests/asset-pack.txt](/manifests/asset-pack.txt)

[Download the asset pack](/rebuild-path-asset-pack.zip)

Data only

### Makana mock data

The thirteen datasets on their own, if you have an org and just need something in it. Scrubbed and synthetic.

13 datasets · 467 KB

[Download the mock data](/makana-mock-data.zip)

Claude skills

### Salesforce SE skills

Five skills for driving Salesforce over its APIs — headless discovery, campaign build, web affinity, Tableau Next, and SE triage.

5 skills · 36 KB

[Download the skills](/salesforce-skills-pack.zip)

### The thirteen datasets

All thirteen datasets in the mock data pack

| File | Rows | What it is |
| --- | --- | --- |
| `appointments.csv` | 2,989 | Patient appointment history, the engagement timeline on the unified profile. |
| `campaign-members.csv` | 1,189 | Campaign membership: which contact is in which campaign, with status. |
| `campaigns.csv` | 10  | Campaign records, including parent and child rows for the multi-content campaigns. |
| `cms-content.json` | 25  | Salesforce CMS content pieces captured from the Marketing Cloud workspace. |
| `consent/comm-subscription-channel-type.csv` | 11  | Subscription and channel pairings, the join between subscription and engagement channel. |
| `consent/comm-subscription-consent.csv` | 5,828 | The consent grants themselves, one row per party, subscription and channel. |
| `consent/comm-subscription.csv` | 7   | The subscription definitions consent rows point at. |
| `consent/contact-point-type-consent.csv` | 1,723 | Per contact-point consent, email and phone, separate from subscription consent. |
| `consent/engagement-channel-type.csv` | 2   | The engagement channel lookup: email and SMS. |
| `contacts.csv` | 890 | Patient-facing contacts. The spine every other dataset joins to. |
| `message-registry.csv` | 27  | Marketing Cloud Next message registry rows, one per campaign and channel. |
| `patients.csv` | 640 | Clinical patient records on Patients\_\_c, keyed to contacts by email. |
| `payer-enrichment.csv` | 640 | Payer and plan enrichment on Individual\_Enrichment\_Payer\_\_c. |

The five skills, in full

The datasets: schema and first 20 rows

Every file scanned for credentials before packaging. Zero found. All data synthetic. MIT-licensed, provided as-is. Verified 3 September 2026.

### What is in the asset pack

Asset pack folder layout

| Folder | Contents | Note |
| --- | --- | --- |
| `00-dependencies/` | `HLSMetadataSoap`, `HLSSiteGeocoder` | Deploy first. `cld_ObjectFactory` will not compile without the first |
| `01-capability-layer/` | 3 classes | Resolve object and field names at runtime so components degrade instead of throwing |
| `02-controllers/` | 17 classes | Three of them hard-fail rather than degrade — see below |
| `03-seed-generators/` | 11 classes | Deterministic. Run last: they write on top of records that must already exist |
| `04-lwc/` | 18 bundles, 72 files | Every screen in the screenshots on this page |
| `05-placement/` | 15 FlexiPages, 11 tabs, 3 permission sets, 4 agent actions | Deploy tabs before permission sets |
| `06-data/` | 13 datasets, ~4.8 MB | 890 contacts, 640 payer, 640 patient, 2,989 appointments, 5,828 consent rows, 25 CMS pieces |
| `07-config/` | 91 files | Segments, dashboards, agents, Data Cloud. Reference material to read and adapt, not artifacts to import |

Broken

### Deploy the included schema first

The schema is now included as `sf` source metadata under `08-schema/`. Custom objects still cannot be created over any REST surface, so this folder is the only entry point. Deploy it before anything else.

`Individual_Enrichment_Payer__c` (plus `cld_Audience__c`), `Patients__c`, `Patient_Appointment__c`, `B2B_Mood_Board__c`, `Contact.HLS_Photo_URL__c`, and `Fake_Report_Data_Object__c` with **36** fields.

Two of those corrections matter. `Fake_Report_Data_Object__c` has been miscounted three times in this document's history. It was published as **seven** – the count granted by one permission set, not the object's shape. Then as **31**, from reading the generators. A direct describe of the object on 4 September 2026 returns **36**: thirty `SEE_*__c` fields, `Group__c`, `Secondary_Grouping__c`, and four stock report fields (`Label__c`, `Value__c`, `Amount__c`, `Percent__c`). Counting a thing by reading the code that writes to it is not the same as counting the thing. And `B2B_Mood_Board__c` was omitted from every prerequisite list because the capability layer was supposed to have replaced it – `cld_ObjectFactory` even says so in a comment – but `cld_SixEngagementSeed` still instantiates it in static Apex and was never migrated.

Broken

### Three classes hard-fail at deploy

`cld_CampaignMembersController` (L66–67), `cld_PopulationHealthController` (L84) and `SEEAppointmentSeed` use static SOQL against org-specific custom fields, so they fail at deploy rather than degrading. That contradicts the capability-layer pattern the rest of the pack follows — `cld_OrgCapability` even probes for one of those exact fields to report a graceful degradation that never happens. Create the fields first, or delete those three files. Notably `HLSProfileController` reads the same objects _through_ `cld_OrgCapability` and degrades correctly, so the pattern works; it just was not applied consistently.

Note

### Known-wrong things you are inheriting

Shipped as-is so nobody thinks they broke it. All are documented in `SETUP.md`.

-   **Six orphaned components** are included for completeness. No FlexiPage references them.
-   **`cld_AbTestConsoleController` hardcodes one segment id** for every campaign's _open segment_ link, pointing at the Win-Back audience. Wrong for six of seven campaign families.
-   **`SEESpinePopulate` infers gender from a 176-name allowlist and defaults unmatched names to `Male`.** Its junk-sentinel list also contains `smith`, `davis` and `green`, so anyone with one of those as a _first_ name is silently marked Unknown. Synthetic data only.
-   **Three Tank pages and their three tabs reference components that are not included.** Those pages fail their own save, which is the compile check, and take their tabs with them. Delete both unless you are rebuilding that feature.
-   **`CldWebVisitApi` is deliberately excluded**, so `Cld_Web_Visit_Guest_Api` cannot be created. That permission set grants **edit on `Contact.Email` and `Contact.Description` to a guest-user profile**. Think hard before rebuilding it.

Broken

### The dashboards have no read path, but they do have a write path

Reading an existing Tableau Next dashboard back is genuinely not possible. The authoring API’s own guidance says it plainly: **no sibling GET endpoints exist to verify state.** Every write returns a success and an identifier, and nothing lets you introspect what is already there. That is why extraction from the source org failed.

Building them is a different matter, and this page previously said otherwise. The write path is complete: four `create*Viz` operations against a Semantic Model, `addWidgetToDashboard` with exact `placement` coordinates, `addGlobalFilterToDashboard` carrying a real field reference, and `addDashboardPage` for multi-page layouts. `getPromotionPackageUrl` returns a metadata migration package for moving a dashboard between orgs.

So the visuals are a spec-and-replay problem, not a redrawing problem. What you cannot do is derive the spec from a finished dashboard by reading it.

### Before you deploy

1.  Read `SETUP.md`. It carries a **Replace before you deploy** table: 71 hardcoded ids across the Apex, roughly 813 more across `07-config/`, the org id in 33 config files, and the tenant-suffixed Data Cloud table `ssotIndividualHls1_Unified_SMH_1768110__dlm` whose numeric suffix is unique to this tenant and appears in four controllers.
2.  Deploy in folder order: `00` → `01` → `02` → `03`.
3.  Run the seeds against synthetic data only. `SEESpinePopulate` infers gender from a name allowlist and is unsuitable for anything real.
4.  Load the data before running the seeds. `06-data/README.md` gives the order. Every `Id` column is org-specific and must be re-keyed on import; `contacts.IndividualId` is the key the consent files join on, and an export that omits it produces 5,828 consent rows that join to nothing.

### The mock data, on its own

890 contacts, 640 payer enrichment rows, 640 patient rows, 2,989 appointments, 1,189 campaign members, 5,828 consent rows and 25 CMS content pieces. Every email address is on the reserved `example.com` domain and every phone number sits in the NANP reserved fictional range – 8,198 addresses and 4,591 numbers, all replaced before packaging.

Twenty documented joins resolve at their original rates. Two of them previously resolved at **zero**: consent hangs off `Individual.PartyId`, and the first export omitted `Contact.IndividualId`, so 5,828 consent rows joined to nothing. That one column is the difference between a usable consent model and a pile of orphans.

Every `Id` column is org-specific and must be re-keyed on import. `06-data/README.md` gives the load order.

### The skills

Five Claude skills, written while building this and used to build it. `sfdc-headless-discovery` is the most portable – the enumerator rule, the three oracles, and a table of Salesforce errors that do not mean what they say. `tableau-next-dashboard-craft` turns a day of _Can’t show visualization_ into ten minutes. `sfdc-web-affinity` covers anonymous-to-known web tracking including a guest-user `with sharing` bug worth knowing about. `sfdc-mcnext-campaign-build` is the densest of the set and only useful if you build Marketing Cloud Next demo orgs. `salesforce-se-os` is a triage router for SE work.

A sixth was withheld. It depends on unpublished documents, and without them an agent would improvise the missing rubric and present the result as authored – which is the opposite of what this document is for. `PROVENANCE.md` in the pack says what was reviewed, what was changed before publication and why, and what each skill needs to run.

### The schema

The pack now carries the schema as deployable metadata. 306 files – five object definitions and 279 field definitions in `sf` source format, under `08-schema/`.

```
sf project deploy start --source-dir 08-schema/force-app
```

That replaces the paragraph above telling you to build six objects by hand. Deploy it first, before anything else in the pack.

**Then assign the included `cld_AssetPack_Schema_Access` permission set.** A metadata deploy grants no field-level security, so without it `cld_OrgCapability.hasField()` reports every field missing even though they deployed correctly – the capability layer does exactly what it should and degrades the whole pack to empty.

Eighteen fields are quarantined in `08-schema/quarantine/`, all for one reason: their parent object is `Visit`, an Industries standard object that does not exist in a plain Enterprise Edition org. They are correct metadata and deploy as a second command in an org that has it.

Honest caveat: this tree was validated structurally – well-formedness, Metadata API element ordering, per-type required elements, length and precision bounds, lookup reachability – and **never actually deployed**. The extraction was read-only.

Broken

### The blocker moved, and it is not the schema any more

With the schema shipped, four Apex classes are now the thing that stops a clean deploy into a plain org. Each uses **static SOQL** against an object that may not exist, and static SOQL against a missing object is a compile error, not a runtime one:

-   `cld_AudienceApplyController` and `cld_DataCloudSendHistory` – `MarketSegment`, `CommSubscriptionChannelType`, `DataCommunicationCap`, `DataCommCapDim`. These need Data Cloud.
-   `HLSCareFinder` and `SEEEncounterSeed` – `Location` and `Visit`. These need Field Service or Scheduler, and Industries.

This is the same defect as the three classes named earlier in this section, and it has the same fix: the capability layer exists precisely so this class of dependency degrades instead of failing, and these are the classes that bypass it. Either exclude the four files from the Apex deploy, or convert their static SOQL to `cld_OrgCapability.safeQuery` the way the other thirty-odd classes already do.

Worth stating plainly, because it is the honest shape of this pack: **the schema was never the hard part.** Removing it just revealed which classes were quietly depending on an org that already had everything.

Claudforce — HLS — Provider + Payer · Verified 3 September 2026 against org trailsignup-a612cacbdcac92 · Liam Sheridan, Salesforce Solutions Engineer