Start free
Andrew Hanna

Andrew Hanna

How to Handle Destructive Changes Safely in a Salesforce Deployment

How to Handle Destructive Changes Safely in a Salesforce Deployment

You delete metadata in Salesforce with a destructiveChanges.xml manifest deployed alongside a package.xml, choosing whether the deletions run before or after the additive part of the deployment. The dangerous part is not the syntax. It is that a successful deletion is not something a deployment rollback can undo, and that the components most likely to break are the ones nobody has in source control.

What is a destructiveChanges manifest?

A destructiveChanges manifest uses the same format as package.xml, with one important difference: wildcards are not supported. Every component you intend to remove must be named.

A minimal entry looks like <types><members>Account.Legacy_Score__c</members><name>CustomField</name></types>.

Two rules that trip people up on the first attempt:

  • A package.xml must still be present, in the same directory. For a delete-only deployment it carries the API version and lists no components.
  • Deletion attempts continue even if some listed components do not exist in the target. That is convenient, and it also means a typo fails silently rather than loudly.

Should deletions run before or after the deploy?

The Salesforce CLI gives you both, through two separate manifests: --pre-destructive-changes deletes before the deploy, --post-destructive-changes deletes after it.

Use post-destructive by default. The additive package lands first, so anything that still references the doomed component can be repointed in the same deployment before it disappears.

Use pre-destructive when the old and the new collide. The clearest cases:

  • You are reusing an API name for a different component.
  • You are changing a field's type by deleting and recreating it.
  • A validation rule or required field would block the data or config change you are deploying.

Getting this backwards produces the two classic failures: a deletion that fails because something still references the component, or an addition that fails because the name is taken.

What does a deletion actually take with it?

This is the part worth being blunt about. Deleting a field deletes its data. Deleted components go to the Recycle Bin unless the deployment sets purgeOnDelete, which makes them immediately eligible for deletion instead. Roll-up summary fields bypass the Recycle Bin regardless of that setting.

And rollbackOnError, which is mandatory for production deployments, reverses the changes of a failed deployment. It is not an undo button for a deletion that succeeded. If you need the data back after a successful purge, you are in a restore conversation, not a deployment one.

There is also a category of breakage that never shows up in your repository: reports, list views and dashboards that reference the field. They are configuration a business user built, they are usually not in Git, and the deployment will not warn you about them.

What pre-flight checks stop a field deletion going wrong?

  1. Run the dependency check in Setup. Salesforce will tell you about formula fields, validation rules, layouts and other declarative references. This is necessary and not sufficient.
  2. Search the metadata by API name. Grep the repository for the API name. Flows, Apex, email templates and permission sets reference fields as strings, and string references do not appear in every dependency view.
  3. Check reports and list views separately. Nothing in your pipeline knows about them. Someone has to look.
  4. Export the data first. One CSV of the column plus record IDs. It costs ten minutes and it is the only real safety net.
  5. Run local tests. For Apex classes and triggers, Salesforce's own guidance is to run all local tests, which is how you find code still referencing the thing you are deleting.
  6. Check for an active Lightning page. You cannot delete a component associated with an active Lightning page. Deactivate the action override in Lightning App Builder first.
  7. Validate against production. A checkOnly deployment, or --dry-run from the CLI, validates and runs tests without saving anything.

How do you sequence a deletion across two releases?

The safe pattern is not a better manifest. It is splitting the deletion into two releases.

  1. Release N, deprecate. Remove every reference: layouts, formulas, flows, Apex, permission sets, reports. Mark the field deprecated in its description so the next person knows. Stop writing to it. Ship nothing destructive.
  2. Soak. Let a reporting cycle pass. This is when the report nobody mentioned surfaces, and it surfaces as a question rather than an incident.
  3. Release N+1, delete. Export the data, then deploy the destructiveChanges manifest on its own, with nothing else in it.

Shipping a deletion alone matters. If it is bundled with twenty other changes and something goes wrong, you cannot isolate the cause quickly, and rollback takes the good changes with it.

The same discipline applies to renames, which Git records as a delete plus an add. Treat every rename as a two-release deprecation unless you can prove nothing references the old name. More delivery playbooks are in our SF Guides library.

FAQ

Can I use a wildcard in destructiveChanges.xml?

No. Wildcards are not supported. Every component to be deleted has to be named explicitly.

Do I still need a package.xml when I am only deleting?

Yes. It must sit in the same directory, carry the API version, and list no components.

Can I recover a field deleted by a deployment?

It goes to the Recycle Bin unless purgeOnDelete was set, and roll-up summary fields bypass the bin regardless. Treat a data export as the real recovery plan.

Why did my destructive deployment succeed but delete nothing?

Deletion attempts continue when a listed component is absent from the target, so a wrong API name produces a green deployment and no change. Diff the target before and after.

When should deletions run before the deploy instead of after?

When the old and new components conflict: reusing an API name, recreating a field with a different type, or removing a rule that would block the rest of the deployment.

Related Articles

Curious about faster shipping before you dive in? Let's talk

Commitment free!