Unused metadata and bloat
Vulkro builds a reference graph over the metadata in your SFDX project and flags components that nothing else appears to reference. Dead metadata is a real cost: it inflates deploy time, widens the AppExchange Security Review surface, hides genuine dependencies behind noise, and makes every refactor riskier than it should be.
Every finding in this surface is framed as a candidate, never a verdict. A static reference graph cannot see runtime-dynamic references: a field named in a SOQL string built at runtime, a component invoked from a Visualforce page rendered by name, a label read through a dynamic API call, or a reference that lives only in an org you did not retrieve. Treat these findings as a triage list, not a delete list. The value is the short, ranked candidate set, not an instruction to remove anything.
SF-UNUSED-001 through SF-UNUSED-020: candidate-unused metadata
The SF-UNUSED-0NN family walks more than 50 metadata types and reports
each component that has no inbound reference anywhere in the retrieved
project. Each metadata type carries its own finding ID so you can
triage (or suppress) one category at a time.
The covered types include, among others:
- Custom objects and custom settings with no field, layout, Apex, flow, or report reference.
- Custom fields not read or written by any Apex class, trigger, flow, Visualforce page, LWC, Aura component, validation rule, report type, or list view.
- Report types that back no saved report.
- Quick actions present on no page layout or Lightning page.
- List views owned by no profile and surfaced on no component.
- Record types assigned in no profile or permission set and selected by no flow or Apex.
- Field sets referenced by no LWC, Aura, or Visualforce markup.
- Compact layouts assigned to no record type.
- Workflow rules that are inactive and referenced nowhere.
- Custom labels read by no Apex, flow, or component.
- Apex classes invoked by no trigger, flow, web service, test, or other class.
- Flows launched by no trigger, no subflow parent, no process, and no action.
What triggers it. A component node in the reference graph with an inbound-edge count of zero after the full project is parsed.
Why it matters. Candidate-unused metadata is the cheapest debt to retire and the easiest to get wrong. Removing a component that is in fact referenced at runtime breaks production silently. Surfacing it as a ranked candidate set lets a human confirm before acting.
How to fix. For each candidate, confirm there is no
runtime-dynamic reference (search Apex for dynamic SOQL that names the
field or object, check for Type.forName or Database.query string
construction, and confirm no managed package or external integration
calls the component by name). If confirmed unreferenced, stage the
removal in a sandbox first. If the reference is real but dynamic, keep
the component and add it to your baseline so the candidate does not
resurface.
SF-BLOAT-001: object field bloat
What triggers it. A custom object that carries a field count past a configurable threshold, where a high share of those fields show no inbound reference in the graph. The finding combines raw field count with the candidate-unused ratio so a legitimately wide object (one whose fields are all used) does not trip it.
Why it matters. A bloated object slows page rendering, inflates SOQL row sizes, complicates field-level security review, and makes the object's true data model hard to read. Field bloat is also a common precursor to accidental data exposure: the more fields an object carries, the more surface a misconfigured permission set or report type can leak.
How to fix. Review the candidate-unused fields on the object first
(they appear under the SF-UNUSED-0NN family). Consolidate
near-duplicate fields, archive fields tied to retired processes, and
move rarely-used attributes to a related object where the one-to-many
relationship is genuine.
SF-BLOAT-002: duplicate-purpose fields
What triggers it. Two or more fields on the same object that appear
to serve the same purpose, inferred from name similarity, identical
data type, and overlapping usage in layouts, flows, and Apex. A
classic shape is Status__c alongside Current_Status__c, or three
date fields that all track the same lifecycle event under different
team conventions.
Why it matters. Duplicate-purpose fields split the source of truth. Reports disagree, automations write to one field while a flow reads the other, and data quality decays. They are also a frequent root cause of inconsistent record-level security, because field-level security is set once per field and rarely kept in sync across duplicates.
How to fix. Pick the canonical field, migrate data and automation to it, and retire the duplicates through the candidate-unused workflow. Where two fields genuinely differ (one is a working value, one is an audit snapshot), rename them so the distinction is explicit and the duplicate-purpose heuristic stops matching.
Reading the output
Findings carry the component type, the API name, the inbound-edge count (zero for the unused family), and for the bloat findings the field count and candidate-unused ratio. JSON output includes the same fields plus the graph node identifier so you can correlate a candidate across runs and decide once whether to baseline it.