Apex Metadata API Introduction
The Salesforce Metadata API allows you to perform many org configuration and setup features programatically. Code examples given by Salesforce are in Java as the API is a SOAP API. In respect to the Apex library provided here, it wraps via the providedMetadataService.cls class the Salesforce SOAP version of the API to make it easier to access for Apex developers. Key terms, structures and operations exposed are still the same however, so reading the Salesforce documentation is still important. You should also review the readme and example class
Handling Asynchronous Aspects
The first challenging aspect is the fact that Salesforce provides this API as an asynchronous API. Which requires some additional coding approaches to handle its responses. Effectively polling for the result of the API calls. To make it easier the Apex library includes some examples for doing this in Batch Apex and using Visualforce’s apex:actionPoller component.
IMPORTANT UPDATE: Since Summer’14 (API 31.), Salesforce have removed the Async API’s in the Metadata API, see here for more details. If you are developing against v30.0 or below this information will still be useful to you. Indeed it is also still relevant to the deploy and retrieve operations.
Create Custom Field Example
Here is an example of creating a CustomField using the create operation. First creating an instance of the service which exposes all the operations and configures the authentication, by passing on the users current Session ID. For more information on calling SOAP from Apex, see here.
MetadataService.MetadataPort service = new MetadataService.MetadataPort(); service.SessionHeader = new MetadataService.SessionHeader_element(); service.SessionHeader.sessionId = UserInfo.getSessionId(); MetadataService.CustomField customField = new MetadataService.CustomField(); customField.fullName = 'Test__c.TestField__c'; customField.label = 'Test Field'; customField.type_x = 'Text'; customField.length = 42; MetadataService.AsyncResult[] results = service.create(new List<MetadataService.Metadata> { customField });
The code then needs to inspect the contents of AsyncResult and be prepared to pass it back to the API to poll for the results periodically. If you study the create documentation you will see a good summary of the steps. Basically calling one of the Metadata API operations, receiving the result and if needed repeatedly calling checkStatus.
You can call the checkStatus method from Apex, though you must have your code wait for Salesforce to process the request, either via Batch Apex context or via Visualforce and its AJAX support.
MetadataService.AsyncResult[] results = service.checkStatus(new List<String> { results[0].Id });
Calling checkStatus from Visualforce
If you have an interactive tool your building, you can use Visualforce and use the apex:actionPoller to store the AsyncResult in your controller and write a controller method to call the checkStatus, which the action poller repeatedly calls until the AsyncResult indicates the request is completed by Salesforce.
public with sharing class MetadataController { public MetadataService.AsyncResult result {get;set;} public PageReference createField() { // .. as per above ... result = createService().create(new List<MetadataService.Metadata> { customField })[0]; displayStatus(); return null; } public PageReference checkStatus() { // Check status of the request result = createService().checkStatus(new List<String> { result.Id })[0]; displayStatus(); return null; } private void displayStatus() { // Inspect the AsyncResult and display the result ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.Info, result.done ? 'Request completed' : 'Request in progress...')); if(result.state == 'Error') ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.Error, result.message)); if(result.done) result = null; } }
This controllers page then looks like this…
<apex:page controller="MetadataController"> <apex:form id="form"> <apex:sectionHeader title="Metadata Demo"> <apex:pageMessages> <apex:actionPoller action="{!checkStatus}" interval="5" rerender="form" rendered="{!NOT(ISNULL(Result))}"> <apex:outputPanel rendered="{!ISNULL(Result)}"> <apex:commandButton value="Create Field" action="{!createField}"> <apex:commandButton value="Delete Field" action="{!deleteField}"> </apex:outputPanel> </apex:form> </apex:page>
The demos for the retrieve and deploy operations also provide good examples of this here and here. Even though they don’t actually use the create operation of the Metadata API, polling for the AsyncResult is the same. Note that the two operations they use, retrieve and deploy are still useful for deploying code or retrieving configuration. But do have the added complexity of handling zip file format, something the library also provides some components for.
Calling checkStatus from Batch Apex
This part of the Readme describes a helper class MetadataCreateJob.cls to do this in Batch Apex which actually handles both calling the create and checkStatus methods for you. Sending the final results to an Apex handler class, in this case a default email handler is provided, but you write your own.
// Pass the Metadata items to the job for processing, indicating any dependencies MetadataCreateJob.run( new List<MetadataCreateJob.Item> { new MetadataCreateJob.Item(customField) }, new MetadataCreateJob.EmailNotificationMetadataAsyncCallback());
What org configuration can be access via the Salesforce Metadata API?
First review this topic from the Salesforce documentation. In respect to the CRUD (Create, Update and Delete) operations of the API you can only pass metadata / component types that extend the class Metadata (or MetadataService.Metadata in the Apex API). If you review the MetadataService.CustomField class you can see it does just this…
public class CustomField extends Metadata {
When you review the individual topics for the metadata component types in the Salesforce documentation, pay attention to those that state that they extend the Metadata type.
NOTE: As per the last section of the Readme for the library, it has manually made this modification as the Salesforce WSDL to Apex code generator that initially generated the wrap did not do this. Most of the popular Metadata types have already had this treatment in the Apex API.
November 21, 2019 at 5:02 am
How to write test class for this.
December 28, 2019 at 7:29 pm
Test class is included in the repo
Pingback: Update picklist value and add it in record type using apex – SFDC Parrot
Pingback: Update picklist value and add it in record type using apex – SFDCians
May 8, 2020 at 10:22 am
Hello Andrew,
Basically, I wanted to copy all profile settings from one profile to another profile.
Is this possible using Metadata API? If not then can you inform me which profile settings can be accessed?
I have tried updating Profile tabVisibility using metadata API, it works as expected. (Sometimes I get Read timed out error.)
When I am trying to update fullName and userLicense of the profile, I got “System.CalloutException: IO Exception: Read timed out”. Not able to do this update.
Do you have any idea about this?
I have raised the issue regarding this in git as well – https://github.com/financialforcedev/apex-mdapi/issues/259
May 9, 2020 at 6:45 am
This is very tricky with profiles as they are sooo big. My advice is to use the fact that profiles are actually able to be queried via soql as they are stored as permission sets behind the scenes. Thus you can write some apex code or other to read them… then create another permission set and assign to your users. Permission sets are way better to manage than profiles – you can create many of them for each feature or capability you want to provision and share them via assigning. https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_objects_permissionset.htm
June 2, 2020 at 12:50 am
Hi Andrew
I am using Metadataservice class in my SandboxPostCopy Class to modify custom settings and custom labels but as it is executed by automated process it is throwing exception”INVALID_SESSION_ID” while executing this class after Sandbox refresh. Is there any workaround for this?
Thanks
July 20, 2020 at 8:55 am
Yes – sadly this user does not provide a session id. I do not know of a workaround for a new sandbox – normally would use named credentials – but since those will not point to your new sandbox that is a challange. I do wonder if you used something like this though … not dug into it myself – https://help.salesforce.com/articleView?id=000329017&language=en_US&type=1&mode=1
July 20, 2020 at 8:40 am
Hi Andrew Fawcett, Do you have any example of updating the List view filter values? I have a requirement to dynamically update the list view filter’s value for an object. Thanks!
July 20, 2020 at 9:16 am
Have a look at the examples here https://github.com/financialforcedev/apex-mdapi/blob/master/apex-mdapi/src/classes/MetadataServiceExamples.cls#L797 – there is no update one – but you can look at the other update examples to get the idea. You can see the types involved.
August 20, 2021 at 5:30 am
Hi Andrew! Can I grab tab visibility for Profile (i.e. System Administrator) via mdapi? And do you know other ways for getting tabs visibility in clean Apex? Thanks!
August 26, 2021 at 7:45 pm
Yeah you should be able to query permission set object and it’s child objects to get at this without md api
October 17, 2022 at 5:51 am
Hi Andrew, I was wondering if we can deploy the apex class? If yes, can you share any of the example please? Thanks
December 8, 2022 at 6:39 pm
Yes you can – search for “GitHub dlrs” and find a repo with a RollupController class in it – that is a good example. You have to be very very explicit to your users that your solution is doing this please – permissions are needed to the running user to allow this – but you still have to be upfront what you are deploying. The example I share is a good example of this.