experience-cms-content-type-generate

SkillFiles & storage

Lets your agent create new Salesforce CMS content types and check they deploy correctly.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the experience-cms-content-type-generate skill

About this capability

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 pa

What this skill tells your AI

The instructions your AI receives, as published by forcedotcom/sf-skills in skills/experience-cms-content-type-generate/SKILL.md and read by ahel’s review.

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).
  • VALIDATEsf 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)

ParamTypeEffect
fqnstring (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.
intentstringThe 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.
suppressCreateContentPromptboolean, default falseSuppresses 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.
spaceIdstringWorkspace 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.
folderIdstringFolder scope for the same check, as an alternative to spaceId. Mutually exclusive with baseType — see below.
baseTypestring, 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 userintent, 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.

statusfqnschemaMeaningCaller action
successpresentpresentType 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_deployedpresentnullBundle 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."
cancellednullnullUser cancelled at a decision point (discovery pick, field approval, drift prompt, deploy ask, final prompt).Exit silently. Do not loop back into this skill.
errornullnullUnrecoverable 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:

  • successContent type "<fqn>" is ready in <org>. (post-deploy: Deployed "<fqn>" to <org>. Component IDs: <ids>.)
  • not_deployedContent type "<fqn>" exists locally but is not deployed to <org>. Deploy it before creating content. (or 7b's "deploy later" one-liner)
  • cancelledCancelled. 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 TypeSkill NameAPI ContextUsage Rule
ContentTypeBundleexperience-cms-content-type-generatemetadata-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, Cancelcancelled. 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 pickNext 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.
CancelEmit 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 FQNget_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:

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
1k
Forks
342
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
experience-cms-content-type-generate
Source
github.com/forcedotcom/sf-skills