GTM Galaxy

Article

Mapping Ghost Fields: Building a CRM Metadata Dependency Grapher with Claude Code

You find a custom field named Lead_Score_V2_Legacy_FINAL. It hasn't been populated in fourteen months. No one remembers why it exists. You go to delete it, but a small voice in your head asks what might break.

In a mature Salesforce org, deleting a field is like pulling a loose thread on a cheap sweater. You think you’re just removing one strand, but suddenly the sleeve falls off. That sleeve is your lead routing flow, an executive dashboard, or a critical integration script that hasn't been updated since 2019.

RevOps is often less about building new things and more about archaeology. We spend our time trying to understand the intent of the people who came before us. The native "Where is this used?" button in Salesforce is a decent start, but it’s fundamentally limited: it misses references in complex flows, it can't see into external scripts, and it famously caps at 2,000 results. If you’re working in an org with a decade of technical debt, 2,000 records isn't even the tip of the iceberg.

The Metadata Dependency Gap

The UI fails because the metadata layer is deep. A field can be referenced in a formula, which is used by a validation rule, which is triggered by an Apex class, which is called by a Flow.

Salesforce exposes this web through the Tooling API via the MetadataComponentDependency object. This object stores the directional relationship between two components. It’s the source of truth for exactly which Flow depends on which Field.

But there’s a catch. Standard Tooling API SOQL queries are limited to 2,000 records. To map a complex org, you need to use the Bulk API 2.0 for Tooling, or chunk your queries by object. This is where a GTM engineering approach, paired with an agent like Claude Code, becomes a superpower.

Building the Scanner with Claude Code

We need a tool that sits on a local machine, queries the org via the Salesforce CLI (sf), and builds a graph of every field dependency without manual clicking.

Using Claude Code, I initialized a Node.js project to handle the heavy lifting. The goal: authenticate via the existing CLI session and pull every dependency for a specific set of target fields.

First, I had Claude identify the target fields by querying the CustomField object to get IDs for everything on the Opportunity and Account objects. Then, we tackled the query limit problem.

The Prompt:

"Write a script using jsforce that uses the Salesforce Bulk API 2.0 to query the MetadataComponentDependency object. I need MetadataComponentName, MetadataComponentType, RefMetadataComponentName, and RefMetadataComponentType. Filter to only show dependencies where the referenced component is a CustomField. Handle the job polling and export the results to a local CSV."

Claude generated a handler that creates the Bulk job and polls the /jobs/query endpoint. This is the "aha" moment: Bulk API 2.0 allows for up to 100,000 dependency records in a single job, bypassing the UI limitations entirely.

Turning Data into a Directed Graph

Raw CSV data is better than nothing, but it’s hard to audit. You want to see the blast radius. I had Claude Code add a local parsing layer to convert the CSV into a JSON directed graph, which we can then output as a Mermaid.js diagram or a simple risk report.

// Claude's logic for quantifying the blast radius
const dependencies = results.map(row => ({
  from: row.MetadataComponentName,
  to: row.RefMetadataComponentName,
  type: row.MetadataComponentType
}));

const riskScore = (fieldName) => {
  const directDeps = dependencies.filter(d => d.to === fieldName);
  return directDeps.length;
};

When you see a field with twenty edges pointing away from it into various Apex classes and Flows, that’s a High Risk field. If a field has zero outgoing edges and hasn't been updated in a year, you’ve found a ghost. It’s safe to move to the graveyard.

Handling the Limits of Static Analysis

Even with a custom scanner, there are two counterarguments to watch for:

  1. External API Scripts: If a Python script on a remote server queries Salesforce via the REST API using a hardcoded field name, the MetadataComponentDependency object won't know it exists. The platform only tracks what is defined within the platform. You still need to check your integration middleware logs (Tray, Workato, n8n).
  2. Dynamic Apex: If a developer wrote a class that constructs a field name as a string at runtime—e.g., String f = 'Custom_Field_' + suffix;—the dependency API will miss it. For these, a simple grep or regex search across your codebase remains a necessary secondary check.

Build vs. Buy

There are enterprise change intelligence platforms like Sonar or Elements.cloud that provide continuous, hosted metadata analysis. If you are managing a global org with dozens of admins and a massive budget, buy them. They offer a level of persistence and UI polish that a CLI tool won't match.

But for a GTM operator, building this scanner with Claude Code provides 80% of the value for the cost of a few API calls. More importantly, it builds technical literacy. You learn how the Tooling API works and how to handle Bulk data flows. You stop being someone who just manages software and start being someone who understands how the revenue system actually functions under the hood.

The Actionable Blast Radius Report

The final output is a markdown report for stakeholders. Instead of saying "I think we can delete this," you provide a technical audit:

  • Field: Obsolete_Lead_Source_Detail__c
  • Dependencies: 3
  • Critical Paths: Lead_Scoring_v2 (Flow), Global_Account_Trigger (Apex)
  • Risk: High (Requires refactoring before deletion)

Next time you're tempted to "test in production" by deleting a field on a Friday afternoon, take thirty minutes to query the metadata instead. Your future self will thank you.

— C.B.