Can I delete this Salesforce field?
Most field cleanups start with a population report and end with a spreadsheet of percentages. That measures the wrong thing. Population tells you how much data is in a field; it never tells you what depends on it. Here is a dependency-first method - what blocks a delete, what breaks silently afterwards, and the four verdicts that make the output actionable.
Short answer: a field is safe to delete when nothing consumes it, not when nothing populates it. Before dispositioning any field, name one thing that consumes it and say what that thing would do if the field vanished. If you cannot fill in that sentence, the field is not decided yet - whatever the percentage says.
Why population is the wrong evidence
Population and dependency are different questions, and they come apart hardest on exactly the fields that matter: formula fields, transient state flags, integration keys, and anything frozen years ago.
- A dense field can be dead. A field populated on every record by a migration, read by nothing since, is residue with a reassuring percentage attached.
- An empty field can be load-bearing. Report filters cannot natively compare a field to the running user, so a formula like
IF(Owner__r.Id = $User.Id, TRUE, FALSE)is often the only available scoping mechanism for a "my records" report. Measured by whoever runs the query, it can read 0% populated. Delete it and the dashboard it scopes silently widens to the whole org. - A formula field has no meaningful population figure at all. If the body references
$User,$UserRole,$ProfileorTODAY(), the percentage measures who ran the query. If it returnsIF(...,1,0)orCASESAFEID(Id), it can never be null, so it reads 100% whether or not a human has ever looked at the object. Read the body; strike the number.
Measure flow, not just stock
Population is a stock measure. It cannot tell you a process stopped five years ago. Three cheap queries per field usually reclassify more rows than the entire population report:
SELECT MAX(Some_Field__c) FROM Custom_Object__c
SELECT COUNT() FROM Custom_Object__c WHERE Some_Field__c > TODAY
SELECT CreatedById, COUNT(Id) FROM Custom_Object__c
WHERE Some_Field__c != null AND CreatedDate = LAST_N_DAYS:180 GROUP BY CreatedByIdA 40%-populated date field whose newest value is four years old is frozen. A 0.7%-populated deadline field with no future-dated value at all is residue - for a "by date" field, that absence is the tell. And if an integration identity created every recent row on a steady cadence, the field is alive regardless of how sparse it looks.
Sandbox caution. A sandbox stops receiving production writes at its refresh point, so record creation falls off a cliff on a specific date and every field looks dead from that month onward. Before reading any "no writes since" finding as death, plot creation volume for the whole object and find the cliff. A field that stops well before it is dead; a field that stops at it is simply unmeasured.
Blocking, breaking, cosmetic
Not every dependency is the same problem. Classify each one, because they fail in completely different ways.
| Class | What happens on delete | When you find out |
|---|---|---|
| Blocking | The platform refuses the delete, or the destructive deploy fails validation | Immediately and loudly, in a sandbox |
| Breaking | The deploy succeeds and something silently misbehaves | Never, or weeks later via a wrong number |
| Cosmetic | A column vanishes, a layout gap appears, an unused grant drops | Visible and harmless |
Blocking covers roll-up summary sources and summary foreign keys, formula references, validation-rule bindings (including errorDisplayField, which blocks even when the field never appears in the rule's logic), any Flow version naming the field - active, draft or obsolete - and master-detail relationships.
Breaking is the dangerous class, because a green deploy is not evidence of safety:
| Role the field plays | What deletion does | Failure mode |
|---|---|---|
| Sole filter criterion on a report or list view | Scope disappears; the result set widens | Silent - fails open |
| Non-blank row gate | Rows previously excluded now appear | Silent - fails open |
Related-list sortField on another object's layout | Every page rendering that list loses its ordering | Silent |
Report date axis (timeFrameFilter) | The time window disappears | Semi-silent |
| Dashboard grouping column | The component loses its axis and breaks | Loud |
| Display column only | One column is missing | Loud, harmless |
This is why "16 reports" and "1 report" are not comparable evidence. Sixteen display columns break nothing in the org; one sole filter is a Keep or a staged retirement with report remediation first.
The dependencies most audits miss
The hardest blockers are not declared on the field you are auditing. They live on other objects' metadata, which means a per-field search scoped to the field's own object cannot find any of them:
- Roll-up summaries -
summarizedField,summaryForeignKeyand the fields insidesummaryFilterItemsare all declared on the parent object. - Cross-object formulas - written as
Relationship__r.Field__c, so a search for the__cspelling misses them entirely. - Related lists on other objects' layouts - including the lookup that keys the related list, whose deletion removes the whole list rather than a column.
- Flow entry criteria and
PRIORVALUE()expressions, which many dependency indexers model as a different kind of edge than a field read. - Lookup filters, dependent-picklist bindings and default-value formulas on other fields entirely.
Four verdicts, not three
Most cleanup spreadsheets offer Keep, Review and Remove. That collapses a real and common answer - right destination, wrong timing - into either a parking label or a same-day delete that destroys history rows.
| Verdict | Means | What must ship with it |
|---|---|---|
| Keep | Live dependency, integration contract, or clear business value | The blocker, never the population. "Keep because 100% populated" gets re-litigated within a year. |
| Review | Genuinely ambiguous; turns on a business decision | The exact question, and the named human who answers it. Without both, it is indistinguishable from an unanswered field. |
| Deprecate-then-Remove | Dead in practice, but holds data, tracked history or cosmetic references | An ordered retirement: strip layout, mark the description, remove from report types, monitor an integration cycle, then delete. |
| Remove | No data of value, no live dependency, no plausible future need | Ordered pre-work: strip layout → strip report-type columns → deploy → delete the field, as separate deployments. |
Two operational notes that bite during execution rather than analysis. Salesforce refuses a field delete while any Flow version references it, active or not - so obsolete flows and all their versions must be cleared first. And bundling the field delete with the layout and report-type strip in one deployment gets refused or silently dropped; run them as separate deployments.
The blind spots no static analysis closes
State these explicitly rather than letting a thorough metadata sweep imply full coverage. Every one of them fails at runtime, not at deploy time:
- Dynamically built SOQL and reflective field access (
.get('Field__c')) - the field name is a string assembled at runtime. - Field lists stored as org DATA in custom settings or custom metadata records, read at runtime by a controller. These are data, not metadata: invisible to a metadata retrieval and to any metadata knowledge base. A single row naming your object can make arbitrary fields load-bearing on a live page.
- External ETL and integration job definitions - usually the only place a deletion can actually break something, and the one place you cannot look from inside the platform.
- Private and personal report folders, which are not retrievable via the Metadata API at all.
- Email template merge fields, and managed-package internals.
- Field history rows, which a delete destroys permanently and no export of current values recovers.
The discipline that matters most: always separate "checked and found nothing" from "could not check." They look identical in a report and mean opposite things. One is a finding worth publishing; the other is an open question wearing a finding's clothes.
Never let a tool's zero stand on its own
A zero from any dependency tool is a statement about the instrument until you have shown the instrument can see that surface. Before recording "no references", run the identical method against a field you already know is referenced. If it does not light up, the method is blind and its zeros mean nothing.
We take this seriously enough to apply it to ourselves. Auditing sf-intelligence against a real field-cleanup engagement surfaced four modelling gaps in our own dependency graph, each of which could return a clean safe verdict for a field the platform refuses to delete: roll-up coupling that lived only as a node property, condition field references that were never edges, formula __r traversals that were skipped, and dynamic related-list columns that were never parsed. All four are now closed and covered by tests. The condition gap had a second floor underneath it worth naming, because it generalises: Flow writes the same condition triplet two ways - leftValueReference inside a decision, field inside a record trigger's start block - and a parser that knows only the first reports decisions perfectly while dropping every entry criterion in the org.
The property that made them dangerous is worth generalising past our own bug list: all four sat inside metadata families the tool had fully retrieved, so no coverage warning fired and the verdict presented as clean rather than hedged. A tool can only caveat the gaps it knows it has. Treat a confident safe on a field with real structural weight - a roll-up, a master-detail, a condition, an integration key - as a prompt to check by hand, no matter which tool produced it.
Running this with sf-intelligence
sf-intelligence is an offline, read-only knowledge base for one Salesforce org. For a field cleanup, the relevant calls are:
sfi.coverage_report- which metadata families the last refresh actually retrieved. A family listed as not modeled means "not checked", never "none".sfi.field_360- the full profile of one field across validation, formulas, writers, readers, UI and integrations.sfi.find_field_anywhere- every incoming edge, grouped by component type.sfi.safe_to_delete_field- a verdict with its reasoning chain, and acoverageCaveatwhen the families it depends on are not fully covered. Render that caveat before the verdict, never as a footnote.
Claude Code users get the whole method as a plugin skill (salesforce-field-audit); other MCP hosts get it as the sfi.field_audit prompt. One prerequisite worth knowing, and it is the fifth gap from the list above - the one still open rather than closed: the default refresh pulls the top 500 reports and dashboards ranked by usage, so on a large org a field's report count is drawn from a fraction of the corpus. Raise SFI_REPORTS_CAP before an audit, and check that Report coverage reads complete rather than pending before quoting any report figure.
FAQ
Does field population tell you if a Salesforce field is unused?
No. Population measures how much data is in a field; it says nothing about what depends on it. The two agree on busy, obviously-live fields and diverge everywhere else. A field with 750,000 values can be dead - written once by a migration years ago and read by nothing. A field with zero values can be load-bearing, because a running-user formula that returns false for the person running the query still scopes a live report.
What stops you from deleting a custom field in Salesforce?
The platform refuses the delete outright when the field is a roll-up summary's summarized field or summary foreign key, is referenced by a formula field, is bound to a validation rule (including as its errorDisplayField, even when it never appears in the rule logic), is named by any Flow version - active or not - or is a master-detail field. Those are blocking. A separate class deletes successfully and breaks something quietly afterwards.
What happens if you delete a field used as a report filter?
The report does not empty - it silently widens. Deleting a field that a report filters on removes the filter, so the report returns more rows than it should while still looking healthy and still returning a plausible number. Nothing errors, nothing fails to deploy, and nobody files a ticket. That is why a field audit must record what each reference does, not just how many references exist.
Can you recover a deleted Salesforce field?
Partly, and only for a short window. A deleted field and its data sit in a recycle state for a limited period (commonly around 15 days) before purging, and that undelete window is the only rollback for the data. Field history rows are a harder loss: deleting a history-tracked field permanently discards its history, and an export of current values does not recover it.
How do you find which fields are actually unused in Salesforce?
Trace dependencies rather than counting rows. For each field, enumerate what consumes it - flow entry criteria, formulas, roll-ups on the parent object, validation rules, related lists on other objects' layouts, list-view and report filters, dashboard groupings, Apex, LWC, and integration contracts - then record what each consumer would do if the field vanished. Reverse dependencies are the ones most audits miss, because a roll-up that blocks the delete is declared on the parent object's metadata, not the field's.