references/cmdt-records.md
# CMDT Records — Value Typing, Coercion, and Error Reference
Depth reference for `customMetadata/<Type>.<Record>.md-meta.xml`. Read this when a record deploy fails, or
when deciding whether an `xsi:type` mismatch is actually a problem.
Every error string in Section 3 is verbatim from `componentFailures[].problem` in
`sf project deploy start --json`.
---
## 1. The type check is asymmetric
There is **no single "types must match" rule.** Some mismatches are silently accepted, others are rejected.
Knowing which prevents both false alarms and missed bugs.
| Field type | `xsi:type` used | Outcome |
|---|---|---|
| Number | `xsd:double` | accepted (canonical) |
| Number | `xsd:int` | **accepted** |
| Number | `xsd:string` (`42`) | **accepted** |
| Number | `xsd:boolean` | rejected — `S Number: value not of required type: true` |
| Number | `xsd:double` with value `abc` | rejected at parse — `Error parsing file: 'abc' is not valid for the type xsd:double` |
| Date | `xsd:date` | accepted (canonical) |
| Date | `xsd:dateTime` | **accepted** |
| Date | `xsd:string` | rejected — `S Date: value not of required type: 2024-01-15` |
| DateTime | `xsd:dateTime` | accepted (canonical) |
| DateTime | `xsd:date` | **accepted** |
| DateTime | `xsd:string` | rejected — `S DateTime: value not of required type: 2024-01-15T10:30:00.000Z` |
| Checkbox | `xsd:boolean` | accepted (canonical) |
| Checkbox | `xsd:string` | rejected — `S Checkbox: value not of required type: true` |
| Checkbox | `xsd:int` | rejected — `S Checkbox: value not of required type: 1` |
| Text | `xsd:string` | accepted (canonical) |
| Text | `xsd:double` | rejected — `S Text: value not of required type: 42.5` |
**Summary of the pattern:** `Number` accepts `xsd:string` and `xsd:int`; Date and DateTime interconvert
freely; **Checkbox and Text are strict.** The most common real-world failure is a Date field given
`xsi:type="xsd:string"`. Only `Number` was probed among the numeric types — do not assume `Percent` behaves
identically.
Always emit the canonical type from the main skill's Section 4 table. The tolerances above are for
diagnosing existing files, not a licence to be loose in generated output.
### `Percent`, `Email`, `Phone`, `Url`, `TextArea`, `LongTextArea`
Their canonical types are confirmed correct by a successful all-fields record deploy, but they were not
individually probed with *wrong* types, so no per-field verbatim rejection string exists. The
`value not of required type` shape is expected to hold, since it held for Date, DateTime, Number, Checkbox,
and Text.
---
## 2. `xsd:picklist` — the documented type that must not be emitted
Salesforce's documentation lists `xsd:picklist` for Picklist fields. It is **not a valid XML Schema type**,
and the platform does not reject it cleanly: instead of a component-level validation error it fails the whole
deploy server-side with no component diagnostics, taking every other component in the deploy down with it.
**Always emit `xsi:type="xsd:string"` for a Picklist field's value; never `xsd:picklist`.** This is the
canonical typing from the main skill's Section 4 table, so following the generation rule avoids the failure
entirely.
---
## 3. Full record error catalog
| What was wrong | VERBATIM error string |
|---|---|
| Date field with `xsi:type="xsd:string"` | `S Date: value not of required type: 2024-01-15` |
| Checkbox field with `xsi:type="xsd:string"` | `S Checkbox: value not of required type: true` |
| Checkbox field with `xsi:type="xsd:int"` | `S Checkbox: value not of required type: 1` |
| Number field with `xsi:type="xsd:boolean"` | `S Number: value not of required type: true` |
| DateTime field with `xsi:type="xsd:string"` | `S DateTime: value not of required type: 2024-01-15T10:30:00.000Z` |
| Text field with `xsi:type="xsd:double"` | `S Text: value not of required type: 42.5` |
| `xsd:double` with non-numeric text | `Error parsing file: 'abc' is not valid for the type xsd:double` |
| Restricted Picklist, value not in the value set | `S Picklist: bad value for restricted picklist field: NotAValidEntry` |
| Text(255) given 300 characters | `S Text: data value too large: AAAA…AAAA (max length=255)` |
| `<value>` with no `xsi:type` | `Error parsing file: Only the following configurations are supported : 1) if the type is mentioned i.g. type="xsi:boolean" 2) With metadata CRUD API with fullName required` |
| `<value xsi:nil="true"/>` on a **required** field | `Required fields are missing: [ReqText__c]` |
| Required field omitted entirely | `Required fields are missing: [ReqText__c]` |
| `<label>` omitted | `Required fields are missing: [MasterLabel]` |
| `<fullName>` inside a `<values>` block | `Error parsing file: Element {http://soap.sforce.com/2006/04/metadata}fullName invalid at this location in type CustomMetadataValue` |
| `<field>` naming a nonexistent field | `Probe01__mdt: could not find fields: Does_Not_Exist__c` |
| Record whose type doesn't exist and isn't in the deploy | `Custom metadata type Probe99__mdt is not available in this organization.` |
| DeveloperName starting with a digit, containing a space, or containing a hyphen | `Custom Metadata Record Name: The Probe01__mdt API Name can only contain underscores and alphanumeric characters. It must be unique, begin with a letter, not include spaces, not end with an underscore, and not contain two consecutive underscores.` |
| DeveloperName over 40 characters | `Value too long for field: fullName maximum length is:40` |
### Exact-text notes
These are easy to get wrong when matching strings:
- **Errors name the field by its Label, not its API name.** `S Date: …` where the API name is `S_Date__c`.
Map errors back to files using the label, or the `fullName`/`fileName` on the failure object.
- The no-`xsi:type` error has **two spaces** after `Error parsing file:`, a **space before the colon** in
`supported :`, and the typo `i.g.` rather than `e.g.`. Confusingly, the example it prints is
`type="xsi:boolean"` even though the attribute you must write is `xsi:type="xsd:boolean"`.
- The `fullName invalid at this location` error has **one** space after `Error parsing file:` and no
trailing period.
- The length error is the literal string `S Text: data value too large: `, then the full untruncated value,
then ` (max length=255)`.
- The DeveloperName error names the type **with** the `__mdt` suffix even though the filename omits it.
- `maximum length is:40` has **no space** after the final colon. The limit is on the record DeveloperName.
- `Required fields are missing: [MasterLabel]` reports the sObject field name, not the metadata element
name `label`.
---
## 4. Deploy ordering — precise
**There is no ordering constraint between a type and its records.** The only rule is that the type and every
referenced field must *resolve* — either already present in the org, or present in the same deploy.
| Case | Result |
|---|---|
| Records alone, type and fields already in the org | **Succeeds** |
| Type + fields + records in one deploy | **Succeeds** |
| Records alone, type does not exist anywhere | **Fails** — `Custom metadata type <Type>__mdt is not available in this organization.` |
The common claim that "the type must be in the same deploy as the records" is **not** a platform rule.
Including the type is one way to satisfy resolution; deploying it earlier is equally valid. That claim
usually comes from hitting the third row.
Two distinct errors distinguish the failure modes:
- `Custom metadata type <Type>__mdt is not available in this organization.` — the **type** is missing.
- `<Type>__mdt: could not find fields: <Field>__c` — the type exists, the **field** is missing.
---
## 5. Other record body rules
- `<protected>` is optional and defaults to `false`. The platform emits `<protected>false</protected>` on
retrieve.
- `<value xsi:nil="true"/>` is what the platform itself emits for every unpopulated field on retrieve, so it
is the canonical way to represent a null. On a required field it is rejected exactly as if the field were
absent.
- Omitting `xsi:type` on a non-nil value is **always** fatal, including for string fields.
- Deploys are atomic. When a batch mixes good and bad components, the good ones appear in
`componentSuccesses` but are still rolled back — a component listed as a success did not necessarily
persist.
- `fileName` in the failure payload is the **MDAPI** path (`customMetadata/X.md`, `objects/Y.object`), never
the source-format path you wrote. Do not pattern-match it expecting `-meta.xml`.
---
## 6. Filename convention — why the no-suffix form wins
Both `Type__mdt.Record.md-meta.xml` and `Type.Record.md-meta.xml` deploy successfully and create real
records, so the `__mdt` suffix is tolerated rather than required.
Salesforce's own tooling settles which to write. After records deployed under **both** spellings existed in
the org, `sf project retrieve start --metadata CustomMetadata` wrote:
```text
force-app/main/default/customMetadata/Probe01.Rec1.md-meta.xml
force-app/main/default/customMetadata/Probe01.Rec2.md-meta.xml
```
The record originally deployed *with* the suffix came back **without** it. Retrieve normalizes to the
no-suffix form, so writing the suffixed form guarantees git churn the first time anyone retrieves.
The MDAPI `fileName` in the deploy payload agrees: `customMetadata/Probe01.Rec1.md`.
Note that the platform still *reports* the type with the suffix in error text — a record file named
`Probe99.Orphan.md-meta.xml` errors with `Custom metadata type Probe99__mdt is not available in this
organization.`
---
## 7. Record DeveloperName sanitization — the deterministic transform
The DeveloperName is the second filename segment (`<Type>.<DeveloperName>.md-meta.xml`) and the key passed to
`getInstance('<DeveloperName>')`. The per-label transform is a pure function — the same label always yields
the same name (no divergence) — but it is **many-to-one**: different labels can map to the same name, so a
per-batch uniqueness check (step 7) is required on top of it. Apply these steps **in order**:
1. Replace every run of non-alphanumeric characters (spaces, punctuation, accents) with a single `_`.
2. Strip any leading `_`.
3. If the result now starts with a digit, prefix `X` (Salesforce's own convention for number-leading names).
4. Strip any trailing `_`.
5. Truncate to 40 characters, then strip a trailing `_` the cut may have introduced.
6. If the result is empty (an all-symbol or non-Latin label folds to nothing), emit the deterministic
fallback `Record_<rowIndex>` (1-based source position) — never an empty name, and never a free-handed literal.
7. Across the **whole batch**, enforce uniqueness (see below) — steps 1–6 guarantee a *valid* name, not a
*distinct* one.
Canonical implementation — invoke the skill-local script (same `$LABEL` at a given `$ROW` → same valid name;
the empty-guard lives **in the script**, not in prose):
```bash
scripts/sanitize-developer-name.sh "$LABEL" "$ROW"
```
Equivalent inline form of what the script runs:
```bash
name=$(printf '%s' "$LABEL" \
| sed -E 's/[^A-Za-z0-9]+/_/g; s/^_+//; s/^([0-9])/X\1/; s/_+$//' \
| cut -c1-40 | sed -E 's/_+$//')
[ -z "$name" ] && name="Record_$ROW" # all-symbol / non-ASCII label collapsed to empty; $ROW = 1-based source position
printf '%s' "$name"
```
Worked examples:
| Source label | DeveloperName |
|---|---|
| `United States` | `United_States` |
| `Gold (EMEA)` | `Gold_EMEA` |
| `2024 Q1` | `X2024_Q1` |
| `ACME — West` | `ACME_West` |
| `Côte d'Ivoire` | `C_te_d_Ivoire` |
Every output satisfies the "Record DeveloperName rules" in the skill: letter-first, alphanumerics and single
underscores only, no leading/trailing underscore, ≤ 40 characters.
**The transform is ASCII-only and lossy for accented / non-Latin labels.** Step 1 folds any character outside
`[A-Za-z0-9]` to `_` — it does **not** transliterate — so `Côte d'Ivoire → C_te_d_Ivoire` (the `ô` becomes
`_`) and `中文 → Record_<rowIndex>` (via the step-6 fallback). Because the DeveloperName is the
`getInstance('<name>')` API key, this is lossy and collision-prone. When a label set relies on non-ASCII
distinctions, transliterate to ASCII first (e.g. `Côte → Cote`) or have the user supply explicit
DeveloperNames rather than deriving them.
### Uniqueness across the batch (step 7)
Steps 1–6 are **many-to-one**. `United States` and `United-States` both collapse to `United_States` (step 1
folds every punctuation/accent run to a single `_`), and step 5's 40-char cut makes any two labels sharing
their first 40 sanitized characters collide as well. Because each record is written to
`<Type>.<DeveloperName>.md-meta.xml`, a collision **silently overwrites** the earlier row — you lose a record
with no deploy error.
So after deriving names for **all** rows, detect duplicates before writing any file:
```bash
# pairs.tsv = one line per source row, "<derived_name>\t<source_label>", in source order
# Print each derived name produced by more than one label, with the source labels that collided:
sort pairs.tsv | awk -F'\t' '{g[$1]=g[$1] (g[$1]?", ":"") $2; n[$1]++} END{for (k in n) if (n[k]>1) print k" <= "g[k]}'
# e.g. United_States <= United States, United-States
```
- **Nothing printed** → names are distinct; write the files.
- **Anything printed** → do **not** overwrite. Default action: **stop and report the colliding source labels**
(e.g. `"United States" and "United-States" both → United_States`) so the user renames or supplies distinct
keys. The DeveloperName is an API key used in `getInstance('<DeveloperName>')`, so silently renaming it can
break consuming code — surface it rather than guess.
- **Only if the user asks you to auto-resolve**, append a deterministic, length-aware suffix: keep the first
occurrence (in source order) unchanged, and for each later collision append `_2`, `_3`, … after truncating
the base so `base + suffix ≤ 40`. Source order + fixed suffixes keep it deterministic — the same batch
always produces the same names.
Collision example (needs step 7): `United States` → `United_States` and `United-States` → `United_States`;
after auto-resolution → `United_States` and `United_States_2`.
SKILL.md
---
name: platform-custom-metadata-type-generate
description: "Use this skill when users need to create, generate, or validate Salesforce Custom Metadata Type metadata — the __mdt object, its fields, and its deployable records. Trigger when users mention custom metadata types, CMDT, __mdt objects, custom metadata records, .md-meta.xml files, or reference/config data that must ship between orgs. Also trigger for admin-maintained mapping, lookup, or crosswalk tables and field mappings (\"map fields from A to B\", code lookups) — config admins change without a code deploy belongs in a CMDT, never hardcoded in Apex or Flow — and when troubleshooting CMDT record/type deploy errors. DO NOT TRIGGER for Custom Settings — route to platform-custom-setting-generate; for regular custom objects holding business records — use platform-custom-object-generate; or for secrets and API keys — recommend a Named Credential, which this skill never generates."
metadata:
version: "1.0"
minApiVersion: "60.0"
domains: ["Platform"]
relatedSkills:
- "platform-custom-field-generate"
- "platform-custom-object-generate"
- "platform-custom-setting-generate"
cliTools:
- tool: ["sf"]
semver: ">=2.136.8"
---
# Salesforce Custom Metadata Type Generator and Validator
## When to Use This Skill
Use this skill when you need to:
- Create a Custom Metadata Type (`__mdt`)
- Generate or validate CMDT fields, including `MetadataRelationship` fields
- Generate CMDT **records** as deployable `.md-meta.xml` files
- Troubleshoot CMDT deployment errors
---
## 1. Overview and Purpose
A Custom Metadata Type produces **two separate artifact families**, and most requests need both:
| Artifact | Path | Metadata type |
|---|---|---|
| Type definition | `objects/<Name>__mdt/<Name>__mdt.object-meta.xml` | `CustomObject` |
| Fields | `objects/<Name>__mdt/fields/<Field>__c.field-meta.xml` | `CustomField` |
| Records | `customMetadata/<Name>.<Record>.md-meta.xml` | `CustomMetadata` |
**API name suffix:** `__mdt` on the type; fields still end in `__c`.
**The defining advantage over a custom setting: CMDT records are metadata and therefore deploy between
orgs.** When a user says configuration should "ship with the package" or "be the same in every org," CMDT
is the right answer.
> **The root element is `<CustomObject>`, but almost none of a custom object's rules apply.** `sharingModel`,
> `nameField`, and `deploymentStatus` are required or normal on a regular custom object and are **hard
> errors** here. Do not carry assumptions across from `platform-custom-object-generate`.
---
## 2. Syntactic Essentials — Type Definition (Tier 1)
### Required and Allowed Elements
| Element | Requirement | Notes |
|---------|-------------|-------|
| `<label>` | **Required** | Singular UI name |
| `<pluralLabel>` | **Required** | Omitting it gives `Must specify a non-empty plural label for the CustomObject` |
| `<visibility>` | Always include | `Public`, or `Protected`/`PackageProtected` only in dev/sandbox/scratch (Section 6) |
| `<description>` | Always include | What this type configures and who owns it |
`<pluralLabel>` being **required here but forbidden on a custom setting** is the most commonly inverted
rule between the two families. Neither failure mentions the other family's rule.
### Forbidden Elements
Every element below produces `Cannot specify: <element> for Custom Metadata Type` — reusing a regular custom
object's skeleton (with `sharingModel`, `deploymentStatus`, a `nameField` block, or `enableSearch`) is the
usual cause:
`sharingModel`, `nameField`, `deploymentStatus`, `enableActivities`, `enableReports`, `enableHistory`,
`enableSearch`
**CORRECT** — minimum valid `__mdt` type:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
<label>Partner Tier</label>
<pluralLabel>Partner Tiers</pluralLabel>
<description>Discount and threshold configuration per partner tier. Ships with the package; edited by the revenue ops team.</description>
<visibility>Public</visibility>
</CustomObject>
```
---
## 3. Field Rules
**This skill owns the CMDT-specific deltas only** — the type allowlist, `fieldManageability`, and
`MetadataRelationship` below. For generic field mechanics (`<fullName>` derivation, `<label>`,
`<description>`, `<inlineHelpText>`, precision/scale, `<length>`, `visibleLines`), follow
`platform-custom-field-generate`.
### Supported Field Types
`Checkbox`, `Date`, `DateTime`, `Email`, `Number`, `Percent`, `Phone`, `Picklist`, `Text`, `TextArea`,
`LongTextArea`, `Url`, plus `MetadataRelationship`.
### Unsupported Field Types
`Currency`, `AutoNumber`, `MasterDetail`, `Summary`, `Location`, `Time`, `EncryptedText`, `Html`,
`MultiselectPicklist`. Each fails with a precise, well-formed error:
```text
Type {TypeName} of CustomMetadataField {Object}__mdt.{Field}__c is not supported for the Entity {Object}__mdt
```
(There is no trailing period.) Example: `Type Currency of CustomMetadataField Partner_Tier__mdt.Discount__c is not supported for the Entity Partner_Tier__mdt`
**Currency is the common trap** — it works on a custom setting but not here. Use `Number` with
`<precision>`/`<scale>` and put the currency in the label or help text.
**Formula fields are unsupported**, but they break the pattern above. A `<formula>` element on an otherwise
legal type gives only:
```text
Invalid data type.
```
### `Lookup` is silently coerced — CRITICAL
`<type>Lookup</type>` on a `__mdt` **does not fail** when its `referenceTo` resolves to a real sObject: the
deploy is green and the platform silently rewrites the field to `MetadataRelationship`. A later retrieve
shows a field the user never wrote:
```xml
<type>MetadataRelationship</type> <!-- was deployed as Lookup -->
```
**Always write `MetadataRelationship` explicitly.** Emitting `Lookup` produces a green deploy and a
source-vs-org mismatch that churns in git the first time anyone retrieves. (Reproduced on a live deploy: a
resolvable `Lookup` returns as `MetadataRelationship`; a non-resolvable `referenceTo` is cleanly rejected.)
### `MetadataRelationship`
Three targets, all valid. `referenceTo` selects which.
| `<referenceTo>` | Purpose | Extra requirement |
|---|---|---|
| Another `__mdt` type | Link two custom metadata types | Must be a *different* type |
| `EntityDefinition` | Point at an sObject | None |
| `FieldDefinition` | Point at a field | **Requires `<metadataRelationshipControllingField>`** |
```xml
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Target_Field__c</fullName>
<label>Target Field</label>
<type>MetadataRelationship</type>
<referenceTo>FieldDefinition</referenceTo>
<metadataRelationshipControllingField>Partner_Tier__mdt.Target_Object__c</metadataRelationshipControllingField>
<relationshipLabel>Target Field</relationshipLabel>
<relationshipName>Target_Field</relationshipName>
</CustomField>
```
The controlling field must be an `EntityDefinition` relationship **on the same type**, referenced as
`Type__mdt.Field__c`. Omitting it gives `Metadata relationships to Field Definition require a controlling field.`
**Self-references are impossible.** Pointing a `MetadataRelationship` at its own parent type fails with
`Cannot add a self-lookup relationship child with cascade or restrict options to the object itself` (verbatim
from a live `__mdt` deploy) — use a second type.
### `<fieldManageability>`
**Optional. It defaults to `DeveloperControlled`** — do not add it unless the user wants a different value.
Valid values are `DeveloperControlled`, `SubscriberControlled`, and `Locked`.
It is valid **only** on `__mdt` fields. On a regular custom object field or a custom setting field:
```text
Field manageability cannot be set on this entity.
```
---
## 4. CMDT Records (`.md-meta.xml`)
### Filename convention
Write `customMetadata/<TypeNameWithout__mdt>.<RecordDeveloperName>.md-meta.xml`.
```text
customMetadata/Partner_Tier.Bronze_AMER.md-meta.xml
```
The `__mdt` suffix in the filename also deploys and creates a real record, so it is tolerated — but
Salesforce's retrieve normalizes to the no-suffix form, so always write it without `__mdt` to avoid git
churn (detail in `references/cmdt-records.md`).
### Required namespaces
All three are mandatory on the root element — `xsi:type` on values will not resolve without them.
```xml
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
```
### `xsi:type` mapping — every value needs one
| Field type | Correct `xsi:type` | Example value |
|---|---|---|
| Checkbox | `xsd:boolean` | `true` |
| Date | `xsd:date` | `2024-01-15` |
| DateTime | `xsd:dateTime` | `2024-01-15T10:30:00.000Z` |
| Number, Percent | `xsd:double` | `42.0` |
| Text, TextArea, LongTextArea | `xsd:string` | `some text` |
| Email, Phone, Url | `xsd:string` | `a@b.com` |
| **Picklist** | **`xsd:string`** | `Beta` |
| MetadataRelationship → `EntityDefinition` | `xsd:string` | `Account` |
| MetadataRelationship → `FieldDefinition` | `xsd:string` | `Account.Name` |
| *any type, null* | *no `xsi:type`* — write `<value xsi:nil="true"/>` | |
A value pointing at **another `__mdt` type** via `MetadataRelationship` was not verified — expect
`xsd:string` with the target's DeveloperName, but confirm.
### `xsd:picklist` must not be emitted — CRITICAL
**Salesforce's own documentation tells you to use `xsd:picklist` for Picklist fields. It is wrong** — it is
not a valid XML Schema type. Instead of a clean validation error, the platform fails the entire deploy
server-side with no component-level diagnostics, taking every other component down with it. **Always emit
`xsd:string` for a Picklist field's value; never `xsd:picklist`.** See `references/cmdt-records.md` §2.
### Record body rules
- `<label>` is **required**. Omitting it gives `Required fields are missing: [MasterLabel]` — note it reports
the sObject field name `MasterLabel`, not `label`.
- `<protected>` is optional and defaults to `false`.
- Never put `<fullName>` inside a `<values>` block — it is a hard parse error.
- `<value xsi:nil="true"/>` works on optional fields. On a **required** field it is rejected exactly as if
the field were absent. Omitting `xsi:type` on a non-nil value is always fatal, including for text fields.
**CORRECT** — a complete record:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<label>Bronze AMER</label>
<protected>false</protected>
<values>
<field>Discount_Percent__c</field>
<value xsi:type="xsd:double">5.0</value>
</values>
<values>
<field>Region__c</field>
<value xsi:type="xsd:string">AMER</value>
</values>
<values>
<field>Effective_Date__c</field>
<value xsi:type="xsd:date">2024-01-15</value>
</values>
<values>
<field>Notes__c</field>
<value xsi:nil="true"/>
</values>
</CustomMetadata>
```
### Record DeveloperName rules
The name is the second segment of the filename. It must begin with a letter, contain only alphanumerics and
underscores, not end with an underscore, not contain two consecutive underscores, and be **at most 40
characters**. Violations give:
```text
Custom Metadata Record Name: The <Type>__mdt API Name can only contain underscores and alphanumeric characters. It must be unique, begin with a letter, not include spaces, not end with an underscore, and not contain two consecutive underscores.
```
Over 40 characters gives `Value too long for field: fullName maximum length is:40` (no space after the colon).
### Deploy ordering
**No ordering constraint** — the type and every referenced field only need to *resolve* (already in the org,
or present in the same deploy). Two failures tell you what is missing:
`Custom metadata type <Type>__mdt is not available in this organization.` (the type is missing) and
`<Type>__mdt: could not find fields: <Field>__c` (the type exists but the field does not).
→ Full record-value error catalog and the `xsi:type` coercion asymmetry: **`references/cmdt-records.md`**.
### Generating records from user input
Record data arrives three ways; in each case emit **one `<Type>.<Record>.md-meta.xml` per row**, mapping
columns to `<field>`/`<value>` pairs with the right `xsi:type`:
- **Inline list/table** — map columns straight to fields.
- **CSV** — header = field API names (or map labels to them), each line = one record; take the DeveloperName
from a key column.
- **Prose** — infer the rows ("Bronze at 5%, Gold at 15%") and confirm the field mapping before generating.
Derive every DeveloperName **deterministically** from the key column or label — never free-hand one. For each
row, run `scripts/sanitize-developer-name.sh "<LABEL>" <ROW>` **before** writing its record file (same input →
same valid name; empty/all-symbol/non-Latin labels fall back to `Record_<ROW>`).
The transform is **many-to-one** — `United States` and `United-States` both yield `United_States`, and since
each record is `<Type>.<DeveloperName>.md-meta.xml`, a collision silently overwrites a row. After deriving the
whole batch, check duplicates **before writing**; on any collision **stop and report the colliding source
labels** rather than overwrite (append a deterministic `_2`/`_3` suffix only if the user asks to auto-resolve).
Ordered algorithm, ASCII-only limitation, uniqueness check, and worked examples: **`references/cmdt-records.md` §7**.
### Consuming records in Apex
Records are read through the generated typed class — **no SOQL, no query rows consumed**, safe in loops and
triggers:
```apex
Map<String, Partner_Tier__mdt> all = Partner_Tier__mdt.getAll();
Partner_Tier__mdt bronze = Partner_Tier__mdt.getInstance('Bronze_AMER'); // by DeveloperName
```
**Accessor truncation — CRITICAL for `LongTextArea`.** `getAll()`/`getInstance()` return only the **first
255 characters** of any field; longer `LongTextArea` values are silently truncated for those callers and
must be read via SOQL (`SELECT ... FROM Partner_Tier__mdt`). Say so whenever you put a `LongTextArea` on a
CMDT.
---
## 5. Choosing the Right Component
Before generating, confirm a CMDT is what the user needs.
| If the config is… | Use | Why |
|---|---|---|
| Reference data that must **deploy between orgs** with its records | **CMDT** (this skill) | Records are metadata |
| An admin-maintained **mapping / lookup / crosswalk** table (field↔field, code↔code, "map A to B") | **CMDT** (this skill) | Editable reference data, one record per pair |
| Admin-editable per profile/user, or org-local | `platform-custom-setting-generate` | Values are data and stay in one org |
| Business records users create and edit at runtime | `platform-custom-object-generate` | CMDT records are not transactional data |
| Translatable UI text | Custom Label — not generated here | Labels are the translation surface |
| Credentials, API keys, tokens | Named Credential / External Credential — not generated here | See Section 6 |
| A permission check in Apex or a flow | Custom Permission — not generated here | Boolean access belongs in the permission model |
The distinction that matters most: **CMDT records deploy, custom setting values do not.** If the user needs
admins to edit values per profile at runtime, that is a custom setting, not a CMDT.
> **Field-mapping and lookup tables are a canonical CMDT use case.** When a user asks to "map fields from A
> to B," maintain a code-to-code lookup, or keep a crosswalk admins can edit, model it as a CMDT — one record
> per pair (e.g. `Lead_Field__c`/`Target_Field__c`, or `MetadataRelationship` fields to `FieldDefinition`) —
> **never** a hardcoded Apex `Map`, constant, or Flow decision. Phrasing like "for conversion" or "for our
> integration" does not make it code: if admins maintain the pairs without a deploy, it is CMDT reference data.
---
## 6. Visibility, Secrets, and the No-Silent-Downgrade Rule
### `Protected` and `PackageProtected` are org-type dependent
Both deploy only in a developer, sandbox, or scratch org. Anywhere else:
```text
You can't set the visibility for a Custom Metadata Type to Protected unless you are in a developer, sandbox, or scratch org.
```
`PackageProtected` gives the same string with `PackageProtected` substituted. The failure is confirmed in a
production-like org; that these values *succeed* in a dev, sandbox, or scratch org is taken from the message
text rather than separately tested.
### The no-silent-downgrade rule — CRITICAL
**If `Protected` fails because of the org type, never "fix" it by switching to `Public`.** Stop and tell the
user. A silent downgrade turns a deliberate confidentiality choice into a world-readable component with a
green deploy and no warning — the worst possible outcome, because nothing signals that anything changed.
Report the situation instead: the org does not permit `Protected`, so the options are to deploy to a
dev/sandbox/scratch org or to accept `Public`. Let the user choose.
This applies to any narrowing of visibility, not only this error.
### Secrets do not belong in a CMDT
A CMDT is **not** a secret store. `Protected` restricts access from outside the namespace, but it is not
encryption and it does not protect the value from code or admins inside it.
When a user asks to store an API key, password, token, client secret, or certificate in a CMDT:
1. **Warn prominently and first** — before generating anything — that a CMDT is the wrong home for a
credential, and that `Protected` is not encryption.
2. **Recommend the right component** — a **Named Credential** with an **External Credential**. Say plainly
that this skill does not generate them.
3. **If the user still insists, comply** — generate the CMDT with `Protected` visibility where the org
allows it, and keep the warning in the response. Do not silently refuse, and do not silently obey.
**The two rules compose, and the order matters.** If the user insists on a CMDT for a secret *and* the org
rejects `Protected`, the no-silent-downgrade rule is what prevents the key from landing in a `Public`
component. Stop and report — never downgrade a secret-bearing type to `Public`.
---
## 7. Deployment Error Reference
Every type- and field-level error string is stated inline in Sections 2–4, next to the mistake that causes
it (`Cannot specify: <element> for Custom Metadata Type`, the unsupported-type string, `Invalid data type.`
for a formula, `Field manageability cannot be set on this entity.`, the `MetadataRelationship` controlling-
field and self-lookup errors, and the `Must specify a non-empty plural label` case).
The **full verbatim record-value catalog** — every wrong-`xsi:type` rejection, `Required fields are missing:
[MasterLabel]`, nil-on-required, the DeveloperName rules, and the exact-text traps (field named by Label not
API name, the doubled space and `i.g.` typo in the no-`xsi:type` error, `maximum length is:40` with no
space) — is in **`references/cmdt-records.md`**.
One matching caution: the suffix here is `for Custom Metadata Type` (spaced, title case); custom settings
use `for CustomSettings`. Do not assume a shared template when matching these strings.
---
## 8. Verification Checklist
### Type Definition Checks
- [ ] Are `<label>`, `<pluralLabel>`, and `<visibility>` all present?
- [ ] Is `<description>` present and specific?
- [ ] Are `<sharingModel>`, `<nameField>`, and `<deploymentStatus>` ABSENT?
- [ ] Are `<enableActivities>`, `<enableReports>`, `<enableHistory>`, and `<enableSearch>` ABSENT?
- [ ] Does the directory and filename end in `__mdt`?
### Field Checks
- [ ] Is every field type in the supported list (Section 3)?
- [ ] Is `Currency` absent? (unsupported here — use `Number`)
- [ ] Is every `<formula>` element absent?
- [ ] Is `<type>Lookup</type>` absent, written as `MetadataRelationship` instead? CRITICAL — `Lookup` deploys silently and is rewritten
- [ ] For `FieldDefinition` relationships, is `<metadataRelationshipControllingField>` present?
- [ ] Does no `MetadataRelationship` point at its own parent type?
- [ ] Is `<fieldManageability>` omitted unless a non-default value was requested?
### Record Checks CRITICAL
- [ ] Is the filename `<Type>.<Record>.md-meta.xml` **without** `__mdt`?
- [ ] Are all three namespaces (`xmlns`, `xmlns:xsd`, `xmlns:xsi`) on the root element?
- [ ] Does every non-nil `<value>` carry an `xsi:type`?
- [ ] Is `xsi:type="xsd:picklist"` absent everywhere? (crashes the whole deploy — use `xsd:string`)
- [ ] Does each `xsi:type` match the field type per the Section 4 table?
- [ ] Is `<label>` present on every record?
- [ ] Is `<fullName>` absent from every `<values>` block?
- [ ] Is every required field given a real value rather than `xsi:nil="true"`?
- [ ] Is every record DeveloperName ≤ 40 characters, starting with a letter, no spaces or hyphens?
- [ ] If records came from a batch/list/CSV, are all derived DeveloperNames distinct (checked with `sort | uniq -d`), with any collision reported rather than silently overwriting a row? CRITICAL — a collision loses a record with no deploy error
- [ ] Does every `<field>` name a field that exists on the type or is in this deploy?
- [ ] If records came from a list, CSV, or prose description, is there exactly one file per row with the columns mapped to the right fields?
### Consumption Checks
- [ ] If the user asked how to read the records, was an Apex snippet given using `getAll()` / `getInstance(developerName)`?
- [ ] If any record populates a `LongTextArea`, was the user warned the accessors truncate to 255 characters and that SOQL is needed for the full value?
### Security Checks CRITICAL
- [ ] If the request involves a secret, key, token, or password, was the warning given **before** generating, with a Named Credential recommended?
- [ ] Was `Protected` left intact rather than silently downgraded to `Public` on an org-type failure?
### Component-Choice Checks
- [ ] If admins need to edit values per profile or user at runtime, was `platform-custom-setting-generate` recommended instead?
- [ ] If the data is really transactional business records, was `platform-custom-object-generate` recommended instead?
---
## Reference File Index
| File | When to read |
|------|-------------|
| `references/cmdt-records.md` | Debugging a CMDT record deploy — full verbatim record-value error catalog, the type-coercion asymmetry (which `xsi:type` mismatches the platform silently accepts), and worked record examples |