The Apex Metadata API (a native Apex wrapper around the Salesforce equivalent) has had a steady increase of followers and questions since it was created in October 2012. Judging by the feedback I have had its enabling quite a few time saving and wizard style solutions in your native applications! This blog introduces a new example using Batch Apex to ‘script’ the creation of custom objects, fields, pages etc. in an org more easily in a none UI context.
The Salesforce Metadata API is an asynchronous API, which means you have to poll it to determine the fate of your requests. At launch I provided some examples of doing this via apex:actionPoller using Visualforce. Recently given some queries on the forums and this blog, I decided to invest a little more in a Batch Apex example. The new MetadataCreateJob class (and test) implements Batch Apex using a custom iterator to process the Metadata items in order and will even wait for proceeding items to complete, to handle dependencies.
The following example (included here in the repo) will perform the following steps with the given name.
- Create a custom object of the given name
- Create 2 custom fields on the object
- Create a Visualforce page that references the two fields using the standard controller
- Email the user once the above is completed, including any errors returned by checkStatus.
Trying out the sample code…
If you want to try out the following example install, MetadataService.cls, MetadataServiceExamples.cls and MetadataCreateJob.cls in your org (test classes are also available). You will also need to add to your Remote Site Settings (under Security Controls), https://na11.salesforce.com (changing na11 to your org instance). Then issue the following command from the Developer Console or Execute Annoynmous from Eclipse.
MetadataServiceExamples.dynamicCreation('Test');
Once you receive the confirmation email, you will then see the following items in your org…
If you repeat the above you should see the following errors (returned from Metadata API’s checkStatus operation) in the email once the job is complete.
The code to perform the above steps is shown below, note how the ‘wait’ parameter is used on the job items to cause the items to be processed only once a dependent proceeding item has been completed.
public static void dynamicCreation(String objectName) { // Define Metadata item to create a Custom Object MetadataService.CustomObject customObject = new MetadataService.CustomObject(); customObject.fullName = objectName + '__c'; customObject.label = objectName; customObject.pluralLabel = objectName+'s'; customObject.nameField = new MetadataService.CustomField(); customObject.nameField.type_x = 'Text'; customObject.nameField.label = 'Test Record'; customObject.deploymentStatus = 'Deployed'; customObject.sharingModel = 'ReadWrite'; // Define Metadata item to create a Custom Field on the above object MetadataService.CustomField customField1 = new MetadataService.CustomField(); customField1.fullName = objectName+'__c.TestField1__c'; customField1.label = 'Test Field 1'; customField1.type_x = 'Text'; customField1.length = 42; // Define Metadata item to create a Custom Field on the above object MetadataService.CustomField customField2 = new MetadataService.CustomField(); customField2.fullName = objectName+'__c.TestField2__c'; customField2.label = 'Test Field 2'; customField2.type_x = 'Text'; customField2.length = 42; // Define Metadata item to create a Visualforce page to display the above field MetadataService.ApexPage apexPage = new MetadataService.ApexPage(); apexPage.apiVersion = 25; apexPage.fullName = objectName.toLowercase(); apexPage.label = objectName + ' Page'; apexPage.content = EncodingUtil.base64Encode(Blob.valueOf( '<apex:page standardController=\''+objectName+'__c\'>'+ '{!' + objectName + '__c.TestField1__c}' + '{!' + objectName + '__c.TestField2__c}' + '</apex:page>')); // Pass the Metadata items to the job for processing, indicating any dependencies MetadataCreateJob.run( new List<MetadataCreateJob.Item> { new MetadataCreateJob.Item(customObject), new MetadataCreateJob.Item(customField1, null, true), // Set wait to true, to process after object creation new MetadataCreateJob.Item(customField2), new MetadataCreateJob.Item(apexPage, null, true) // Set wait to true, to process after field creation }, new MetadataCreateJob.EmailNotificationMetadataAsyncCallback()); }
Note: This will email the user when the work is completed. However you can implement your own callback handler if you wish, take a look at the MetadataCreateJob.IMetadataAsyncCallback interface and the email example included.
This is quite a basic example, but starts to illustrate how you could build a more dynamic solution. Perhaps one that takes some form of data input, like a CSV file, which will create the object and fields, before importing the data. Or one that is fired from an Apex Trigger whenever some configuration data in your application is changed?
What will you do with this?
June 27, 2014 at 11:56 am
Hi Andrew Fawcett,
Here how to add my object related pagelayout
MetadataService.Layout layout =
(MetadataService.Layout) service.readMetadata(‘Layout’,
new String[] {”rajesh__’+ objectname+’__c’-objectname Layout’ }).getRecords()[0];//Here only problem i think
My object like:’rajesh__’+ objectname+’__c’ and
Object creation time created page layout name is: objectname Layout
help me……..
June 27, 2014 at 12:57 pm
Are you sure the layout name is correct? Check the name with those listed in the Salesforce Workbench, it can list metadata types.
June 27, 2014 at 1:08 pm
Hi Andrew Fawcett,
Object name:’rajesh__’+ objectName+’__c’//i added but this name also not accepted( __ not accepted)
pagelayoutname :-objectname layout
But object null error will comming.please once add my object name below code
MetadataService.Layout layout =
(MetadataService.Layout) service.readMetadata(‘Layout’,
new String[] { ‘Test__c-Test Layout’ }).getRecords()[0];
System.NullPointerException: Attempt to de-reference a null object
Error is in expression ‘{!createField}’ in component in page rajesh:createfields
Class.rajesh.MetadataController11.updateFieldpageLayout: line 882, column 1
line 882: layout.layoutSections[0]. layoutColumns[0].layoutItems.add(layoutItem);
Class.rajesh.MetadataController11.createField: line 243, column 1
line 243: updateFieldpageLayout();//i call above code using this method
help me …..
June 27, 2014 at 1:29 pm
Hi Andrew Fawcett,
I saw Layout name is like below
objectname__c-objectname Layout
createdById: 00590000002M4B3AAK
createdByName: rajesh k
createdDate: 2014-06-26T08:07:27.000Z
fileName: layouts/objectname__c-objectname Layout.layout
fullName: Ramobj__c-objectname Layout
June 27, 2014 at 1:31 pm
I really want to help you, but the blog post comments are for comments about the blog contents. I will only help you further if you please raise an issue here, https://github.com/financialforcedev/apex-mdapi/issues?state=open.
June 27, 2014 at 1:35 pm
Hi Andrew Fawcett,
please help me this is only last.
June 27, 2014 at 1:36 pm
I have a feeling it won’t be, let’s move this chat over to a GitHub issue, it will be much easier to chat there. Thank you!
June 28, 2014 at 10:27 am
Hi Andrew Fawcett,
How to perform List operation on MetadataService.LayoutItem .In my layout more than one fields are there that’s why attempt to dereference null object error will came
June 28, 2014 at 1:21 pm
Please ask your questions here, https://github.com/financialforcedev/apex-mdapi/issues?state=open. Thank you.
July 5, 2014 at 9:45 am
Please ask this question on GitHub, where I can help more easily. Thanks!
July 1, 2015 at 11:42 am
Hi Andy, the new zip folder does not consist of the MetadataCreateJob.cls. I got this error while trying to work the code for batch.
Error: Compile Error: Invalid type: MetadataCreateJob.Item
July 1, 2015 at 5:06 pm
This class is no longer required as create operation is now a sync process and not async. So the class is not needed. You can remove references to it and instead in your code call the new create method which will perform the operation immediatly.
July 6, 2015 at 1:01 am
Andy,
This approach works fine for synchronous Apex (VF Controller, or anonymous Apex), but if I try to use this in asynchronous apex (Batch Apex), then I get the following error:
Web service callout failed: WebService returned a SOAP Fault: INVALID_SESSION_ID: Invalid Session ID found in SessionHeader: Illegal Session faultcode=sf:INVALID_SESSION_ID faultactor=
Looks like it doesn’t like the Session ID, it gets using UserInfo.getSessionID(), in Batch Apex.
Thanks for your help!
-Haroon
July 6, 2015 at 12:58 pm
You need to pass your session id into the batch Job
July 6, 2015 at 3:51 pm
That worked beautifully, Andy. I have a need to invoke this batch from an email service. When I try that, I run into the same issue. Can you suggest anyway, I can invoke this successfully, from an email service?
Thanks!
July 7, 2015 at 11:56 am
You will need to login again via login Api to get a session, or use an oauth token, or try named credentials. The later I have not tried but could be the best option, might try it this week and blog if it works
July 7, 2015 at 5:02 pm
Thanks, Andrew! Will look forward to that, and try it myself as well.
-Haroon
November 5, 2020 at 3:22 am
Can i know how to build this for metadata picklist field values to sync to other system from batch process . in this blog batch job class show not available.
June 5, 2021 at 7:21 am
This class got deleted from the repo – it can be found by going back through the repo history. I did however find it in a previously branched repo here – https://github.com/dhawalsaiya/exercise/blob/master/classes/MetadataCreateJob.cls