Destructive Changes

sfp handles destructive changes according to the type of package. Here is a rundown on how the behaviour is according to various package types and modes

Unlocked Packages

Salesforce handles destructive changes in unlocked packages / org dependent unlocked packages as part of the package upgrade process.

From the Salesforce documentation (https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_unlocked_pkg_install_pkg_upgrade.htm?q=delete+metadata)

Metadata that was removed in the new package version is also removed from the target org as part of the upgrade. Removed metadata is metadata not included in the current package version install, but present in the previous package version installed in the target org. If metadata is removed before the upgrade occurs, the upgrade proceeds normally. Some examples where metadata is deprecated and not deleted are:

  • User-entered data in custom objects and fields are deprecated and not deleted. Admins can export such data if necessary.
  • An object such as an Apex class is deprecated and not deleted if it’s referenced in a Lightning component that is part of the package.

sfp utilizes mixed mode while installing unlocked packages to the target org. So any metadata that can be deleted is removed from the target org. If the component is deprecated, it has to be manually removed.
Components that are hard deleted upon a version upgrade is found here.

Source Packages

Source packages support destructive changes using folder structure to demarcate components that need to be deleted. One can make use of pre-destructive and `post-destructive folders to mark components that need to be deleted

// Consider a source package feature-management
// with path as src/feature-management

└── feature-management
    ├── main
    ├──── default
    ├────────  <metadata-contents>
    ├── <a data-footnote-ref href="#user-content-fn-1">post-destructive</a>
<strong>    ├────────  <metadata-contents>
</strong>    ├──<a data-footnote-ref href="#user-content-fn-2"> pre-destructive</a>
    ├────────  <metadata-contents>
    └── test

Metadata deployment applies the package contents and its pre/post-destructive changes in one transaction. Flow deletion uses the separate handling described below.

Flow deletion

For source and diff packages, put the Flow metadata file in pre-destructive/flows/ or post-destructive/flows/ inside the package directory. During installation, sfp deactivates the Flow if active and deletes all its versions. No package deployment script is required. A package containing only destructive Flow files also works.

src/feature-management/
  post-destructive/flows/My_Flow.flow-meta.xml

The install log reports each Flow, whether it was deactivated, the number of versions deleted, and the result. If a Flow is already absent, sfp logs a warning and continues. If lookup, deactivation, or deletion fails, sfp warns with the Flow name and Salesforce error, skips that Flow delete, and continues installing the remaining components. The failed Flow is omitted from metadata deletion too. Any completed deactivation or version deletions remain in effect; review the warning and retry after resolving the cause. Progress appears in a folded Destructive Flows log group, with a warning summary outside the fold.

For both folders, Flow deletion happens before the metadata deployment. It is not part of the deployment transaction: if the later deployment fails, the Flow remains deleted. Remove references that block deletion from the org before this installation; removing them in the same deployment cannot unblock Flow deletion. Other metadata retains the usual pre/post-destructive order.

To retain plain metadata deletion for a package, set "enableFlowDestructiveHandling": false in its sfdx-project.json package descriptor. The setting defaults to true and is independent of enableFlowActivation.

Branch diff-deploy preserves each package's setting. When a diff contains both enabled and opted-out packages, sfp deploys enabled metadata and opted-out metadata as separate synthetic packages, in that order, before alias-aware and data packages. A later package failure does not roll back an earlier package. Diffs whose packages share the setting use one synthetic package.

Check-only deployment skips this handling and retains the plain Flow destructive member, which Salesforce may reject if the Flow is active. Dry-run installation does not delete Flows. Unlocked packages and git-deletion-generated destructiveChanges.xml do not use this handling. The sfp org flow cleanup command continues to keep the active version.


Things to look out for

  • Test destructive changes in your review environment thoroughly before merging your changes
  • You will need to understand the dependency implications while dealing with destructive changes, especially the follow on effects of a deletion in other packages, It is recommended you do a compile all of all apex classes (https://salesforce.stackexchange.com/a/149955 & https://salesforce.stackexchange.com/a/391614) to detect any errors on apex classes or triggers
  • After the version of package is installed across all the target orgs, you would need to merge another change which would remove the post-destructive or pre-destructive folders. You do not need to rush through this , as sfp ignores any warning associated with missing components in the org {% endhint %\}

Data Packages

Data packages utilize sfdmu under the hood, and one can utilize any of the below approaches to remove data records.

Approach 1: Combined Upsert and Delete Operations

One effective method involves configuring SFDMU to perform both upsert and delete operations in sequence for the same object. This approach ensures comprehensive data management—updating and inserting relevant records first, followed by removing outdated entries based on specific conditions.

Upsert Operation: Updates or inserts records based on a defined external ID, aligning the Salesforce org with new or updated data from a source file.

{
  "name": "CustomObject__c",
  "operation": "Upsert",
  "externalId": "External_Id__c",
  "query": "SELECT Id, Name, IsActive__c FROM CustomObject__c WHERE SomeCondition = true"
}

Delete Operation: Deletes specific records that meet certain criteria, such as being marked as inactive, to ensure the org only contains relevant and active data.

{
  "name": "CustomObject__c",
  "operation": "Delete",
  "query": "SELECT Id FROM CustomObject__c WHERE IsActive__c = false"
}

Approach 2: Utilizing deleteOldData

Another approach involves using the deleteOldData parameter. This parameter is particularly useful when needing to clean up old data that no longer matches the current dataset in the source before inserting or updating new records.

  • Delete Old Data: Before performing data insertion or updates, SFDMU can be configured to remove all existing records that no longer match the new dataset criteria, thus simplifying the maintenance of data freshness and relevance in the target org
// Use of deleteOldData
{
  "name": "CustomObject__c",
  "operation": "Upsert",
  "externalId": "External_Id__c",
  "deleteOldData": true
}

On this page