Custom Metadata records can be created, updated and deleted synchronously using Apex web services. This is simpler and more responsive for interactive scenarios, instead of using the native utility which requires asynchronous callback handlers.

This custom class supports the creation of Custom Metadata records from Apex code. It calls the Metadata API synchronously with immediate results for each record.

Example usage:

First paste the CustomMetadataClient.cls into your org.

Then call the methods to manipulate MDT records.

Unit tests here: CustomMetadataClientTest.cls

//create a metadata record
Database.UpsertResult result = CustomMetadataClient.upsertMetadata(
    Custom__mdt.SObjectType,
    new Map<SObjectField,Object>{
        Custom__mdt.DeveloperName => 'Developer_Name',
        Custom__mdt.MasterLabel => 'Master Label',
        Custom__mdt.Position__c => 3
    }
);
//delete a metadata record
Database.DeleteResult result = CustomMetadataClient.deleteMetadata(
    Custom__mdt.SObjectType,
    new List<String>{'Developer_Name'}
);

How to construct the records?

Use a Map<SObjectField,Object> to represent each record. This is more verbose than constructing a normal SObject, but necessary because Apex complains that Custom__mdt fields are not writeable.

What does each metadata operation return?

An instance of Database.UpsertResult is returned by upserts, and an instance of Database.DeleteResult is returned by deletes.

We actually coerce the responses back into these native types so they make MDT operations look and feel more like normal SObject operations.

  • getId() is the FullName (as opposed to the ID)
  • getErrors() lists details of any error messages
  • isSuccess() indicates if each record succeeded
  • isCreated() indicates new vs existing records

The intent is to provide the Metadata API response in familiar terms without introducing a new type.

Caveats

Note with all API operations, a remote site setting is needed.

HTTP callouts are not transactions and cannot be rolled back!







Very nice post! I think, some changes needed with given code (in gist [error at getter of protocolAndHost] and in post at DeleteMetadata() code, actual code is expecting String; however here you have passed List of Strings).

Thanks Yasar, fixed.

Nice! I've used the AndyintheCloud Custom Metadata Services github package but that is asynchronous so this is a big improvement.
However, don't the Mock inner classes need to be public so one's testmethods can use them?

Thank you. The private response mocks are called by the main code instead of injecting public ones, as this works with namespace isolation. It helps when running tests in a managed package: https://salesforce.stackexchange.com/a/18217

Super Helpful, thanks for making this. Is there any way to use this with a Named Credential rather than a session ID? I tried editing the MetadataClient class and setting '{!$Credential.OAuthToken}' as this.SessionHeader.sessionId, as well as changing the endpoint to 'callout:MY_NAMEDCRED_HERE/services/Soap/m/42.0', but have not been able to get it to work. I consistently get the following error code. Any ideas would be appreciated.

INVALID_SESSION_ID: Invalid Session ID found in SessionHeader: Illegal Session. Session not found, missing session hash

I tried this as you described in a v54 sandbox and it worked alright. Possibly there is an issue with the named credential or the org security settings. Some ideas... hope this helps

- double check Allow Merge Fields in HTTP Body = true
- check the constructor is not overwriting this.SessionHeader.sessionId
- as SOAP callout does not refresh the session, try a REST callout prior like this:

// refresh the session if needed
HttpRequest request = new HttpRequest();
request.setEndpoint('callout:MY_NAMEDCRED_HERE/services/data/v42.0/limits');
request.setMethod('GET');
new Http().send(request);

// then call web service
CustomMetadataClient.upsertMetadata(...)

Austin Elwell  

Thanks, it is working now. I'm not sure exactly what the issue was. I changed my Named Credential URL as a test, but that didn't work, so I changed it right back, and then it started working. I'm 99% sure I put back the exact same URL, since I only adjusted it slightly so it was easy to undo, and I've been using that URL successfully with other callouts so I don't think there were any typos. Anyway, I appreciate you taking the time to help, thanks again.