---
name: "caldova-demo-setup"
description: "Stand up a CDX Caldova demo tenant end to end, reskinned to your own industry. Opens with a guided start: detects whether you already have a tenant, then interviews you once about your industry, territory, named accounts, which workloads to install and how deep the data should be - and runs unattended from there. Covers claiming the tenant on CDX, the MCAPS BYOT Azure subscription, creating the Dataverse environment, installing the CE app portfolio, wiring connections and connection references, "
---

## Purpose

You claimed a **CDX Caldova tenant** and it arrives with licences but **no Dynamics 365 apps
installed**. This skill takes it from empty to demo-ready.

It is written from a real end-to-end build (Aug 2026). Everything here was executed and
verified, including the things that **did not work** — those are called out explicitly so you
do not repeat the same dead ends.

**Caldova** is Microsoft's fictional **pharmaceutical / life-sciences** enterprise. The packaged
demo narrative is supplier sourcing, procurement, RFP management, supplier risk and contract
manufacturing (partner org in the scenarios: **Allerveo**). You can keep that narrative or
reskin it to your own industry — see `reference/04-demo-data-reskin.md`.

---

## 🚀 START HERE — run the guided start

**When a user invokes this skill, do not start executing and do not dump a questionnaire.** Open
**`reference/00-guided-start.md`** and follow it. It is the choreography for the first ten minutes:

1. **Detect before you ask.** `pac auth list`, `pac env list`, a record-count sweep, and a check for
   an existing `~/.caldova-demo-profile.json`. Then *tell the user what you found* and branch:

   | Found | Branch |
   |---|---|
   | No auth profile, no tenant | **A** — claim a tenant (`00-claim-tenant.md`) |
   | Auth profile, empty org | **B** — fresh build → intake |
   | Org already has records | **C** — resume; summarise, then ask what is next |
   | Decision record exists | **D** — reuse it; confirm, do not re-interview |

2. **Interview in six themed batches**, not one wall of thirteen questions — who you sell to,
   workloads, data depth, the gated decisions, operating preferences, sign-ins.
   Content and rationale for each question live in `reference/00-operator-profile.md`.

3. **Confirm the whole plan once**, then run unattended.

4. **Write `~/.caldova-demo-profile.json` before the first build step**, so a crash or a new session
   never re-interviews the user. **Never write a password into it.**

> **No tenant yet?** Branch A covers claiming plus the MCAPS BYOT Azure subscription request. Those
> two are **sequential, not parallel** — the tenant must exist before the request has anything to
> attach to. And the launcher is **not a progress bar**: it can spin for **15–30 minutes after the
> tenant already exists**. Verify in **My Dashboard**. Run the intake while it provisions.

---

## Read this before you touch anything

### First: confirm which identity you are operating as

**Ask the operator for the Caldova admin UPN and sign in as that account before running anything.**

| Identity | Example | Use for |
|---|---|---|
| Their corporate account | `{{USER_EMAIL}}` | The CDX portal only |
| **Caldova tenant admin** | `admin@caldova<NNNNNNNN>.onmicrosoft.com` | **Everything else in this playbook** |

Get the UPN from **CDX → My Dashboard → the tenant row → Credentials**. Then:

```powershell
pac auth create --name caldova   # interactive browser sign-in as the Caldova admin
pac auth list                    # confirm the * is on the caldova profile
```

…and sign the browser in to the M365 admin portal and PPAC as that same account. Later, once the
BYOT subscription lands, `az login --tenant <caldova-tenant-id>` too.

> 🔒 **Ask for the UPN, never the password.** All sign-ins are interactive browser prompts — the
> operator types their password into the Microsoft sign-in page. Do not request, store, echo or
> write a tenant password anywhere.

**Why this is first:** wrong-identity failures do not announce themselves. `pac` returns an empty
environment list (looks like nothing was created), portals load with nothing in them, and file
uploads land in the wrong OneDrive. **If something is inexplicably empty, check identity before
debugging anything else.** Full detail in `reference/00-claim-tenant.md` § Step 2.5.

### Phase 0.5 is mandatory: one intake, then run unattended

**This skill is exportable. Whoever is running it is probably not the person who wrote it.**

**Design contract: ask everything at the start, then work alone.** A full build contains four
multi-hour waits and dozens of small decisions. Asking each one when you reach it pins the operator
to the session for a day. `reference/00-operator-profile.md` collects **all thirteen** decisions in
a single exchange — industry and accounts, which workloads, data depth, Sales Premium
authorisation, live-send policy, journey shape, recovery policy, notifications, and the batched
sign-ins.

Every worked example in this skill (the CPG brand families, the retail/grocery archetypes, the
`gen-ci-data.py` product list) comes from **one** seller's territory. They are illustrations, not
defaults. Inheriting them silently produces a demo that resonates with nobody.

State the privacy rule out loud: **recommend fictional company names**, because a demo tenant
carrying real account names shows Customer B's pipeline to Customer A the moment it is demoed.

#### After intake, only three things justify interrupting the operator

1. **An interactive password prompt** — cannot be automated. Batch every sign-in at intake so this
   happens once, at a time of their choosing, rather than four hours in.
2. **A destructive or commercial action not pre-authorised** — deleting an environment, sending
   real email, accepting licensing terms.
3. **A genuine dead end**, after diagnosis, with every unblocked task already finished.

Everything else has a rule instead of a question:

| Situation | Do this, don't ask |
|---|---|
| Long wait | Switch to independent work — seed data while CI installs, configure Service while F&O provisions |
| Failure | Retry once, apply the intake recovery policy, else record it and **continue with everything not blocked by it** |
| Masked error (`{1}{0}`) | Diagnose with a raw `fetch` first — most of these were solved without the operator |
| Ambiguous choice | Take the reversible option and note it (non-semantic over semantic, Draft over Live) |

**If you catch yourself about to ask a mid-build question, it belongs in the intake.** Add it to
`reference/00-operator-profile.md` so the next operator is never asked it at that point again.

### The single most important lesson: progress indicators lie

This recurs at **every layer** of the build. Tooling and portals report state confidently and
wrongly, in both directions — claiming success that has not happened, and claiming
in-progress long after work has finished.

**Five separate `pac` commands returned confidently wrong answers:**

| Command | What it claimed | Reality |
|---|---|---|
| `pac application list` | 33 apps in BOTH Installed and NotInstalled | Broken filter |
| `pac solution list` | 3 solutions | Full Sales + CS + Field Service + Project Ops deployed |
| `pac copilot list` | All 43 agents "Published / Provisioned" | 37 were **Draft** |
| `pac admin list` | Environment Type = Sandbox | Already converted to **Production** |
| `pac admin list` | **1 environment in the tenant** | **3** — it silently omits every environment without a Dataverse DB, including F&O envs mid-provision |

**And three UI surfaces do the same:**

| Surface | What it showed | Reality |
|---|---|---|
| **CDX tenant launcher** | Still working, 15–30 min | **Tenant already created** — visible in My Dashboard |
| CI provisioning portal | `Installing` | App module already existed in the org |
| PPAC direct URLs (licensing/apps/dataflows) | 404 / error page | Pages exist; reachable via the tab strip |
| **`bots` table** | All 45 agents `statecode=0 status=1` | Says nothing about publish state — check Copilot Studio |
| **`msdyn_aiconfiguration`** | 50 rows, looks configured | They are **Copilot prompts**, not predictive scoring models |
| **F&O `Create — Succeeded`** | Environment deployed fine | **Demo data may be entirely absent** — verify by legal entity |
| **CI admin provisioning grid** | `Not Installed` for CI – Data / Journeys | **Fully entitled** — needed only a self-service region pick at `home.ci.ai.dynamics.com` |
| **Per-user licence check** | No Customer Insights licence on the user | **CI is an ORG-level service plan** — it can *never* appear per user, on any tenant |
| **Billing → Your products** | **1 product** in the tenant | **17 licences** under Billing → **Licenses**, incl. the multi-app demo SKU |
| **Predictive scoring form** | Valid, **Get started** enabled | Default **BPF filter** matched **0 of 421 leads** — API-seeded records have no BPF instance. Set BPF = `None`. |
| **CI activity relationship dialog** | **Apply** enabled on an email→`CustomerId` join | Joins against the target's **primary key**; produces **zero** matches |
| **CI – Journeys email** | Compliance profile bound, real address present | `msdynmkt_placeholders` tokens **declared but unbound** — `Ready to send` silently does nothing |
| **Entitlement PATCH** | Succeeded on create, so edits look safe | Editable **only while Draft** — every PATCH after activation fails |
| **Journey analytics** | `Sent 40 · Delivered 40 · 0 failed`, but the demo folder was empty | Delivered fine — **Exchange Junk-filtered it**, and junk runs *before* inbox rules |
| **Email open rate 97.5%** | Outstanding engagement | **Defender scanning the tracking pixel** — 44 opens, all client "Unknown", mail never read |
| **CI prediction data picker** | "There are no available tables for the selected activity type" | Data is fine — the activity's **semantic type** was `Purchase`; OOB models only accept `SalesOrder` / `SalesOrderLine` / `Subscription` |
| **CI measure template picker** | Offers no monetary column, only `ActivityTime` / `Title` / `EventDate`… | Templates read the **projected `UnifiedActivity`**, not your source table. Unmapped semantic fields simply do not exist downstream |
| **Prediction review page** | `Customer transaction history: In progress`, `Run` greyed | Stale/misleading. The left-rail tab is the truth — it read **"Step is empty"** because the Add-data selection never persisted through `Save draft` |
| **F&O `Allowed MCP clients` grid** | Renders **empty** for ~10s, and virtualized cells read as `""` even when visible | 7 first-party clients were **already registered**. Verify via `/data/McpAllowedClients`, never the DOM |
| **F&O "company which does not exist"** | Account/permissions problem, or a broken environment | Usually **self-inflicted** — an unknown `?cmp=` value **persists to your session** and breaks every later page load. Recover with `?cmp=DAT` |
| **CI segment "Estimated segment size"** | The member count you'll get | An estimate — previewed **79**, resolved to **50** |
| **CI segment-suggestions `Run` greyed, no error** | Config incomplete somewhere unknown | The Activity step's **"Confirm the applicable activity source table"** checkbox only renders *after* you pick the type; skip it and the review page shows a blank source table |
| **CI differentiator attribute picker** | Every listed attribute is usable | **Date measures are offered but rejected on Save** — `'Lastpurchasedate' ... isn't allowed` |
| **CI differentiator "No item found. Could not find any segments."** | No segments exist | They do — the other one was still **`Refreshing`**. Needs 2 segments with *completed* refreshes |
| **F&O account structure `Draft` after Activate** | Activation failed | It's a **batch job**; it stays `Draft` until the batch service runs it — and that service can be idle on a fresh environment |
| **A reskinned tenant** | The rebrand is complete | The reskin covers **records**, not **config** — queues, case subjects and price lists keep their Contoso names and are visible in every dropdown |
| **A timed-out OData batch** | The work failed, re-run it | **The browser kept going.** Quotes read 38 at timeout and 56 minutes later. Re-query counts and resume by diff, or you will duplicate |
| **Sales agent stuck on `Draft` after Start** | The start silently failed — retry it | There is a **second `Start agent` confirmation prompt below the fold**. Two identically-named buttons; click the **last** one. (This corrects an earlier, wrong note in the skill) |
| **F&O MCP server reachable** | Transport is healthy, so the agent "works" | Says nothing about data — Caldova's ERP has **1 legal entity and 0 customers** |
| **118 knowledge articles in the org** | The knowledge base is well stocked | Only **6 were Published**. 41 good articles sat in **Draft** — invisible to knowledge search and agent assist. Filter on `statecode eq 3 and islatestversion eq true`, never on row count |
| **F&O "nothing can post"** | The batch service is idle/stuck | ❌ **Wrong diagnosis.** `FiscalPeriods` = **0** — the fiscal *years* exist as headers with no periods inside, because creating a year over OData never generates them. Fix on the **`mi=FiscalCalendars`** form, not in batch |
| **F&O required-field error on insert** | Fill those fields and it will work | If the retry returns a **`TargetInvocationException`**, the entity refuses standalone inserts entirely. Stop guessing payloads — go to the form |
| **A rejected Dataverse POST naming only the entity** | The whole payload shape is wrong | Often **one** field is poisoning it — `description` alone breaks `contracts`. **Bisect the payload**; the message will not name the field |
| **Repeating Windows sign-in prompt for the demo tenant** | Your session expired — sign in again | Signing in never fixes it. The **Dataverse CLI has no auth profile for that org**, so it falls back to your corporate WAM identity and hits `AADSTS90072`. `dsregcmd /listaccounts` shows **one** account, in corp. Filter the AAD log on your **demo tenant GUID** (not on `AADSTS`, which is full of unrelated noise) |
| **Cross-tenant Dataverse MCP** | Can't work — the demo tenant is foreign to your device | **It works fine with a profile.** A sibling server pointed at a different tenant worked because `dataverse auth list` had a matching row. **`auth create` first, register second** — the reverse order is what causes the prompts |
| **A Dataverse MCP server that is registered *and* enabled** | It is working | Verify at the **tool list**, not the settings screen. It can spawn, fail auth, exit, and still read as enabled. `m-diagnostics.jsonl` logs `failed to connect`; a row in `auth list` with a *still-failing* server means the token **expired** (re-run `auth create`), while **no row** means it never authenticated at all |
| **MCP server set to `enabled: false`** | It is off and cannot prompt any more | ⚠️ **It still spawns.** `enabled` gates *tool exposure*, not *process spawning* — the host relaunches it every session, it retries the cross-tenant token, and **the prompt keeps coming**. `m-diagnostics.jsonl` logs `MCP server "…" failed to connect` at the exact moment of the dialog. You must **delete the entry from `m-mcp-servers.json`** |
| **Omnichannel / Contact Center "provisioned"** | Config record + all `msdyn_oc*` tables + 14 workstreams + telephony solutions all present | Those ship with the **solutions**. The runtime is separate and the five **channel toggles ship OFF** (admin center → Channels → **Manage channels**). Tick **Chat** → **Save** and the runtime provisions in **~90s** |
| **Enabling a channel — the Save "fails"** | The toggle reverted on reload, so it did not save | ⚠️ **It did save.** The checkbox reverts while provisioning is **in flight**, with no error and no toast. Poll `CCaaS_GetContactCenterState()` — `State=2/Status=2001` → **`State=1/Status=2002`** — instead of re-toggling in a loop |
| **`CCaaS_GetContactCenterState()` returns 404** | The API is unsupported in this org | **Transient.** It 404s *during* provisioning, alongside `remote name could not be resolved: e-<orgid>…`. Both clear once the runtime lands |
| **A chat widget with a real app ID and embed snippet** | The widget is live | The snippet is generated **locally by a Dataverse plugin** and works fine against an unprovisioned runtime. It bootstraps, builds its iframe, then dies on `ERR_NAME_NOT_RESOLVED`. **The page just looks empty — only the console names the cause.** Working looks like a bubble saying *"We're online"* and **zero** console errors |
| **F&O `Allowed MCP clients` grid** | Renders **empty** for ~10s, and virtualized cells read as `""` even when visible | 7 first-party clients were already registered. Verify via `/data/McpAllowedClients`, never the DOM |
| **Close Agent picker "has no options"** | The field is broken or the data is missing | Its options are **`role="menuitemcheckbox"` inside `.fui-Combobox__listbox`**, not `role="option"` — and it is **search-as-you-type**, so nothing renders until you `fill()` a term |
| **Close Agent Products `having errors`** | Your product selection is wrong | The products are fine — it validates that the *fields* hold data. Click **`Add missing info`**; in Caldova the real gap was an empty **`Product page URL`** on every product |
| **Close Agent `Email delivery` = `is filled`** | Delivery is configured and understood | It self-satisfies with **zero input** — and there is **no autonomy switch**. A started Close Agent runs a **fixed five-touch outbound sequence** (2/3/4/5-day, plus 7-day) |
| **Close Agent target "Contact → Email → Contains data"** | A narrow, safe test set | It is **every open opportunity with a contact email**. Query the domains before starting — one real address makes it an outbound campaign |
| **Close Agent `Knowledge sources` = `is not filled`** | Optional. `Save` succeeds and returns **"Changes saved"** | ⚠️ **Optional for Save, FATAL for Start.** The aria text is the *loading* state — the section spins forever with no error and permanently greys `Start agent`. Ticking prerequisites, confirming Copilot Studio consent, uploading a file source, and publishing the bot all fail to clear it. **Screenshot the tab — the spinner is invisible in `innerText`** |

**Only trust these verification methods:**

| To check… | Look here |
|---|---|
| Tenant claimed / created | **CDX `My Dashboard` → Overview**, not the launcher window |
| Apps installed | `https://<org>.crm.dynamics.com/main.aspx?forceUCI=1&pagetype=apps` |
| Environment type, **and whether an environment exists at all** | Power Platform admin center environment hub |
| Agent publish state | Copilot Studio **Agents** list |
| **CI – Data / Journeys availability** | **`home.ci.ai.dynamics.com`** — and *complete the first-run region form*, which **is** the install |
| **CI licence entitlement** | `admin.cloud.microsoft/#/licenses` → the SKU → **Manage apps & services**. Never a per-user check; never "Your products" |
| CI – Data progress | The **Getting started** checklist on the CI home page |
| Agent publish state | Copilot Studio **Agents** list — never the `bots` table |
| Predictive scoring | **Sales Hub → App Settings**, and a record count by `statecode` |
| **CI prediction / measure readiness** | The activity's **semantic field map** (`Configure activities` → step 2 → *Map field types* = **Yes**). If `Sales amount` is unmapped, nothing downstream can see money |
| **A prediction wizard step's real state** | The **left-rail tab tooltip** (`Step is valid` / `Step is empty`) — not the Review page summary |
| F&O demo data | `/data/LegalEntities?$top=50` — expect **more than one row**; `dat` alone means empty. Also check `/data/MainAccounts?$top=1&$count=true` ≠ 0. **Don't look for `USMF`** — it ships only with the Contoso dataset |
| **F&O MCP server live?** | `GET /mcp` → **401 + `WWW-Authenticate`** (a **404** means not enabled), and `/.well-known/oauth-protected-resource` → **200** |
| **F&O allowed MCP clients** | `GET /data/McpAllowedClients?$top=20` — check before adding, Microsoft seeds 7 first-party entries |
| **F&O MCP server live?** | `GET /mcp` should return **401 + WWW-Authenticate** (404 = not enabled), and `/.well-known/oauth-protected-resource` should return **200** |
| Anything in Dataverse | `Xrm.WebApi` from a browser console (see below) |

The general rule for this whole playbook: **a spinner is not evidence. Go look at the management
surface.**

**Corollary — a form is not a spinner.** If a portal is *asking you something*, answering it is the
work, not a delay to wait out. This skill once told an operator CI – Data was not installed when it
only needed a region selected. **Never report a product unavailable until you have opened its own
portal and completed whatever it asks for.**

### Never call an install failed before 60 minutes

Observed landing times: Sales Premium ~5.5 min, CS Demo Data ~20 min, Project Operations
~35 min, **Field Service ~50+ min**. `pac application install` frequently never returns even
after the app has fully deployed, and hard-errors at exactly 60 minutes. I wrongly killed
Field Service at 48 minutes; it had actually landed.

### The `Xrm.WebApi` technique — use this constantly

Any open model-driven app page exposes `Xrm` in the top frame. Direct Dataverse reads/writes
from there are dramatically more reliable than the admin UI widgets, which are hosted in
cross-origin island-cluster iframes that intermittently fail with `ERR_CONNECTION_CLOSED`.

```js
const base = Xrm.Utility.getGlobalContext().getClientUrl();
const r = await Xrm.WebApi.retrieveMultipleRecords('account','?$select=name&$top=50');
await Xrm.WebApi.updateRecord('account', id, { name: 'New Name' });
```

Use it for bulk edits, verification sweeps and anything the UI makes tedious.

> ⚠️ **Batch your loops at 50–60 records.** Every `Xrm.WebApi` call is a separate HTTP round trip,
> and creating a record in a terminal state costs **two** (create, then update `statecode`). A
> 300-record loop runs for minutes and **will hit the automation tool's execution timeout
> mid-loop**, leaving a partial write you may not notice. Verify the tally after every batch, and
> never schedule a whole class of record last — if that batch is the one that dies, the gap is
> invisible until something downstream fails.

---

## Phase order

Run these in order. Later phases depend on earlier ones.

| # | Phase | Reference | Rough time |
|---|---|---|---|
| **0** | **Claim the tenant, then request the Azure subscription** (serial) | **`reference/00-claim-tenant.md`** | **~15 min claim + procurement lead time** |
| **0.2** | **Get the admin UPN and sign in as the Caldova admin** | **`reference/00-claim-tenant.md` § 2.5** | **5 min, mandatory** |
| **0.5** | **Single intake — all 13 decisions + batched sign-ins** | **`reference/00-operator-profile.md`** | **10 min, mandatory, do it FIRST** |
| 1 | Create environment with D365 apps enabled | `reference/01-environment-provisioning.md` | 10 min + install time |
| 2 | Install the CE app portfolio | `reference/01-environment-provisioning.md` | 1–3 hrs mostly waiting |
| 3 | **Create connections + bind connection references** | `reference/02-connections-and-agents.md` | 20 min |
| 4 | Configure first-party agents | `reference/02-connections-and-agents.md` | 30 min |
| 5 | Convert to Production + branding | `reference/03-branding-and-polish.md` | 15 min |
| **5.5** | **First-party agents + predictive scoring (needs seeded data)** | **`reference/08-agents-and-sales-insights.md`** | **45 min + async training** |
| 6 | Reskin demo data to your industry | `reference/04-demo-data-reskin.md` | 30–45 min |
| 7 | Customer Insights (Journeys + Data) — optional | `reference/06-customer-insights.md` | **2-3 hrs per product** |
| 8 | Chapters 2 & 3 add-ons — optional, needs Azure sub | see "Chapters 2 & 3" below | procurement lead time + deploy |
| 9 | Finance & Operations apps — optional, separate env | `reference/07-finance-and-operations.md` | hours |
**Phase 0 is the longest pole in the tent, and it is serial.** Claim the tenant → wait for
`Completed` **in My Dashboard** (the launcher is unreliable and may spin 15–30 min after the tenant
exists) → **only then** request the NET NEW Azure subscription, which needs an existing tenant to
attach to and carries procurement lead time. Two back-to-back waits, neither startable early. Two
things there will bite you if you skip the file: **CDX tenant slots are capped** (a full `3 of 3`
blocks the claim silently), and **changing any tenant password permanently breaks the add-on
experiences**.

**Phase 0.5 costs ten minutes and needs no tenant**, which makes it the right way to spend a
Phase 0 wait — and running it early is what buys the operator an unattended build. Its answers feed
Phases 5.5, 6, 7 and 9, so collecting them late means asking questions those phases have already
stalled on.

**Ordering note:** although numbered 0.5, treat the intake as the *actual first action* whenever the
tenant already exists. Everything downstream reads from its decision record.

**Phase 3 is the one everyone misses.** A fresh environment has **zero connections** and every
connection reference unbound, which silently blocks every first-party agent. Their admin pages
just spin on "Loading" forever with no error. Do this before touching agents.

**Phase 7 is irreversible.** Customer Insights installs cannot be cancelled mid-cycle and may
consume production licensing. Read `reference/06-customer-insights.md` before starting it.

---

## Quick start

```powershell
# 1. Authenticate AS THE CALDOVA TENANT ADMIN (interactive browser sign-in)
#    Ask the operator for the admin UPN: admin@caldova<NNNNNNNN>.onmicrosoft.com
#    Get it from CDX > My Dashboard > tenant row > Credentials
pac auth create --name caldova

# 2. Confirm the caldova profile is the ACTIVE one (look for the *)
pac auth list

# 3. Confirm which app templates the tenant licences
pac admin list-app-templates
```

Step 2 is not optional. If a previous profile is still active, every subsequent command silently
targets the wrong tenant.

Then follow `reference/01-environment-provisioning.md`.

> **Do not** create the environment with `pac admin create --templates`. It does not reliably
> set the "Enable Dynamics 365 apps" flag, and that flag is **irreversible**. Use the PPAC
> wizard. Details in the reference file.

---

## Known-good outcome

A finished environment from this playbook has:

- **~18–22 published apps** (count varies with which optional packages you install) — Sales Hub,
  Sales Team Member, Copilot Service workspace, Copilot Service admin center, Field Service
  (+ Mobile, + Connected), Project, Project Operations (+ Team Member), Resource Scheduling,
  Channel Integration Framework, D365 App for Outlook, Agentic Simulations, Power Pages
  Management, Power Platform Environment Settings, Solution Health Hub
- **8 connections**, all Connected, with **all connection references bound**
- Customer Service agents configured (Case Management, Customer Intent, Knowledge)
- Environment Type = **Production** (no SANDBOX badge)
- Custom branded app header
- Demo data matching your industry

Run `scripts/verify-environment.js` to confirm all of the above in one call.

---

## Honest limitations

One thing this playbook could **not** solve:

1. **Publishing the first-party agents.** 38 of 44 Copilot Studio agents ship as Draft and
   would not publish. `PvaPublish` API → `409 Conflict / ExternalServiceException`,
   `pac copilot publish` → `OrganizationServiceFault`, Copilot Service admin center Publish →
   "Unable to publish Copilot Studio agents", and the Copilot Studio Publish button is
   **disabled** with agent status stuck on "Setting up…". Three independent paths failing
   identically = server-side provisioning state. Waiting 25+ minutes did not clear it.
   If you hit this, raise support referencing the `PvaPublish` 409.
   **Agents still function for demo purposes where the admin surface enables them directly**
   (Case Management, Customer Intent and Knowledge agents were all configured successfully).

That is the only unsolved item. **Chapters 2 & 3 are no longer a dead end** — see below.

---

## Chapters 2 & 3: the Azure subscription gate

> Full request procedure, states to wait for, and the tenant-scoping caveat live in
> `reference/00-claim-tenant.md` § Step 3. Summary below.

The Caldova CDX page (`https://cdx.transform.microsoft.com/caldova`) carries two **Add-On
experience cards**, both fronted by `* * * PRE-REQ: MUST ADD NEW AZURE SUB VIA MCAPS BYOT * * *`:

| Add-on | Solution area | Deploys |
|---|---|---|
| **Chapter 2: Build, Observe, and Tune** | Cloud and AI Platforms | Caldova's invoice-assurance solution, proof of concept → scaled and governed |
| **Chapter 3: Security + Agent 365 Scenarios** | AI Business Solutions, Security | XDR base infra, Entra Connect, MDE, Agent 365 |

This is **not** a permanent block — it is a procurement step:

1. Request a **NET NEW** Azure subscription via the MCAPS BYOT process → <https://aka.ms/MCAPSBYOT>.
   It must be net new; an existing subscription attached to another tenant will not do.
2. Wait for it to reach **Status `Completed`** / step `AzTsPreReqsCompleted` in the CDX tenant
   detail page, under **Subscriptions**. It appears there automatically once provisioned —
   you do not attach it by hand.
3. Only then do the **Get Add-On** buttons on the two cards actually work.

Verified Aug 2026: subscription `ME-Caldova<NNNNNNNN>-{{USER_ALIAS}}-1` landed on the CDX tenant page
with `AzTsPreReqsCompleted / Completed`, releasing both add-ons.

> **Do not change any tenant passwords.** The CDX page states in caps that passwords must remain
> exactly as issued or **the add-on experiences will not work**. This is easy to trip over if you
> have been hardening the tenant.

Note the add-on subscription lives in the **Caldova tenant**, not your corporate tenant, so
`az login --tenant cadd283e-...` as the Caldova admin is required before any `az` work against it.

### Supporting documentation on that page

- `Seller FAQ - Caldova Tenant`
- `STEP 1 - Caldova Demo Experience One-Time Setup Guide`
- `STEP 2 - Caldova Demo Experience Chapter Narrative`

---

## Reference files

| File | Contents |
|---|---|
| **`reference/00-guided-start.md`** | **🚀 START HERE — detect-before-you-ask, the four branches, six-batch interview, plan confirmation, decision record** |
| `reference/00-claim-tenant.md` | **Phase 0 — claiming the CDX experience, tenant slot limits, credentials, the MCAPS BYOT Azure subscription** |
| `reference/00-operator-profile.md` | **Phase 0.5 — intake *content*: the 13 decisions and why each matters, archetype starters by industry** |
| `reference/01-environment-provisioning.md` | Environment creation, solo-only apps, app install strategy, timings |
| `reference/02-connections-and-agents.md` | The connection-reference blocker, agent prerequisite chains, Entra app user |
| `reference/03-branding-and-polish.md` | Production conversion, modern theming, the undocumented master switch |
| `reference/04-demo-data-reskin.md` | Reskin methodology, entity checklist, the fields everyone forgets |
| `reference/05-troubleshooting.md` | Every failure hit during the build and how it was diagnosed |
| `reference/06-customer-insights.md` | CI Journeys + CI Data: install surface, CDP demo data, the **semantic activity map** that gates measures/predictions, measures, segments, suggestions, insights, churn + CLV models |
| `reference/07-finance-and-operations.md` | F&O: **ask Murray Fife for templates first**; licence checks, the Sandbox-only demo-data template, wizard traps, the **MCP server**, and the full **build-from-scratch-over-OData** recipe (legal entities → GL → posting profiles) |
| `reference/08-agents-and-sales-insights.md` | **First-party agent inventory, predictive lead/opportunity scoring, and how to seed a trainable corpus** |
| `reference/09-reference-build-state.md` | **Snapshot of one real completed build — what "done" looks like, and what was left outstanding** |
| `assets/caldova-demo-setup-overview.pptx` | **8-slide overview deck** — what the skill does, how to install it, how to use it. Hand this to someone instead of explaining it |
| `assets/build-overview-deck.js` | `pptxgenjs` generator for that deck — edit and re-run `node build-overview-deck.js` to restyle or re-word it |
| `scripts/bind-connection-references.js` | Bind all connection references to connections in one pass |
| `scripts/reskin-demo-data.js` | Template for bulk demo-data rewriting |
| `scripts/verify-environment.js` | Health check: apps, connections, refs, agents, data cleanliness |
| `scripts/gen-ci-data.py` | Generate an 11-source CPG CDP dataset with built-in identity-resolution difficulty |

JavaScript scripts are browser-console code. Paste into DevTools on any open model-driven app
page, or run through Playwright. The Python script runs standalone.

> ⚠️ `gen-ci-data.py` ships with a **CPG** product portfolio and company list. Those are worked
> examples from the original build. Replace them with the operator's brand architecture from
> Phase 0 — a CDP dataset that disagrees with the CRM reskin breaks the exact cross-system
> unification demo it exists to prove.
