experience-cms-content-type-generate — скилл для Claude Code: типы
Создаёт ContentTypeBundle в Salesforce CMS: находит существующие типы, собирает два файла и проверяет их dry-run деплоем.
npx skills add forcedotcom/sf-skills --skill experience-cms-content-type-generate -a claude-codeЧто делает experience-cms-content-type-generate
Скилл берёт на себя весь путь от запроса до готового бандла. Сначала он смотрит локальную папку <sfdx-source>/contentTypes/ и спрашивает metadata-grounding про уже существующие типы — стандартные и кастомные. Потом генерирует schema.json и .contentTypeBundle-meta.xml, прогоняет sf project deploy start --dry-run против выбранной org и сам чинит ошибки схемы, до трёх попыток. Деплой спрашивает подтверждение: без «да» ничего не уедет.
Логика жёсткая: новые типы только создаются, существующие только сверяются с org. Если попросить добавить или переименовать поле у уже развёрнутого типа, скилл скажет об этом одной строкой и всё равно покажет сводку по типу — молча делать вид, что правка прошла, он не станет. Для правок придётся руками менять schema.json и деплоить заново либо идти в CMS UI.
Что нужно заранее: CLI sf версии 2.0.0 и выше, API не ниже 64.0, плюс MCP-серверы metadata-grounding и content-readonly. Для вызова с известным воркспейсом передают spaceId или folderId, но не вместе с baseType — эти параметры взаимоисключающие, и вызов с обоими не пройдёт.
Какие задачи решает
- создать переиспользуемый тип контента в Salesforce CMS
- сверить локальный бандл с тем, что уже развёрнуто в org
- поймать ошибки схемы до реального деплоя
- сгенерировать
schema.jsonи.contentTypeBundle-meta.xmlпод валидатор - найти существующие типы перед тем, как плодить новый
Примеры запросов агенту
- создай тип контента для новостной карточки в Salesforce CMS
- сгенерируй схему для типа «Акция» в CMS
- разреши тип
myNS__PromoCardи покажи его схему
Как установить experience-cms-content-type-generate
1Через Skills CLI (Vercel)
npx skills add forcedotcom/sf-skills --skill experience-cms-content-type-generate -a claude-code2Вручную: положить файлы
~/.claude/skills/experience-cms-content-type-generate/SKILL.mdПапка скилла целиком; для одного проекта — .claude/skills/experience-cms-content-type-generate/
1Через Skills CLI (Vercel)
npx skills add forcedotcom/sf-skills --skill experience-cms-content-type-generate -a codex2Вручную: положить файлы
.agents/skills/experience-cms-content-type-generate/SKILL.mdДля всех проектов — ~/.agents/skills/
1Через Skills CLI (Vercel)
npx skills add forcedotcom/sf-skills --skill experience-cms-content-type-generate -a cursor2Вручную: положить файлы
.cursor/skills/experience-cms-content-type-generate/SKILL.mdCursor читает и .agents/skills/, и .claude/skills/
1GitHub CLI
gh skill install forcedotcom/sf-skills experience-cms-content-type-generate2Вручную: положить файлы
.github/skills/experience-cms-content-type-generate/SKILL.md1Через Skills CLI (Vercel)
npx skills add forcedotcom/sf-skills --skill experience-cms-content-type-generate -a gemini-cli2Командой платформы
gemini skills install https://github.com/forcedotcom/sf-skills --path skills/experience-cms-content-type-generateВручную: положить файлы
.devin/skills/experience-cms-content-type-generate/SKILL.mdСтарый путь .windsurf/skills/ тоже читается
Через Skills CLI (Vercel)
npx skills add forcedotcom/sf-skills --skill experience-cms-content-type-generate -a opencodeClaude.ai
Упакуйте папку в zip и загрузите в Settings → Capabilities → Skills
По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов
Из того же набора167
Похожие
Рядом в теме «Настройка агента: память, контекст, субагенты»
Все скиллы по теме «Настройка агента: память, контекст, субагенты» →Вопросы о experience-cms-content-type-generate
Как установить experience-cms-content-type-generate в Claude?
Выполните npx skills add forcedotcom/sf-skills --skill experience-cms-content-type-generate -a claude-code. Скилл встанет в ~/.claude/skills/experience-cms-content-type-generate/SKILL.md.
Что делает experience-cms-content-type-generate?
Создаёт ContentTypeBundle для Salesforce CMS: находит существующие типы через metadata-grounding, собирает два файла бандла, проверяет их dry-run деплоем и при подтверждении разворачивает.
Можно ли этим скиллом изменить существующий тип?
Нет. Новые типы он только создаёт, существующие — сверяет с org. Чтобы добавить или переименовать поле, правьте schema.json и деплойте заново либо используйте CMS UI.
Исходный текст
SKILL.md · Apache-2.0
---
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 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 "<original message>" 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 <N> 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: <row-1 FQN>` / `Provide an FQN` / `Create new: <newName>` / `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: <Name>` | Go to **step 1e** with `fqn = <namespace>__<Name>` (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: <contentTypeName>` | 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:<DeveloperName> --target-org <alias> --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: "<grounding row id from 1c>" })` 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 "<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 `<sfdx-source>/contentTypes/<DeveloperName>/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=<alias>` or `defaultUsername` in `sfdx-project.json`). Resolve to `<orgAlias>` and pass as `--target-org <orgAlias>` 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 `<orgAlias>`. 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 `<alias> (<username>) [<devhub|sandbox|scratch|prod>]`, plus `Cancel`. On pick, use as `<orgAlias>` 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: <contentTypeName>` 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.** `<contentTypeName>` 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 `<contentTypeName>` at `<sfdx-source>/contentTypes/<contentTypeName>/`:
| # | 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 NO...
© forcedotcom · Apache-2.0 · forcedotcom/sf-skills