Salesforce Custom Metadata Types: Apex Configuration Without SOQL Queries
How to declare and read configuration in Salesforce using Custom Metadata Types, accessed in Apex without SOQL queries, plus limits and pitfalls to avoid.
23 Oct 2025, 17:15 UTC

Why Custom Metadata Types matter for Apex configuration
Custom Metadata Types let you store configuration data as deployable metadata that Apex can read without issuing a SOQL query. This means the data does not count toward your SOQL governor limits, making it ideal for feature toggles, environment-specific constants, or any setting that should live alongside your code in version control.
Setting up a Custom Metadata Type
- In Salesforce Setup, go to Custom Metadata Types and click New Custom Metadata Type. Enter a label (e.g., Feature Toggle); the API name becomes FeatureToggle__mdt by default.
- Add fields for your configuration. For a feature flag, add a Checkbox named IsEnabled__c and perhaps a Text field for a description.
- After saving the type, click Manage Records then New. Create a record (e.g., developer name Feature_A) and set IsEnabled__c to true.
- Deploy the type and its records using a Change Set, Ant, Maven, or the Metadata API. Custom Metadata is metadata, not bulk data, so it travels with your code deployment.
Reading the configuration in Apex
Once the type and records exist in an org, Apex can access them directly.
// Retrieve all records into a map keyed by developer name
Map allToggles = FeatureToggle__mdt.getAll();
// Get a single record by developer name
FeatureToggle__mdt featureA = FeatureToggle__mdt.getInstance('Feature_A');
// Reference a field value
if (featureA.IsEnabled__c) {
System.debug('Feature A is enabled');
} else {
System.debug('Feature A is disabled or the record does not exist');
}
The getAll() method returns a map without executing a SOQL query, so your query limit stays intact. The getInstance() method returns a record instance, or an empty instance with default field values if the developer name does not exist.
Limits and practical considerations
- Record limit: Each Custom Metadata type is limited to 1,000 records. If you need more, split the configuration across multiple types or consider Custom Settings.
- Read-only after deployment: Once deployed, UI records appear read-only. Standard DML (insert, update, delete) or SOQL on the type will fail. Changes require a metadata deployment.
- Heap usage: While getAll() avoids a SOQL query, the returned map counts toward the Apex heap limit. With 1,000 small records this is typically fine, but large text fields can approach the limit.
- Naming suffix: Always reference the type name ending in __mdt in Apex (e.g., FeatureToggle__mdt). Omitting the suffix causes a compile-time error.
- Missing records: getInstance() never returns null; it returns an instance with defaults (false for checkboxes, empty string for text). To check existence, use containsKey() on the map from getAll() or verify a unique field value.
Common mistakes to avoid
- Writing SOQL like SELECT IsEnabled__c FROM FeatureToggle__mdt WHERE DeveloperName = 'Feature_A' – Custom Metadata Types do not support SOQL queries; the statement will throw a System.SOQLException.
- Attempting update featureA after retrieving a record – you'll receive System.FinalException: Record is read-only.
- Assuming getInstance('Nonexistent') returns null and dereferencing fields without a null check – this causes a NullPointerException. Instead, check the map from getAll().
- Deploying only the records without the type definition – the deployment fails because the type does not exist in the target org.
Verification steps for a sandbox or scratch org
- Create a Custom Metadata type named TestConfig__mdt with a Text field Value__c.
- Add a record Test_Record__c and set Value__c = 'Hello'.
- Deploy the type and record to the target org via a Change Set.
- In the target org's Developer Console, run this anonymous Apex:
Map m = TestConfig__mdt.getAll();
System.debug('Value: ' + m.get('Test_Record__c').Value__c);
- Open the Debug Log and confirm:
- No SOQL statements appear for the getAll() call.
- The debug output shows Value: Hello.
- To verify the read-only nature, run:
TestConfig__mdt rec = TestConfig__mdt.getInstance('Test_Record__c'); rec.Value__c = 'Changed'; update rec; // Expect System.FinalException: Record is read-only - You should see the System.FinalException message, confirming the record cannot be altered with DML.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.