Salesforce configuration debt

How to find it, measure it and pay it down.

Salesforce configuration debt is the accumulated declarative setup in an org — custom fields, objects, flows, validation rules, profiles, permission sets and installed packages — that nobody uses, nobody can explain, or that overlaps with something else, and that makes every later change slower and riskier. It differs from code debt because it is created by clicks rather than commits: it usually has no version history, no tests and no named owner, so it cannot be found by reading a repository. It has to be inventoried from the org itself.

  • It falls into five groups: data model, automation, access, code and installed packages.
  • Most of it can be inventoried with Setup, a few SOQL queries and the open-source Org Check app.
  • Metadata with a namespace prefix belongs to a managed package: it is the vendor's debt, not yours.
  • Pay it down in order: inventory, check dependencies, deactivate, test in a sandbox, then delete.

What counts as configuration debt

Not everything old is debt. An item belongs on the list when it is unused, when nobody can explain it, or when it duplicates something else. The inventory below is grouped the way you will search for it.

Data model

Fields and objects that store nothing, or that nobody can explain.

  • Custom fields that are empty on nearly every record, or that are on no page layout and referenced by nothing.
  • Custom fields with no description or help text, so nobody can say what they were for.
  • Custom objects that no user, flow or integration has written to in a long time.
  • Picklists carrying duplicate or near-duplicate values ("Closed - Won" and "Closed Won"), and inactive values still present on old records.
  • Several fields that hold the same fact under different names.

Automation

More than one thing firing on the same record, in an order nobody designed.

  • Overlapping record-triggered flows on one object that update the same fields.
  • Legacy Workflow Rules and Process Builder processes still active alongside newer flows.
  • Flows whose data and callout elements have no fault path, so a failure surfaces to the user as an unhandled fault.
  • Multiple Apex triggers on the same object, whose execution order Salesforce does not guarantee.
  • Inactive flow versions and deactivated automation that were never removed.

Access

Permissions granted once and never reviewed.

  • Profiles carrying broad permissions such as Modify All Data or View All Data for users who do not need them.
  • Permission sets assigned to nobody, and permission sets that are clones of clones.
  • Active users who have not logged in for months and are still consuming a licence.
  • Integration users with far wider access than the integration uses.

Code

Apex and other code that is old, untested or tied to one environment.

  • Apex classes and triggers saved against very old API versions.
  • Low test coverage, or coverage that has not been measured because tests have not been run.
  • Hard-coded record IDs, URLs or user names that differ between sandbox and production.
  • Classes that nothing calls any more.

Packages

Installed software that outlived its purpose.

  • Managed packages that are installed but that no user is assigned to or uses.
  • Trial or pilot packages left behind after the evaluation ended.
  • Packages whose licences are still being paid for with few or no assigned users.

How to find it with tools already in Salesforce

You do not need to buy anything to build a first inventory. These are all either part of the platform or free.

Where to lookWhat it findsHow to use it
Setup → Health CheckThe security baselineCompares your password, session and network access settings with the Salesforce Baseline Standard and gives a score with the settings that fall short. It covers security settings only; it does not look at fields, flows or Apex.
Object Manager → Fields & Relationships → a field → "Where is this used?"References to a custom fieldLists the layouts, flows, formulas, validation rules, Apex and reports that reference the field. It tells you what would be affected by a change. It does not tell you whether the field holds data: check that with a report or a count query filtered on the field not being blank.
FlowDefinitionView (standard API)The flow inventoryOne row per flow definition, with its type, whether it is active and when it was last modified. Inactive flows and flows untouched for years are the first candidates for review.
ApexClass and ApexTrigger, with ApiVersionOld codeSort by ApiVersion to find the oldest classes. Filter on NamespacePrefix first so you only count code that is yours.
ApexCodeCoverageAggregate (Tooling API)Test coverage per classHolds covered and uncovered line counts for each class or trigger, as recorded by the most recent test runs.
PermissionSetAssignment and UserLicenseUnassigned permission sets and unused licencesA permission set with no rows in PermissionSetAssignment is assigned to nobody. UserLicense shows used against total seats per licence type.
Setup → Installed PackagesManaged packages and their licencesLists every installed package with its namespace prefix, version and, for licensed packages, how many licences are allowed and used.
Org Check (Salesforce Labs, open source)A broad technical-debt inventoryA free app you install in the org. It reports on custom fields, automation, Apex, profiles, permission sets and more in one place, and its source is public.

Three queries to start with

Inactive flows

SELECT ApiName, Label, ProcessType, IsActive, LastModifiedDate
FROM FlowDefinitionView
WHERE IsActive = false

Run this in the standard API (for example the Developer Console query editor with "Use Tooling API" unticked). Remove the WHERE clause to see every flow.

Your own Apex classes, oldest API version first

SELECT Name, ApiVersion, NamespacePrefix
FROM ApexClass
WHERE NamespacePrefix = null
ORDER BY ApiVersion ASC

The NamespacePrefix = null filter excludes classes that belong to installed managed packages.

Licence usage

SELECT Name, UsedLicenses, TotalLicenses
FROM UserLicense

Compare used against total for each licence type. Not every row is a paid seat: some licence types are included with the org at no charge.

Two caveats most guides get wrong

An empty ApexCodeCoverageAggregate does not mean 0% coverage

That object is filled in by test runs. If nobody has run tests recently it returns no rows, and the correct reading is "coverage has not been measured", not "coverage is zero". Run all tests, then query it again. Classes from managed packages do not appear in it at all.

Anything with a NamespacePrefix is the vendor's, not yours

Classes, pages, fields and other metadata with a namespace prefix belong to an installed managed package. You cannot edit them, and their API versions and test coverage are the vendor's responsibility. Filter them out before counting, or an org with one large package installed will look as if it has hundreds of old, untested classes of its own. The decision you do own is whether the package should stay installed.

How to measure it

A raw count of findings is misleading: forty undocumented fields matter less than one failing test that blocks every deployment. A simple score that holds up:

  1. Count findings in each of the five groups, after removing managed-package metadata.
  2. Give each finding a blast radius: how many users, records or integrations are affected if it misbehaves. A field on one team's layout is small; a trigger on Account or a profile assigned to most users is large.
  3. Flag whether it blocks a release. Production Apex coverage below the 75% Salesforce requires for deployment, or a failing test, stops every deployment and outranks anything cosmetic.
  4. Sort by release-blocking first, then blast radius, then count. Track the totals by group over time rather than chasing a single number.

The limits of a dollar figure

You can put a cost on configuration debt, but only by assumption. Any figure is hours multiplied by a rate: hours spent investigating before each change, hours of rework after a broken deployment, and licences paid for and not used. The licence part can be read from the org and your contract. The hours part is an estimate. State the hours and the rate next to the number every time it is quoted, and treat a cost estimate without its assumptions as a guess.

How to pay it down safely

The risk in a clean-up is not leaving something behind. It is deleting something that was still in use. Work in this order.

  1. 1

    Inventory before you touch anything

    Export the lists above (fields, flows, Apex, permission sets, packages) and record the date. This is the baseline you measure against and the record of what existed.

  2. 2

    Check dependencies before deleting

    For each candidate, find what references it: "Where is this used?" for fields, and a search of flows, Apex, reports and integrations for everything else. A field that looks unused may be read by an integration or a report that runs once a quarter.

  3. 3

    Deactivate before you delete

    Deactivate flows, rules and triggers, remove fields from layouts and take away field-level access, then wait through at least one full business cycle (a month-end or quarter-end) to see whether anything or anyone misses it.

  4. 4

    Make the change in a sandbox first

    Apply the removal in a sandbox, run all Apex tests and walk through the affected business processes. Deploy to production only after that passes.

  5. 5

    Keep a rollback path

    Retrieve the metadata and export the data in any field you are about to delete. A deleted custom field can be restored from Deleted Fields for 15 days; after that it is erased along with its data. Changing a field's type is not a safe alternative to deleting it: a retype reinterprets the stored values rather than converting them, and some type changes lose data.

  6. 6

    Re-measure

    Run the same inventory again and compare the counts by group with the baseline. Add a description to everything that survives so the next review is faster.

If a change fails with an error you do not recognise, the Salesforce error fix guides cover the common ones.

Where Wrenk fits

Everything above can be done by hand. Wrenk automates the inventory and the dependency checks for teams that would rather not run them manually each quarter.

  • Runs a health check against a connected Salesforce org covering security settings, permissions, automation, data model, Apex, licences and installed managed packages.
  • Attributes managed-package metadata to its vendor and leaves it out of your score.
  • Reports a check it could not run as "not assessed" rather than as passed or failed, and separates "coverage not measured" from "measured at zero".
  • Before a field change, reads what references the field and returns an ordered plan. Changes run in a sandbox first when one is connected, and any production write needs explicit confirmation.
  • Works inside Claude and ChatGPT through an MCP connector, as well as in the web app.

The free guest health check is a short questionnaire: it needs no account and no org connection. A check against live data needs a connected org. See what the Salesforce health check covers or compare Salesforce org health tools.

Frequently asked questions

What is configuration debt in Salesforce?

Configuration debt is the declarative setup in an org — custom fields, objects, flows, validation rules, profiles, permission sets and installed packages — that is unused, undocumented or overlapping, and that makes later changes slower and riskier. It is the click-built counterpart to code debt.

How is configuration debt different from technical debt?

Technical debt is the broader term and includes Apex and integration code. Configuration debt is the part created through Setup rather than through code. It usually has no version history, no tests and no named owner, so it has to be found by inventorying the org rather than by reading a repository.

How do I find unused fields in Salesforce?

There are two separate questions. To see whether a custom field is referenced, open it in Object Manager and use "Where is this used?". To see whether it holds data, run a report or a count query filtered on the field not being blank. A field is a removal candidate only when it is both unreferenced and empty or stale. The open-source Org Check app from Salesforce Labs reports on custom fields across the whole org.

Does an empty ApexCodeCoverageAggregate mean my coverage is 0%?

No. ApexCodeCoverageAggregate is populated by test runs. If it returns no rows, tests have not been run recently and coverage is unmeasured. Run all tests and query it again before drawing any conclusion.

Is it safe to delete a custom field?

Only after checking what references it and exporting its data. A deleted custom field can be restored from Deleted Fields for 15 days; after that it is permanently erased together with its data. Remove it from layouts and take away field-level access first, wait a full business cycle, and make the deletion in a sandbox before production.

Should managed-package components count towards my technical debt?

No. Metadata with a namespace prefix belongs to the package vendor. You cannot change its code, API versions or test coverage. Exclude it from your counts. What you can decide is whether the package is still used and should stay installed.

Get a first read on your org

Answer a short questionnaire about your setup. No account and no org connection needed.

Run Free Health Check