--- 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 `/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 "", 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: "" }` — 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 `` isn't deployed to ``. 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 "" is ready in .` (post-deploy: `Deployed "" to . Component IDs: .`) - `not_deployed` → `Content type "" exists locally but is not deployed to . Deploy it before creating content.` (or 7b's "deploy later" one-liner) - `cancelled` → `Cancelled. No files written.` - `error` → `. .` (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 `, `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:` 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: ` / `Provide an FQN` / `Create new: ` / `Cancel` (`` avoids colliding with any FQN in `combined`). Empty combined → NO TABLE, just `ask_user_tool`: `Create new: ` / `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: `/contentTypes//` — NOT `contentTypeBundles//`. - Two files only: `schema.json` and `.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` → ``. Bundles live at `/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 and also create a 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 `/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 row 1; rows 2–5 and beyond-cap results surface in the FQN option parenthetical; OR (2) invocation is delegated (`suppressCreateContentPrompt === true` OR caller supplied `fqn`) — always available in the delegated no-matches variant. Otherwise (direct invocation AND zero matches) → `showFqnOption = false`; skip the pick prompt, print an info line, auto-proceed to step 2. Offering FQN input when the direct user asked to create fresh and nothing matched switches their goal. **Intent-sanity gate on direct-invocation zero-matches.** Before auto-proceeding, extract at least one recognizable content-domain noun from the message (real word, named entity, or compound domain vocabulary — not gibberish, not filler-only after stripping mechanic nouns like `content type`/`bundle`/`CMS`/`schema`/`metadata`). ≥1 recognizable noun → proceed to step 2 → 3, with the step 3 proposed name built ONLY from those tokens. Gibberish/filler-only → do NOT auto-proceed; ask `Your request "" doesn't name a content domain. What kind of content type would you like to create (e.g. news, blog, press release, product)?` with `Cancel` + free-text; free-text restarts 1b, `Cancel` → `cancelled`. Same gate applies when 1c's rebuilt query would be empty — do NOT dispatch a blank `search_metadata`. STRICTLY DO NOT fabricate a name from tokens absent from the message. Full rule + examples → `references/discovery-details.md#1d`. **Prompt templates** (has-matches, zero-matches direct auto-proceed, zero-matches delegated, "Provide an FQN" follow-up) → `assets/discovery-prompts.md`. Copy verbatim. **1d output shape — TWO separate outputs, never merged (the #1 reported UX defect):** 1. **Chat markdown FIRST** — the `Top matching content types:` header + the `FQN | Description | Location` table (Output 1 in `assets/discovery-prompts.md`). Plain chat text, NOT the tool. 2. **THEN `ask_user_tool`** whose `question` is EXACTLY `Found matches. Pick one:` (verbatim, one short sentence) with the 4 FIXED options: `Use existing: ` / `Provide an FQN` / `Create new: ` / `Cancel`. **STRICTLY DO NOT:** write any preamble/prose sentence before or instead of the table; put the table, its columns, or the `(checked supported… / org check skipped…)` suffix INSIDE the `question` field (it renders as one flat line with no markdown — the reported broken wall of text); paraphrase or "make more helpful" the `question` (it is verbatim `Found matches. Pick one:`); add a `Use existing:` option per row (options are FIXED at 4 — extra rows are reached via `Provide an FQN`). Everything structured goes in the chat markdown of Output 1; the `question` field stays a single plain sentence. → `assets/discovery-prompts.md`, copy verbatim. **Routing after the user replies:** | User pick | Next action | |---|---| | `Use existing: ` | Go to **step 1e** with `fqn = __` (default namespace `c` for local matches whose folder isn't namespaced). | | `Provide an FQN` (only present when `showFqnOption = true`) | Ask the follow-up in `assets/discovery-prompts.md`. On reply → **step 1e** with that FQN. | | `Create new: ` | Continue to step 2 → step 3. | | `Cancel` | Emit `cancelled` outcome per § Invocation contract, print `Cancelled. No files written.`, then `Task Completed`. | STRICTLY DO NOT print the pick list and announce "proceeding to create a new one" in the same message. The user picks, not you. ### 1e. Retrieve and reconcile Reached when 1d resolves to a match / provided FQN, OR when the caller invoked with `{fqn}`. Goal: return `{fqn, schema}` matching the org. **Namespace gate:** - **Custom FQN** (`c__*`, non-platform namespace) → run `sf project retrieve start --metadata ContentTypeBundle: --target-org --json`. Always retrieve for custom. - **OOTB FQN** (`sfdc_cms__*`) → skip `sf project retrieve start` (returns nothing usable). **This branch needs a live `metadata-grounding` schema, regardless of which flow surfaced the FQN** — `get_content_types_for_workspace` (Flow 2) never returns a schema, only `{fqn, name, description}`. If 1c already recorded `grounding=unavailable`, do NOT attempt `query_metadata` — go straight to the error below. Otherwise dispatch `query_metadata({ metadataType: "ContentTypeBundle", id: "" })` NOW for the picked FQN only, and resolve the schema from its response (fall back to `describe_metadata` if it returns no schema payload). If that response is error/empty/non-schema, surface `Can't resolve OOTB FQN "" without a real schema from metadata-grounding. Retry when grounding is back, or provide a custom FQN.` and exit with `error`. **Never fabricate OOTB schemas from training data** — "sfdc_cms__news typically has title/body/summary…" IS the bug thought that poisons 3–7. Fail closed. Full rule → `references/retrieve-and-reconcile.md` § Namespace gate. **Reconciliation** — compare retrieved schema against `/contentTypes//schema.json`. Full matrix + drift template + routing → `references/retrieve-and-reconcile.md`. **Routing outcomes:** - Not in local, retrieved → write local, return `{fqn, schema=retrieved}`. - In both, match → return `{fqn, schema=local}`. - In both, differ → drift prompt (`Deploy local to org` → step 5/7; `Overwrite local with org` → replace files, return; `Cancel` → exit). - In local only → print step 7.5 summary first (user sees what they'd deploy), then offer deploy (5/7) or cancel. - In neither → re-dispatch 1d pick with `Try a different FQN` option. **On success, return `{fqn, schema}` and continue to 7.5 → 8.** No proposal-of-fields, no re-validation — reconciled schema is source of truth. Create-only for new; reconcile-only for existing. "Create new" in 1d is the ONLY path that proceeds to step 2. ### 2. Resolve target org Prefer the SF CLI default (alias from `sf config set target-org=` or `defaultUsername` in `sfdx-project.json`). Resolve to `` and pass as `--target-org ` on every `sf` call. When a default is set, this step is silent. **No default — fallback:** run `sf org list --json` (silent): - **Zero authenticated orgs** → emit `error` with message `No authenticated Salesforce org found. Run sf org login web and re-run.`, print, stop. - **Exactly one** → use silently as ``. Do not modify `sf config`. - **Two or more** → dispatch `ask_user_tool` with question `No default Salesforce org is set. Pick the org to use for this run:` and one option per org labelled ` () []`, plus `Cancel`. On pick, use as `` for this run only (do NOT run `sf config set target-org=…`). `Cancel` → emit `cancelled`, print `Cancelled. No files written.`, `Task Completed`. The picked alias applies to every subsequent `sf` call this run (1e retrieve, 5 dry-run, 7 deploy). ### 3. Propose fields and get user approval Preconditions: 1c dispatched `search_metadata` (or recorded `grounding=unavailable`), and 1d resolved to `Create new: ` in the previous turn. Other 1d picks route elsewhere: `Use existing` / `Provide an FQN` → 1e; `Cancel` → emit `cancelled`. If you just printed a pick list via `ask_user_tool` and the user hasn't replied, stop — your turn is over. **3a. Determine field properties (agent-internal).** Set `apiName` (camelCase), `title` (human-readable), `lightning:type`, `required`. Default-minimal: no `lightning:textIndexed`, `lightning:localizable`, length/range bounds, `enum`, `const`, `placeholderText` unless the user asked. **Content-type name must be derivable from the user's message.** `` and its `title` must be built from tokens actually present (PascalCase/word-order shaping OK — `laptop review` → `LaptopReview`). STRICTLY DO NOT invent from thin air, from an unrelated open tab, or from prior-session context. If 1d's sanity gate passed but the noun set is thin (one word), use that word — do not embellish. Proposing a name with tokens absent from the message IS the hallucination anti-pattern; stop and restart 1d's gate. Initialize `userEditedFields = false` agent-internally. Governs step 6's branching. Flipped to `true` only when the user types free-text edits in the 3b loop (see `references/edit-fields-loop.md`). `Edit fields` → `Approve as shown` without typing any edit leaves it `false` — proposal accepted unchanged. **3b. Present proposed fields — markdown table FIRST as chat-visible text, THEN `ask_user_tool` in the same turn.** Do NOT collapse the table into the tool prompt — the UI strips formatting and the user cannot approve informed. Message 1 (chat-visible plain text): ```text Proposed fields for `` at `/contentTypes//`: | # | API name | Type | Required | Constraints | Title | |---|---|---|---|---|---| | 1 | `title` | `lightning__textType` | yes | maxLength: 200 | Title | | 2 | `body` | `lightning__richTextType` | yes | — | Body | ``` Constraints format follows 7.5 (see `references/schema-summary-format.md`): comma-separated `key: value` pairs for any per-field constraint asked or proposed — `maxLength`, `minLength`, `minimum`, `maximum`, `enum`, `const`, `lightning:localizable`, `lightning:allowedUrlSchemes`, etc. Use `—` when a field has no constraints beyond `required`. Message 2 (same turn): dispatch `ask_user_tool` with question `Approve these fields, edit them, or cancel?` and options `Approve`, `Edit fields`, `Cancel`. - `Approve` → step 4. - `Edit fields` → follow the loop below. Loop until approved. - `Cancel` → emit `cancelled`, print `Cancelled. No files written.`, `Task Completed`. **Edit-fields loop** — each round reprints the current table (chat-visible), then dispatches `ask_user_tool` with `Approve as shown` / `Cancel` options and accepts free-text edit instructions. **Free-text tool result IS the edit instructions — parse and apply, then loop; do NOT stop.** Full template + anti-stop guidance → `references/edit-fields-loop.md`. ### 4. Create files (silent — confirm in two lines max) Precondition: step 3b returned the user's `Approve` in the immediately preceding turn. No file write before approval. Create `/contentTypes//`. Use the already-loaded `assets/schema-example.json` (per § Schema rules — do NOT re-fetch a sibling `schema.json`). Generate `schema.json` for the approved fields and `.contentTypeBundle-meta.xml` with exactly these four lines, swapping only ``: ```xml ``` Do NOT add ``, ``, ``, ``, or any legacy SOAP element — those fail the validator. Per-field forbidden keys (`type`, `format`, `default`, empty `lightning:uiOptions`) → `references/schema-rules.md`. After writing, print exactly two lines: ```text Created /contentTypes//schema.json Created /contentTypes//.contentTypeBundle-meta.xml ``` Then proceed to step 5. Before dispatching 5a, run the pre-deploy schema checklist in `references/pre-deploy-checklist.md` (agent-internal, do not print). Fix in place and re-check. ### 5. Validate (mandatory) Step 5 is unconditional. Skip only if step 1d ended with `Use existing`. `grounding=unavailable` does not skip step 5 (different auth path). Do not suggest the dry-run as a command for the user to run — you run it. **5a. Run** (no preamble in chat): ```bash sf project deploy start --source-dir /contentTypes/ --target-org --dry-run --json ``` **5b. Result handling:** - `result.status === "Succeeded"` → print one line: `Validated against : Succeeded (dry-run — nothing deployed yet).` This is NOT a terminal state — your next action is step 7a's `ask_user_tool`. - `result.status === "Failed"` with `componentFailures` → step 6 (auto-fix). - `result.status === "Failed"` with zero `componentFailures` → CLI/auth issue. Show the raw response, ask the user to verify `sf org list`, source path, and `sfdx-project.json`. Do not count as one of the 3 attempts. ### 6. Fix validator failures — silent auto-fix or ask-first, based on `userEditedFields` Branches on `userEditedFields` (set in 3a). The split reflects who authored the broken schema: - `false` → user accepted the agent's proposal unchanged. Validator failure is the agent's mistake. **Silent auto-fix.** - `true` → user typed edits in 3b. Failure may reflect intent (e.g. `enum` on integer, `default` on text) only the user can resolve. **Show planned fixes, ask first.** Both branches share the sweep-the-whole-schema principle and the 3-attempt cap on agent-driven fixes. Each attempt fixes every problem the validator could possibly flag across the whole schema, not one field at a time — validators only report 1–2 errors per pass; don't fall into one-field-per-attempt. Error-to-fix mapping (incl. safe vs. intent-changing `Kind`) → `references/deployment-errors.md`. **6a. Silent auto-fix (`userEditedFields === false`)** 1. Read every `componentFailures[].problem`. 2. Sweep `properties` for the same class of issue. E.g. one `placeholderText missing` → audit all `lightning:uiOptions`; one `format` rejected → strip `type`/`format`/`default` from every field; one disallowed type-specific key → audit every field of that `lightning:type`. 3. Re-write once, re-run step 5 once. Counts as one of 3 attempts. If still failing after 3 attempts: surface the last error and route to 6b's `Let me edit` follow-up. On abandon / exit without successful validation, emit `error` with the last validator error as `message`, stop. **6b. Ask-before-fix (`userEditedFields === true`) — show planned fixes** The user authored something the validator rejected. Do NOT silently rewrite their intent. Message 1 (chat-visible plain text, NOT in `ask_user_tool`) — enumerate every `componentFailures[].problem`, look up fixes in `references/deployment-errors.md`: ```text Validation failed on your edited schema. Planned fixes: ✗ : Planned: () ✗ : … Planned: … ``` Include EVERY error this pass, grouped by field. When a fix drops/changes a user-authored constraint (`enum`, `const`, `lightning:localizable`, numeric bound, type swap), the parenthetical MUST state the concrete consequence — "loses numeric ordering", "no longer restricted to enum values", "text won't localize" — not "changes the type." Kind=safe fixes (`default` strip, `type`/`format` strip, `allowedUrlSchemes: ["https"]`) get the literal `safe: validator hygiene, no intent change`. Message 2 (same turn) — dispatch `ask_user_tool` with question `How would you like to proceed?` and options `Apply these fixes and continue` / `Let me edit — I'll fix it, then re-validate` / `Cancel`. Route: - **`Apply these fixes and continue`** → apply exactly the fixes shown, re-write once, re-run 5 once. Counts as one of 3 attempts. Success → 7a. Fail → loop back to top of 6b with the new errors (`userEditedFields` stays `true`). - **`Let me edit`** → print file paths from step 4's two-line confirmation and validator errors verbatim, dispatch `ask_user_tool` with question `Reply when you're done editing.` and options `Re-validate` / `Cancel`. `Re-validate` → re-run step 5 (user-driven cycles do NOT count against 3 attempts). `Cancel` → emit `cancelled`, print `Cancelled. Your edits remain at /contentTypes//`, `Task Completed`. Do NOT delete the files. - **`Cancel`** → emit `cancelled`, print `Cancelled. No files written.`, `Task Completed`. If the 3-attempt cap on `Apply these fixes` is exhausted: surface the last error and route to the `Let me edit` follow-up (do NOT re-offer `Apply these fixes`). On subsequent `Cancel`, emit `error` with the last validator error as `message`, stop. ### 7. Deploy (yes/no — wait for the next user turn) Step 5's success unlocks step 7; it does not replace it. Ask the user via `ask_user_tool` before any deploy or terminal marker. **7a.** Dispatch `ask_user_tool`: - Question: `Validation succeeded. Deploy "" to now?` - Options: `Yes - deploy now` / `No - I'll deploy later` **7b. User says no**: emit `not_deployed` outcome and stop. Print one line — ```text Validated. Nothing deployed. To deploy later: sf project deploy start --source-dir /contentTypes/ --target-org ``` Return outcome: `{ status: "not_deployed", fqn: "__", schema: null, message: "" }`. Do NOT run step 7.5 or step 8 — the type isn't in the org. **7c. User says yes**: run ```bash sf project deploy start --source-dir /contentTypes/ --target-org --json ``` **7d. Deploy errors:** - With `componentFailures` (rare — occurs after manual edits between validate and deploy, or transient org-side state change): route to step 6's branch matching the current `userEditedFields` flag. Because reaching 7d always follows at least one user-visible turn (step 7a's `Yes - deploy now`), the flag is authoritative for who last authored the file. On successful re-validation, re-ask step 7a's deploy prompt. On unrecoverable failure (auto-fix exhausted then cancelled, or user cancelled in 6b), emit `error` outcome per § Invocation contract with the last validator error as the `message`, then stop. - With zero `componentFailures` (auth/network): emit `error` outcome per § Invocation contract with the raw response summary as the `message`, then stop. **7e. Success**: print one line — `Deployed to . Component IDs: .` This is a `success` terminal state — the outcome to emit at task end is `{ status: "success", fqn: "__", schema: , message: "" }`. Continue to step 7.5. Do not run any other command between here and step 7.5 (see Rule 4). ### 7.5. Schema summary (on every resolved-schema path) Runs whenever `{fqn, schema}` has resolved, regardless of source or of `suppressCreateContentPrompt` — informational, not a turn. Run ONCE per run — track `summaryPrinted` agent-internally; 1e's "local only" branch prints it before deploy-or-cancel, so a subsequent 7e must not re-trigger it. Run when: 1e returned `{fqn, schema}` with non-null schema; 7e succeeded; 1e's drift-prompt "Deploy local to org" reached 7e; or 1e's "local only" branch before deploy-or-cancel. Apply the `summaryPrinted` gate. Do NOT run when: user picked `Cancel`, 7b (deploy-later), any error path with no resolved schema, or `summaryPrinted === true`. Print as chat text (NOT in `ask_user_tool`): ```text Content type "" is ready. Schema: | # | API name | Type | Required | Constraints | Title | |---|---|---|---|---|---| | 1 | `title` | `lightning__textType` | yes | maxLength: 200 | Title | | 2 | `body` | `lightning__richTextType` | yes | — | Body | ``` Column definitions, constraint rules, sort/truncation (20-property cap), do-not-print list → `references/schema-summary-format.md`. Continue immediately to step 8 (no separate turn, no intermediate prompt). ### 8. Trailing "create content?" prompt (gated by `suppressCreateContentPrompt`) Reached only from `success` — step 8 controls only whether an extra prompt is shown after it. Gate: `suppressCreateContentPrompt === true` → skip entirely, emit `success` + `Task Completed` (the prompt would duplicate a turn the caller already drives). Otherwise run when success came via: 1e returned `{fqn, schema}`, 7e succeeded, or 1e drift-prompt "Deploy local to org" reached 7e. Do NOT run on `Cancel` (`cancelled`), 7b (`not_deployed`), or any error path (`error`). Dispatch `ask_user_tool` with question `Would you like to create content using "" now?` and options `Yes — create content now` / `No — I'm done`. - `Yes` → hand off to `experience-cms-content-generate` with `{ fqn, schema, suppressCreateContentPrompt: true }`; also pass `intent: ` when 1a captured one (e.g. `create a news type and also create an article about our Q4 launch` → forward `create an article about our Q4 launch`). Omit `intent` when no residual. Don't loop back. Emit `success` + `Task Completed`. - `No` → print `Done. FQN: .`, emit `success`, `Task Completed`. Do not run any command after step 8 resolves (Rule 4). ## Schema rules (validator truth) CMS deploy validator is source of truth. Full ruleset → **`references/schema-rules.md`** (root, per-field keys, `lightning:uiOptions` sharp edge, per-type accepted keys, quirks). Load before step 4 and step 6. **Also load `assets/schema-example.json` before step 4** — canonical shape reference, every supported `lightning:type` in default-minimal form. Do NOT use a sibling `schema.json` as reference just because step 1b/1e primed it in context — siblings carry user-specific constraints (`maxLength`, `lightning:localizable`, indexing) that would leak in without justification. Non-negotiables: root has `unevaluatedProperties: false` (root only), no `$schema`; per-field only `title` / `description` / `lightning:type` / `lightning:uiOptions` + the type's accepted keys; never `type` / `format` / `default` / empty `lightning:uiOptions`; only `sfdc_cms:metadataContent` mixin. ## Anti-patterns Full anti-pattern table (violation / rationalization / correct action) → `references/anti-patterns.md`. If you catch yourself thinking one of the "bug thoughts" listed there, STOP — that thought is the bug. ## Notes - Folder name, ``, and the bundle's `title` should be consistent (PascalCase folder, human-readable label and title). - Avoid `lightning__fileType` and `lightning__contentReferenceType` — currently unsupported. - Eval datasets live in `packages/adk-eval/eval/domains/experience-cms-content-type-generate/`, not in `tests/evals/`. ## Reference file index - `references/agent-checklist.md` — mandatory progress checklist and tripwires. - `references/schema-rules.md` — validator ruleset (bundle root + per-field + `lightning:uiOptions` + per-type accepted keys + validator quirks). - `references/pre-deploy-checklist.md` — agent-internal schema sanity check before step 5. - `references/deployment-errors.md` — validator error → fix mapping (step 6). - `references/anti-patterns.md` — bug-thought catalogue. - `references/discovery-details.md` — rationale for 1b/1c/1d rules. - `references/discovery-query-rules.md` — single source of truth for how the `search_metadata` query is constructed. Referenced from step 1c here AND from the parent skill `experience-cms-content-generate` (as a drift-safety net). - `references/retrieve-and-reconcile.md` — step 1e reconciliation table and drift routing. - `references/edit-fields-loop.md` — step 3b edit-loop template and routing table. - `references/schema-summary-format.md` — step 7.5 column/sort/truncation rules. - `assets/discovery-prompts.md` — step 1d pick-list template + zero-matches variants. - `assets/schema-example.json` — reference schema covering every supported `lightning:type`.