Force Flow Version Deleter
- abhyash
- Oct 30, 2022
- 3 min read
Updated: Nov 4, 2022
v1 Prepared By: Abhyash Timsina - 30 October 2022
See this post on Medium: https://medium.com/@abhyash/force-flow-version-deleter-8084a0ac1b5c
Motivation: Currently when you have multiple flow versions and the Salesforce limit is 50, it gets tedious to delete one by one. Sometimes you need to delete the flow interview before you delete the flow version. With this solution, from a screen flow, it deletes all obsolete flow versions and flow interviews in one click. Note - This is a personal project and not intended for financial gain. I do understand this is a common issue with many flow developers.
What needs to be solved: We need to have a zip file consisting of 2 files – destructiveChanges.xml and package.xml. The destructiveChanges.xml file should have a loop of {FlowAPIName} + '-' + {ObsoleteVersionNumber}. This zip file should then be deployed to Workbench to perform the delete operation.
destructiveChanges.xml Sample
<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
<types>
<members>FlowAPIName-1</members>
<members>FlowAPIName-2</members>
<members>FlowAPIName-3</members>
<name>Flow</name>
</types>
</Package>
package.xml Sample
<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
<version>56.0</version>
</Package>
My Technical Components

Open-Source Technical Components (used for zip file generation and callout to Workbench)

Salesforce Idea
Special Thanks
Eric Smith – Who built the original idea
Flow (with annotation):

1. Get All Flows

2. Flow Selection Screen:

2a. Flow Selection Screen - What User Sees

3 – Get record details for the selected flow

4. Get all failed flow interviews for the selected flow

6. If yes, delete all failed flow interviews for the selected flow

6. If yes, Delete all failed flow interviews for the selected flow

7. Apex Action – Call the Invocable Method from Flow

8. Confirmation Screen:

8a. Confirmation Screen - What User Sees

Source Code For ForceFlowVersionDeleter Apex Class (with annotation starting with //)
/** * Created by Abhyash Timsina on 30/10/22. */ public with sharing class ForceFlowVersionDeleter {
// allows user to populate these variables from step 7 of the flow
public class flowInput {
// e.g. The flows api name
@InvocableVariable(Required=true)
public String flowName;
// e.g. Latest version of the flow
@InvocableVariable(Required=true)
public Integer latestVersion;
// e.g. 55.0
@InvocableVariable(Required=true)
public Decimal apiVersion;
}
// invocable method that accepts the flow inputs and calls the xmlGenerator method using the flow inputs as parameters
@InvocableMethod(Label='Force Flow Version Deleter Invocable' Description='Delete Old Flow Versions For Flow')
// doesnt return anything
public static void flowVersionsToDelete(flowInput[] requests) {
for (flowInput request : requests) {
// pass variables as parameters to xmlGenerator method xmlGenerator(request.flowName, request.latestVersion, request.apiVersion); } }
// method to generate xml, zip file and send to workbench for deletion public static void xmlGenerator(String flowName, Integer latestVersion, Decimal apiVersion) { // for creation of destructiveChanges xml
String xmlstring = '';
// create DOM document to store XML
DOM.Document doc = new DOM.Document();
// top level string in XML file dom.XmlNode body = doc.createRootElement('Package xmlns="http://soap.sforce.com/2006/04/metadata"', null, null); body.addTextNode('<types>');
// checks what the latest version of the flow is and make a list of all other versions to delete for (Integer i = 1; i < latestVersion; i++) { body.addChildElement('members', null, null).addTextNode(flowName + '-' + i); }
// these is to append the xml file structure as per requirements of destructiveChanges
body.addTextNode('<name>Flow</name>');
body.addTextNode('</types>');
body.addTextNode('<version>' + apiVersion + '</version>');
body.addTextNode('</Package>');
xmlstring = doc.toXmlString();
// replace any bad strings
String finalXMLString = xmlstring.replace('<', '<'); String destructiveChangesXml = finalXMLString.replace('</Package xmlns="http://soap.sforce.com/2006/04/metadata">', ''); // for creation of package.xml, similar process as above
String xmlstring2 = ''; DOM.Document doc2 = new DOM.Document(); dom.XmlNode body2 = doc2.createRootElement('Package xmlns="http://soap.sforce.com/2006/04/metadata"', null, null); body2.addTextNode('<version>' + apiVersion + '</version>'); body2.addTextNode('</Package>'); xmlstring2 = doc2.toXmlString(); String finalXMLString2 = xmlstring2.replace('<', '<'); String packageXml = finalXMLString2.replace('</Package xmlns="http://soap.sforce.com/2006/04/metadata">', ''); // Use Zippex to zip the file with 2 inputs (package.xml and destructiveChanges.xml) Zippex packageZip = new Zippex(); Blob fileData = Blob.valueOf(packageXml); packageZip.addFile('package.xml', fileData, null); fileData = Blob.valueOf(destructiveChangesXml); packageZip.addFile('destructiveChanges.xml', fileData, null); // Future Callout to perform the workbench action async (can't do dml and callout in same method)
workbenchCallout(EncodingUtil.base64Encode(packageZip.getZipArchive())); } @Future(Callout = true) public static void workbenchCallout(String packageZip){ // Create instance of MetadataService object and pass logged in users session Id for Workbench
MetadataService.MetadataPort service = new MetadataService.MetadataPort(); service.SessionHeader = new MetadataService.SessionHeader_element(); service.SessionHeader.sessionId = UserInfo.getSessionId();
// Workbench Deployment Options – only ignoreWarnings and singlePackage is required MetadataService.DeployOptions deploymentOptions = new MetadataService.DeployOptions(); deploymentOptions.allowMissingFiles = false; deploymentOptions.autoUpdatePackage = false; deploymentOptions.checkOnly = false; deploymentOptions.ignoreWarnings = true; deploymentOptions.performRetrieve = false; deploymentOptions.purgeOnDelete = false; deploymentOptions.rollbackOnError = false; deploymentOptions.singlePackage = true; deploymentOptions.testLevel = 'NoTestRun';
// Future Callout to Workbench with the zip file and deploymentOptions, which will then do the delete operation MetadataService.AsyncResult deployResult= service.deploy(packageZip, deploymentOptions); } }
** Some of above could have been built with Lightning Web Components but I have kept it as simple as possible
Package:
For all files used in this project and how to set it up in your org – please request me on LinkedIn.
Comments