
Hello, and welcome to Another Salesforce Blog! Here I will be posting solutions to problems that I couldn’t find an answer to in hopes of helping those who find themselves stuck when using the Salesforce platform.
User Story
We have a new field on the Account object, and we want to prevent users from being able to edit the Account object while we deploy the new field. To do this, we will be using an ANT script to make the Account object Read Only for all profiles and permission sets, and restoring access when we are done with our changes.
Background
We have already covered the basics of how to do this in my previous post, Mass Updating Profile Object Permissions Via ANT Migration Tool. Unfortunately, manually updating profiles and permission sets is a labor intensive and error prone process. Don’t worry, there’s a better way!
XSLT stands for XSL Transformations, and is an XML styling language. What this means is we can take a chunk of XML code, and transform it into another piece of XML. Per w3Schools, “XSLT transforms an XML source-tree into an XML result-tree.” Basically, much like CSS is a defined template for how HTML is displayed, XSLT defines a template for a transform.
This tutorial assumes that you have the most recent version of Apache ANT installed, instructions for which can be found here. I have my ANT folder installed under my C:\
directory for ease of access.
Solution
We are going to use XSLT to modify existing Account object permissions on profiles to make Accounts Read Only.
Building the files
We will start by retrieving the profiles using Apache Ant. We will need three files, build.properties
, build.xml
, and package.xml
. The package.xml
file will be contained within a folder called retrievepkg
. Additionally, we will have a second folder called deploypkg
, which we will populate with our changes. I have my files located in the folder C:\ant\ASFB - XSLT Tutorial
for ease of access.

If you are not familiar with ANT, you can refer to my previous posts, Getting Started With ANT: Retrieving Metadata via ANT Migration Tool for Salesforce, and Mass Updating Profile Object Permissions via ANT Migration Tool for detailed information on how to create the specified files.
build.properties
The build.properties
file will contain information such as your username, password, package name, and login URLs.
Additionally, we are naming our blackout.package
, restoration.package
, and package.root
filepaths, and naming our transform.xml.xslt
file. We will create this file, named TransformXML.xslt
, in just a moment.
The following build.properties
file is an example of what we would use if we were writing this script for an enterprise organization, so that we could test in Stage before we deploy in Prod. If you are using a developer environment, you can simply specify sf.username
and sf.password
here.
sf.testurl = https://test.salesforce.com
sf.loginurl = https://login.salesforce.com
sf.maxPoll = 600
deployTarget = C:\\ant\\ASFB - XSLT Tutorial\\deploypkg
retrieveTarget = C:\\ant\\ASFB - XSLT Tutorial\\retrievepkg
package.root = C:\\ant\\ASFB - XSLT Tutorial\\
transform.xml.xslt = C:\\ant\\ASFB - XSLT Tutorial\\TransformXML.xslt
# stage
sf.username.stage = USERNAME.stage
sf.password.stage = PASSWORD[SECURITYTOKEN]
# prod
sf.usernameProd = USERNAME
sf.passwordProd = PASSWORD[SECURITYTOKEN]
package.xml
Our package.xml
file, located in the retrievepkg
and deploypkg
folders, will need to pull all profiles and the Account object. It will read as follows:
<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
<types>
<members>Account</members>
<name>CustomObject</name>
</types>
<types>
<members>*</members>
<name>Profile</name>
</types>
<version>36.0</version>
</Package>
build.xml
Next, we must write the series of commands to be executed by ANT. This will include a command to retrieve information from each environment (stage and prod), transform the information, and deploy the transformed information to the specified environment. We will also create two commands to restore the original data.
<project name="AnotherSalesforceBlog" default="test" xmlns:sf="antlib:com.salesforce">
<property file="build.properties"/>
<property environment="env"/>
<taskdef resource="com/salesforce/antlib.xml" uri="antlib:com.salesforce">
<classpath>
<pathelement location="C:\Program Files\ant\ant-salesforce.jar" />
</classpath>
</taskdef>
<target name="getstage">
<sf:retrieve username="${sf.usernameStage}"
password="${sf.passwordStage}"
serverurl="${sf.testurl}"
maxPoll="${sf.maxPoll}"
retrieveTarget="retrievepkg"
unpackaged="retrievepkg\\package.xml" />
</target>
<target name="getprod">
<sf:retrieve username="${sf.usernameProd}"
password="${sf.passwordProd}"
serverurl="${sf.loginurl}"
maxPoll="${sf.maxPoll}"
retrieveTarget="retrievepkg"
unpackaged="retrievepkg\\package.xml" />
</target>
<!--
<target name="transform">
...
</target>
-->
<target name="deployReadOnlyStage">
<sf:deploy username="${sf.usernameStage}"
password="${sf.passwordStage}"
serverurl="${sf.testurl}"
maxPoll="${sf.maxPoll}"
deployRoot="${deployTarget}"
rollbackOnError="true" />
</target>
<target name="deployRestorationStage">
<sf:deploy username="${sf.usernameStage}"
password="${sf.passwordStage}"
serverurl="${sf.testurl}"
maxPoll="${sf.maxPoll}"
deployRoot="${retrieveTarget}"
rollbackOnError="true" />
</target>
<target name="deployReadOnlyProd">
<sf:deploy username="${sf.usernameProd}"
password="${sf.passwordProd}"
serverurl="${sf.loginurl}"
maxPoll="${sf.maxPoll}"
deployRoot="${deployTarget}"
rollbackOnError="true" />
</target>
<target name="deployRestorationProd">
<sf:deploy username="${sf.usernameProd}"
password="${sf.passwordProd}"
serverurl="${sf.loginurl}"
maxPoll="${sf.maxPoll}"
deployRoot="${retrieveTarget}"
rollbackOnError="true" />
</target>
</project>
For now, we will leave the transform command commented out, and we will come back to it in a moment. First, we need to write our .xslt
file.
TransformXML.xslt
First, we must declare that this XSL file will be a transform, and we must declare our namespaces, which include the XSL Transform namespace and the Salesforce Metadata namespace:
<xsl:transform version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:m="http://soap.sforce.com/2006/04/metadata">
...
</xsl:transform>
Next, we will define the layout of our transform XML document:
...
<xsl:strip-space elements="*"/>
<xsl:output omit-xml-declaration="no" method="xml" indent="yes" />
...
The xsl:strip-space
element is used to remove white-space only nodes, while the xsl:output
element defines the format of the output document. We are transforming one XML document into another, new XML document. We do want the XML declaration at the top of our file, and we do want the output to be indented according to its hierarchic structure.
We want our template to match the entire document, so we use an XPath expression to define the template for the entire document, xsl: template match="/"
. We want our template to be applied to all children of the root node, so we use the xsl:apply-templates
element with another XPath expression, select="node()|@*"
. This matches a node of any kind (node
), allows for several paths to be selected (|
), and matches any attribute node (@*
). Effectively, this will iterate through the whole document.
...
<xsl:template match="/">
<xsl:apply-templates select="node()|@*" />
</xsl:template>
...
Next, we want to change all editable Field Permissions to “false.” This will effectively make our Accounts read only.
Let’s break this down.
In the XML for a profile, the field permissions look like this:

What we are looking at is a child element (fieldPermissions
) of the Profile
element with three child elements (editable
, field
, and readable
). We want to access a subchild of the root element (Profile
>fieldPermissions
>editable
) and change it to false.
We do this by writing another match
and invoking the namespace “m” for our Salesforce metadata namespace:
...
<xsl:template
match="m:fieldPermissions/m:editable">
<xsl:element name="{name()}">false</xsl:element>
</xsl:template>
...
This looks in the XML input document for fieldPermissions
elements, looks for the child elements editable
, and dynamically retrieves gets the name of the output element using the name()
function. The value of the element is changed to false
.
Next, we want to do the same thing for object permissions.

Again, this is a child element of the Profile
element. We want to change the allowCreate
, allowDelete
, and allowEdit
child elements to false
so that the Account object cannot be modified while we are deploying the new field.
This will look similar to the last step, but because there are multiple child objects, we need to add an XPath expression (|
) to allow for multiple paths to be selected:
...
<xsl:template match="m:objectPermissions/m:allowCreate|m:objectPermissions/m:allowEdit|m:objectPermissions/m:allowDelete|m:objectPermissions/m:modifyAllRecords">
<xsl:element name="{name()}">false</xsl:element>
</xsl:template>
...
Lastly, we need to include an <xsl: copy>
element to create a node with the same name, namespace, and type as the current node. Per Microsoft, this element makes identity transformation possible.
...
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*" />
</xsl:copy>
</xsl:template>
...
All together, our TransformXML.xslt
document will look like this:
<xsl:transform version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:m="http://soap.sforce.com/2006/04/metadata">
<xsl:strip-space elements="*"/>
<xsl:output omit-xml-declaration="no" method="xml" indent="yes" />
<xsl:template match="/">
<xsl:apply-templates select="node()|@*" />
</xsl:template>
<xsl:template
match="m:fieldPermissions/m:editable">
<xsl:element name="{name()}">false</xsl:element>
</xsl:template>
<xsl:template match="m:objectPermissions/m:allowCreate|m:objectPermissions/m:allowEdit|m:objectPermissions/m:allowDelete|m:objectPermissions/m:modifyAllRecords">
<xsl:element name="{name()}">false</xsl:element>
</xsl:template>
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*" />
</xsl:copy>
</xsl:template>
</xsl:transform>
Package.xml – transform command
Back to our package.xml
document. Let’s take a look at our transform:
<target name="transform">
...
</target>
What we need to do is apply our TransformXML.xslt
document to every profile in our retrievepkg
folder (except for our Admin profiles, don’t want to lock ourselves out!).
<target name="transform">
<xslt
in="${retrieveTarget}\\profiles\\PROFILENAME.profile"
out="${deployTarget}\\profiles\\PROFILENAME.profile"
style="${transform.xml.xslt}">
</xslt>
...
</target>
This takes our “in” file (a profile in the retrievepkg
folder), applies the transform, and outputs it as a profile in the deploypkg
folder. We would complete this for every profile that we want to update in our org.
For example, if I wanted to update the Analytics Cloud Integration User profile and the Analytics Cloud Security User profile, my transform command would look like this:
<target name="transform">
<xslt
in="${retrieveTarget}\\profiles\\Analytics Cloud Integration User.profile"
out="${deployTarget}\\profiles\\Analytics Cloud Integration User.profile"
style="${transform.xml.xslt}">
</xslt>
<xslt
in="${retrieveTarget}\\profiles\\Analytics Cloud Security User.profile"
out="${deployTarget}\\profiles\\Analytics Cloud Security User.profile"
style="${transform.xml.xslt}">
</xslt>
</target>
We can add this back into our package.xml
file, yielding the following:
<project name="AnotherSalesforceBlog" default="test" xmlns:sf="antlib:com.salesforce">
<property file="build.properties"/>
<property environment="env"/>
<taskdef resource="com/salesforce/antlib.xml" uri="antlib:com.salesforce">
<classpath>
<pathelement location="C:\Program Files\ant\ant-salesforce.jar" />
</classpath>
</taskdef>
<target name="getstage">
<sf:retrieve username="${sf.usernameStage}"
password="${sf.passwordStage}"
serverurl="${sf.testurl}"
maxPoll="${sf.maxPoll}"
retrieveTarget="retrievepkg"
unpackaged="retrievepkg\\package.xml" />
</target>
<target name="getprod">
<sf:retrieve username="${sf.usernameProd}"
password="${sf.passwordProd}"
serverurl="${sf.loginurl}"
maxPoll="${sf.maxPoll}"
retrieveTarget="retrievepkg"
unpackaged="retrievepkg\\package.xml" />
</target>
<target name="transform">
<xslt
in="${retrieveTarget}\\profiles\\Analytics Cloud Integration User.profile"
out="${deployTarget}\\profiles\\Analytics Cloud Integration User.profile"
style="${transform.xml.xslt}">
</xslt>
<xslt
in="${retrieveTarget}\\profiles\\Analytics Cloud Security User.profile"
out="${deployTarget}\\profiles\\Analytics Cloud Security User.profile"
style="${transform.xml.xslt}">
</xslt>
</target>
<target name="deployReadOnlyStage">
<sf:deploy username="${sf.usernameStage}"
password="${sf.passwordStage}"
serverurl="${sf.testurl}"
maxPoll="${sf.maxPoll}"
deployRoot="${deployTarget}"
rollbackOnError="true" />
</target>
<target name="deployRestorationStage">
<sf:deploy username="${sf.usernameStage}"
password="${sf.passwordStage}"
serverurl="${sf.testurl}"
maxPoll="${sf.maxPoll}"
deployRoot="${retrieveTarget}"
rollbackOnError="true" />
</target>
<target name="deployReadOnlyProd">
<sf:deploy username="${sf.usernameProd}"
password="${sf.passwordProd}"
serverurl="${sf.loginurl}"
maxPoll="${sf.maxPoll}"
deployRoot="${deployTarget}"
rollbackOnError="true" />
</target>
<target name="deployRestorationProd">
<sf:deploy username="${sf.usernameProd}"
password="${sf.passwordProd}"
serverurl="${sf.loginurl}"
maxPoll="${sf.maxPoll}"
deployRoot="${retrieveTarget}"
rollbackOnError="true" />
</target>
</project>
Copy the objects folder from retrievepkg to deploypkg
In order to deploy all of the pieces in the package.xml
file, we must copy the objects
folder from retrievepkg
to deploypkg
. We are also able to remove the Account object from the package.xml
file in the deploypkg
folder, but I prefer to deploy everything just to be safe.
Calling the commands
Finally, we are able to execute our commands.
Open up a command line and navigate to the folder with your build.xml
file. Call the command ant getstage
to retrieve profile metadata from your organization.
Next, call ant transform
to transform your file to make the Account object Read Only for the profiles specified in your transform command.
After the file has been transformed, we are able to call the ant deployReadOnly
command for the environment we specify. This will deploy the transformed profiles in the deploy pkg
folder to our environment.
Lastly, when we want to restore access to the Account object for the specified profiles, we call the ant deployRestoration
command for our environment. This will deploy the profiles in our retrievepkg
folder, or the folders that we got directly from Salesforce, restoring the original access.
Thanks for reading, let me know if you have any comments or questions!
-Evelyn, Another Salesforce Blog

Make a one-time donation
Make a monthly donation
Make a yearly donation
Choose an amount
Or enter a custom amount
Help keep Another Salesforce Blog on the internet by donating today!
Your contribution is appreciated.
Your contribution is appreciated.
DonateDonate monthlyDonate yearly