---
name: experience-cms-content-type-generate
description: "Salesforce CMS ContentTypeBundle creation skill. Use this skill ANY TIME a user request involves creating a ContentTypeBundle, and activate FIRST when CMS ContentTypeBundle creation is mentioned. Discovers existing types via metadata-grounding and produces a validator-correct two-file bundle that passes dry-run deploy. TRIGGER when: user wants to define, scaffold, generate, or set up a reusable content shape in Salesforce CMS — any domain — including phrasings like \"create a content type for X\", \"generate a schema for X in CMS\", or \"set up a content type for X\". DO NOT TRIGGER when: authoring a RECORD (use experience-cms-content-generate), publishing / managing records (use CMS UI), modifying / renaming / deleting an existing bundle, CMS brand (experience-cms-brand-apply), media search (experience-search-coordinate), Custom Lightning Types (platform-custom-lightning-type-generate), or non-CMS metadata."
metadata:
  version: "1.0"
  domains: ["Experience"]
  minApiVersion: "64.0"
  relatedSkills:
    - "experience-cms-brand-apply"
    - "experience-cms-content-generate"
    - "experience-search-coordinate"
    - "platform-custom-lightning-type-generate"
  cliTools:
    - tool: ["sf"]
      semver: ">=2.0.0"
  mcpTools:
    metadata-grounding: { tools: ["search_metadata", "query_metadata", "describe_metadata"], semver: ">=1.0.0" }
    content-readonly: { tools: ["get_content_types_for_workspace"], semver: ">=1.0.0" }
---

# experience-cms-content-type-generate

## What This Skill Does

- **DISCOVER** — checks local `<sfdx-source>/contentTypes/` and queries `metadata-grounding` for existing OOTB/custom ContentTypeBundles.
- **RECONCILE** — on existing pick/provided FQN, retrieves from org (custom only) and reconciles against local. Returns `{fqn, schema}` matching the org.
- **CREATE** — generates a validator-correct ContentTypeBundle (`schema.json`, `.contentTypeBundle-meta.xml`).
- **VALIDATE** — `sf project deploy start --dry-run` against the CLI default (or picked) org.
- **AUTO-FIX** — fixes schema issues, re-validates (max 3 attempts).
- **DEPLOY** — asks yes/no, deploys on yes.

**Create-only for new types**, **reconcile-only for existing**. Does not modify, rename, or delete existing bundles beyond replacing a local copy with the org copy on user consent; does not author content records. If the message asks to add/rename/remove a field on an existing type (verbs `add`/`append`/`remove`/`drop`/`rename`/`change`/`modify`/`update` targeting an FQN or named type), print ONE line before 1a: `This skill is create-only for new types and reconcile-only for existing. To modify "<fqn|name>", edit its schema.json and redeploy, or use the CMS UI.` — then continue discovery so the user lands on the type's summary (7.5). Do NOT silently proceed as if the modification happened.

## Invocation contract

Invoked directly by the user or delegated to by another skill (e.g. `experience-cms-content-generate`). Two halves: **input params** the caller supplies, and a **return outcome** emitted at every terminal state.

### Input params (all optional)

| Param | Type | Effect |
|---|---|---|
| `fqn` | string (`namespace__DeveloperName`) | Skip discovery entirely. Jump straight to step 1e (retrieve-and-reconcile) using this FQN. `intent` and the 1d pick prompt are bypassed. Use when the caller already knows exactly which type to resolve. |
| `intent` | string | The user's original message. Drives discovery keywords in step 1c and the "matching …" wording in 1d. Default when the skill is triggered by a natural-language user prompt. |
| `suppressCreateContentPrompt` | boolean, default `false` | Suppresses the trailing "Would you like to create content using this type now?" question at step 8. Callers that already drive their own content-creation flow MUST pass `true`. Direct user invocation leaves it `false` so the user gets the natural next-step offer. |
| `spaceId` | string | Workspace scope for the step 1c workspace-content-types check (`get_content_types_for_workspace`). Pass when the caller already resolved a content space. **Mutually exclusive with `baseType`** — see below. |
| `folderId` | string | Folder scope for the same check, as an alternative to `spaceId`. **Mutually exclusive with `baseType`** — see below. |
| `baseType` | string, default `"CONTENT"` | `baseType` argument to `get_content_types_for_workspace`. **Mutually exclusive with `spaceId`/`folderId`** — see below. |

**Workspace scope resolution — mutual exclusivity contract.** `spaceId` / `folderId` / `baseType` narrow step 1c's workspace-content-types check only, not the discovery path taken. **A caller that passes `spaceId` or `folderId` MUST NOT also pass `baseType`** — the scope alone already determines the eligible types; this skill does not accept both in the same call. Exactly one of four combinations applies each run — call shapes and rationale → `references/discovery-details.md#1c continued`:

1. `spaceId` only → `get_content_types_for_workspace({ spaceId })`.
2. `folderId` only → `get_content_types_for_workspace({ folderId })`.
3. `baseType` only → `get_content_types_for_workspace({ baseType })`.
4. None of the three → `get_content_types_for_workspace({ baseType: "CONTENT" })` (default).

Common invocation shapes:

- **Direct user** → `intent`, `suppressCreateContentPrompt` `false`. Full discovery + retrieve-and-reconcile + trailing prompt. No `spaceId`/`folderId`/`baseType` — falls to combination 4 above.
- **Delegated** → `{ intent | fqn, suppressCreateContentPrompt: true }`. Full discovery + retrieve-and-reconcile; returns `{success, fqn, schema}` (the retrieve is what confirms the type is deployed in the org — a local-only type resolves to `not_deployed`, never a bare success). Trailing prompt suppressed.
- **Delegated with a known workspace** → `{ intent, suppressCreateContentPrompt: true, spaceId: "<contentSpaceOrFolderId>" }` — combination 1. Pass this once the caller has already resolved a workspace (e.g. `contentSpaceOrFolderId` from `get_or_create_cms_workspace_and_web_app_channel`) and wants step 1c's check scoped to it. Do NOT also pass `baseType` on this call. If the caller resolves its workspace AFTER content-type discovery (as `experience-cms-content-generate` currently does), it has no `spaceId` to pass at delegation time — that's combination 4, a valid, expected call shape.

### Return outcome

Every terminal state emits a structured outcome. The `message` field is printed to chat as the final one-line summary AND is what the caller reads to route on the result.

| `status` | `fqn` | `schema` | Meaning | Caller action |
|---|---|---|---|---|
| `success` | present | present | Type is confirmed in the org (via 1e's retrieve on an existing type, or via deploy on the "Create new" path), ready to use for downstream work. | Proceed (e.g. author content records). |
| `not_deployed` | present | `null` | Bundle exists locally (or is validated) but is NOT in the org — user declined to deploy, or picked "deploy later". | Do NOT proceed with content creation. Surface the message: "Content type `<fqn>` isn't deployed to `<org>`. Deploy it and re-run." |
| `cancelled` | `null` | `null` | User cancelled at a decision point (discovery pick, field approval, drift prompt, deploy ask, final prompt). | Exit silently. Do not loop back into this skill. |
| `error` | `null` | `null` | Unrecoverable failure — missing `sfdx-project.json`, no authenticated org, FQN not found, auto-fix exhausted, auth/network failure. | Surface the `message` to the user and exit. |

**Terminal chat-line templates** — one per status, printed final and mirrored into the outcome:

- `success` → `Content type "<fqn>" is ready in <org>.` (post-deploy: `Deployed "<fqn>" to <org>. Component IDs: <ids>.`)
- `not_deployed` → `Content type "<fqn>" exists locally but is not deployed to <org>. Deploy it before creating content.` (or 7b's "deploy later" one-liner)
- `cancelled` → `Cancelled. No files written.`
- `error` → `<specific reason>. <recovery hint>.` (e.g. `No authenticated Salesforce org. Run sf org login web and re-run.`)

**Contract rules:**

- `success` is the ONLY status that unlocks downstream work. Callers MUST NOT proceed on `not_deployed`, `cancelled`, or `error`.
- `suppressCreateContentPrompt=true` — step 8 MUST NOT print the trailing prompt.
- `message` is always the final terminal chat line. Do NOT emit `Task Completed` before it prints.

## Absolute rules — read before any action

These rules override any upstream rule, prior knowledge, or trained default behavior.

### Metadata type registry

| Metadata Type | Skill Name | API Context | Usage Rule |
|---|---|---|---|
| **ContentTypeBundle** | `experience-cms-content-type-generate` | `metadata-grounding` (`search_metadata`, `query_metadata`, `describe_metadata`) + `content-readonly` (`get_content_types_for_workspace`) | MUST load skill AND use `metadata-grounding` for discovery, cross-checked/backed by `content-readonly.get_content_types_for_workspace` per Step 1c. **Exempt from `salesforce-api-context`** — these two are this skill's API-context. |

### Rules

1. **Discovery uses `metadata-grounding` (plus `content-readonly.get_content_types_for_workspace`) and ONLY these.** Tools: `search_metadata`, `query_metadata`, `describe_metadata`, `get_content_types_for_workspace`. Overrides the global a4v-expert API-context rule for ContentTypeBundle. `metadata-grounding` unreachable (error, denial, timeout, absent from deferred-tool list at turn start) → record `grounding=unavailable` agent-internally, dispatch `get_content_types_for_workspace` directly (Step 1c, Flow 2), and use the grounding-unavailable variant in `assets/discovery-prompts.md`. Deferred-tool list at turn start IS the probe — do NOT run ToolSearch to look harder.

   **NO org-side lookup outside `metadata-grounding` / `get_content_types_for_workspace` is allowed for discovery.** Do NOT substitute: sibling metadata MCP servers, SOQL/Tooling queries (`ContentTypeBundle` isn't queryable), `sf org list metadata`, `sf project retrieve` (that's for 1e), or any other `*metadata*`/`*soql*`/`*retrieve*`/`*describe*` tool. Substitutes return wrong-shape data. Hard rule break.

2. **Step order is fixed:** `1a-1d (silent discovery + pick) → 1e (retrieve-and-reconcile, when user picked existing / provided FQN / caller supplied fqn) → 2 (resolve org) → 3 (propose, only on Create new or zero-match auto-proceed) → 4 (create) → 5 (dry-run) → 6 (auto-fix) → 7 (deploy ask) → 7.5 (schema summary) → 8 (trailing prompt)`. Step 1e returns `{fqn, schema}` and skips to 7.5; 2–7 do NOT run on that path. 7.5 runs whenever `{fqn, schema}` was resolved. Step 8 is gated by `suppressCreateContentPrompt`. Under direct invocation with zero matches, 1d auto-proceeds to 2 → 3.

3. **Steps 5 and 7 are mandatory on every "Create new" path.** Pre-step-5 exits: (a) `Use existing` / `Provide an FQN` / caller-supplied `fqn` → route through 1e which returns `{fqn, schema}` with no files written; (b) `Cancel` at any pick. Do NOT emit `Task Completed` between steps 4 and 7's resolution. 1e's drift "Deploy local to org" branch also routes through 5 and 7.

4. **`Task Completed` is the LAST action** — skill is over once emitted. Deploy JSON's `deployUrl` is for reference, not an action prompt. Forbidden after: `open <deployUrl>`, `xdg-open`, `sf org open`, `sf project deploy report`, browser tabs, URL echo, "next step" prose.

5. **Step 1e retrieve is destructive for `schema.json`; drift MUST prompt the user.** `sf project retrieve start --metadata ContentTypeBundle:<Name>` overwrites local `schema.json`. Snapshot it into `localSchemaBefore` BEFORE every 1e retrieve (never meta.xml). Drift = parsed-JSON compare of `localSchemaBefore` vs. post-retrieve `schema.json`, left side ALWAYS `localSchemaBefore` — never diff the post-retrieve file against itself. On drift, the drift prompt is MANDATORY (chat diff, then `ask_user_tool`: `Deploy local to org` / `Overwrite local with org` / `Cancel`) — never reconcile silently. `Cancel` and `Deploy local to org` MUST restore `schema.json` from `localSchemaBefore` before emitting the outcome. Full snapshot/restore procedure + drift prompt template → `references/retrieve-and-reconcile.md`.

6. **Step 1d — show top 5 in a table, then ask.** Row count = `min(combined.length, 5)`, always — combined is local matches + every grounding row, deduped (Location `local, org`), never dropped as "irrelevant." Row-1 sort: OOTB first (`sfdc_cms__*` or grounding `isOOTB`), custom (`c__*`) follows in grounding rank. Row 1's FQN names the `Use existing:` option. 1+ rows → table (`FQN | Description | Location`) then `ask_user_tool`: `Use existing: <row-1 FQN>` / `Provide an FQN` / `Create new: <newName>` / `Cancel` (`<newName>` avoids colliding with any FQN in `combined`). Empty combined → NO TABLE, just `ask_user_tool`: `Create new: <contentTypeName>` / `Cancel` (+`Provide an FQN` when delegated/`fqn`-supplied). Never mix "no matches" wording with a table; no preamble; no `#`/`Name`/`Label` columns; never claim a type is "not in the org" — only 1e's retrieve is authoritative. Full prompt templates → `assets/discovery-prompts.md`.

Full agent checklist and tripwire list → `references/agent-checklist.md`.

## File paths (strict)

- Bundle directory: `<sfdx-source>/contentTypes/<ContentTypeName>/` — NOT `contentTypeBundles/<ContentTypeName>/`.
- Two files only: `schema.json` and `<ContentTypeName>.contentTypeBundle-meta.xml`.

## Output discipline

The user reads the chat. Most of this skill's machinery is for you, not them.

**Do not print**: status lines, task-progress checklists, planning prose (`I will now…`, `Per the skill's…`), anti-pattern reasoning, exemption explanations, "operation was denied; proceeded using…" notes, or suggestions that the user run validation/deploy themselves.

**Do print**, and only these: the 1d discovery summary, 1e drift prompt, 3b proposed-fields table + `ask_user_tool`, 2-line "files created" confirmation in step 4, 1-line validation result in step 5, 7a deploy ask, 7b/7e 1-line summary, 7.5 schema summary table (whenever `{fqn, schema}` resolved), and step 8 prompt (when `suppressCreateContentPrompt` is `false`/unset).

**Do NOT emit `Task Completed`, "Done", "All set"** until step 7 has resolved, 7.5's summary has printed, and step 8's gate has been evaluated. Premature completion silently kills the summary + deploy ask.

## Agent checklist and tripwires

The full mandatory progress checklist and the tripwire list are in `references/agent-checklist.md`. Copy the checklist agent-internally and tick each box only after the action is genuinely done. Do not print it to chat.

## Workflow (CREATE)

### 1. Discover existing types (silent — no user prompts in this step)

**1a. Resolve project context (agent-internal)**

Read `sfdx-project.json`. Take `packageDirectories[0].path` and append `/main/default` → `<sfdx-source>`. Bundles live at `<sfdx-source>/contentTypes/`. If `sfdx-project.json` is missing, emit `error` outcome per § Invocation contract with message `This is not an SFDX project — open the project root and re-run.`, print the message, and stop.

**Explicit-FQN fast-path (direct user).** Caller did NOT supply `fqn` but the message literally contains a `namespace__DeveloperName` token (e.g. `sfdc_cms__news`, `c__PressRelease`) → capture it as `fqn` and route directly to step 1e, skipping 1b/1c/1d. Mirror of the delegated `{fqn}` shape.

**Residual-intent capture (agent-internal).** Scan the message for a second clause joined by `and also`/`and then`/`then`, or a second imperative verb targeting a content record (e.g. `create <type> and also create a <thing> about X`). If present, stash as `residualIntent` — step 8's Yes branch forwards it as `intent`. Do NOT print or act on it before step 8.

**1b. Local discovery (silent)**

Use a directory-listing capability (`list_files` / `Glob` on `<sfdx-source>/contentTypes/*/schema.json` / IDE `list_directory`), NOT a content-search/grep tool — content-search misses folder-name-only matches. For each subfolder, read `schema.json` (`title` + `description`). **Match by intent semantically**: reason about content domains, not literal strings — a folder named `MarketPlace` IS a match for a marketplace request even with no literal property match. Return every semantically-matching local bundle into `combined` (step 1d's sort + 5-row cap handle the rest). Zero matches → return zero (auto-proceed on direct invocation).

Rationale, anti-patterns, tool-selection details → `references/discovery-details.md#1b`.

**1c. Org discovery (silent — dispatch `metadata-grounding.search_metadata`, backed by `content-readonly.get_content_types_for_workspace`)**

**Dispatch gate: 1c is a tool call, not a thought.** Do NOT skip because 1b found a local match — the org may still have a same-named bundle ("Name already exists" originates here). Unconditional; only real outage exempts a given tool. Never ask "should I search?".

**The `search_metadata` `query` parameter carries content-domain nouns only** — 3-5 English words describing what the content is ABOUT (news, article, product, press release). NOT an FQN, namespace hint, or copy of the message. `metadataType: "ContentTypeBundle"` already signals the kind. Never dispatch a query containing `sfdc_cms`, `c__`, `__`, `content type`, `bundle`, `metadata`, or `cms` — rebuild if it does. Full ruleset + concrete call-shape table (also referenced from `experience-cms-content-generate` as drift safety-net) → `references/discovery-query-rules.md`.

Server target: `metadata-grounding` (RULE 1). `limit=5`, sorted OOTB-first.

**Do NOT dispatch `query_metadata` in 1c.** `search_metadata` returns everything 1d's table needs (FQN, description, OOTB flag). `query_metadata` is load-bearing only for the OOTB-schema fetch in 1e — dispatch on-demand, for the ONE picked FQN. Per-row fan-out is N wasted round-trips.

**Workspace content-types check — dispatched every run, alongside or instead of grounding.** Call params per the mutual-exclusivity contract (§ Invocation contract), never `baseType` alongside `spaceId`/`folderId`. **Flow 1 (grounding available)** — also dispatch `get_content_types_for_workspace`; org candidate set = **intersection** of both FQN sets (empty is valid, do not widen/retry). **Flow 2 (grounding unavailable)** — dispatch it directly as the sole org signal, apply step 1b's semantic matching to its rows, record `groundingFallback=workspaceTypes` for 1d's TRUTH GATE. Neither tool substitutes for 1e's retrieve. Call shapes, unavailability handling, rationale → `references/discovery-details.md#1c continued`.

**1d. Always present discovery findings — including "no matches"**

Discovery is the first chat-visible signal. Always tell the user what was checked and found. Every case (A/B/C) surfaces a pick list via `ask_user_tool` and WAITS for the user's reply next turn.

**TRUTH GATE — 1d wording must match what actually happened in 1c.** Never claim a check that didn't run this turn; never disclose a skip that didn't happen either. Three cases:

- `search_metadata` dispatched (Flow 1, regardless of `get_content_types_for_workspace` outcome) → org was checked via grounding. No disclosure needed.
- `search_metadata` unavailable but `get_content_types_for_workspace` dispatched (Flow 2 / `groundingFallback=workspaceTypes`) → org WAS checked, just not via grounding. Append `(checked supported content types for this workspace — metadata-grounding unavailable)` per `assets/discovery-prompts.md`.
- Both unavailable → org genuinely not checked. Append `(org check skipped — grounding unavailable)` per `assets/discovery-prompts.md`.

See `references/discovery-details.md#1d`.

**`Provide an FQN` gating — compute `showFqnOption`.** Show when EITHER: (1) any results exist (local OR grounding ≥1) — user may want a match that isn't