Today I wrote on LinkedIn about something you never forget in your career: the person who knew far more than you and, instead of making you feel it, sat down to teach you.

Good seniors do that. But in a Salesforce org, a lot of what they teach is never written anywhere. Why does this field exist? Who asked for this flow? What is this validation rule protecting? The answer lives in one or two heads, and the new admin depends on those people having time to explain.

This post shows a quick way to find those gaps with three Tooling API and SOQL queries and a few lines of pandas. The output is a list of components without a description, grouped by the person who last touched them. That list is a good agenda for a senior and a new admin to sit down together.

1. Custom fields without description or help text

Run it with the Salesforce CLI against the Tooling API:

sf data query --use-tooling-api --result-format csv \ --query "SELECT EntityDefinition.QualifiedApiName, DeveloperName, Description, InlineHelpText, LastModifiedDate, LastModifiedById FROM CustomField WHERE NamespacePrefix = null" \ > fields.csv 

NamespacePrefix = null leaves out fields that come from managed packages, which you do not own. Description is for admins. InlineHelpText is what users see next to the field. Both empty usually means the meaning of the field lives in someone's memory.

2. Active flows without description

FlowDefinitionView is queryable with regular SOQL:

sf data query --result-format csv \ --query "SELECT ApiName, Label, Description, ProcessType, LastModifiedBy, LastModifiedDate FROM FlowDefinitionView WHERE IsActive = true" \ > flows.csv 

A flow nobody can explain is the riskiest item on this list, because it runs on its own.

3. Active validation rules without description

sf data query --use-tooling-api --result-format csv \ --query "SELECT EntityDefinition.QualifiedApiName, ValidationName, Description, ErrorMessage, LastModifiedDate, LastModifiedById FROM ValidationRule WHERE Active = true AND NamespacePrefix = null" \ > rules.csv sf data query --result-format csv \ --query "SELECT Id, Name, IsActive FROM User WHERE UserType = 'Standard'" \ > users.csv 

Read the ErrorMessage column too. A message like "Invalid value" with no description behind it is what makes a new user stop asking and go back to a spreadsheet.

4. Build the list

import pandas as pd users = pd.read_csv("users.csv") users["Id15"] = users["Id"].str[:15] names = dict(zip(users["Id15"], users["Name"])) active = dict(zip(users["Id15"], users["IsActive"])) def blank(s): return s.isna() | (s.astype(str).str.strip() == "") fields = pd.read_csv("fields.csv") fields = fields[blank(fields["Description"]) & blank(fields["InlineHelpText"])] fields["component"] = "field " + fields["EntityDefinition.QualifiedApiName"] + "." + fields["DeveloperName"] rules = pd.read_csv("rules.csv") rules = rules[blank(rules["Description"])] rules["component"] = "rule " + rules["EntityDefinition.QualifiedApiName"] + "." + rules["ValidationName"] out = [] for df in (fields, rules): ids = df["LastModifiedById"].astype(str).str[:15] out.append(pd.DataFrame({ "component": df["component"], "ask": ids.map(names), "still_active": ids.map(active), "last_change": df["LastModifiedDate"], })) flows = pd.read_csv("flows.csv") flows = flows[blank(flows["Description"])] out.append(pd.DataFrame({ "component": "flow " + flows["ApiName"], "ask": flows["LastModifiedBy"], "still_active": None, "last_change": flows["LastModifiedDate"], })) gaps = pd.concat(out).sort_values(["ask", "last_change"]) gaps.to_csv("knowledge_gaps.csv", index=False) print(gaps.groupby("ask", dropna=False).size().sort_values(ascending=False)) 

The CSV headers can vary slightly between CLI versions, so check the column names in your files first. The last line tells you who carries most of the undocumented configuration.

5. What to do with the list

Do not turn it into a cleanup sprint for one person. Turn it into pairing sessions:

What you see What to do
One name owns most of the gaps Book a recurring slot with that person and a newer admin
Last modifier is an inactive user The explanation may have left with them; ask whoever uses the component today
Flow without description Start here: it runs without anyone watching
Rule with a vague error message Rewrite the message with the user who keeps hitting it

In each session the senior explains, and the newer admin writes the description and the help text. The senior teaches, the junior learns the org, and what was said stays in the org for the next person.

If your ERP is native to the same org, as with EGA Futura ERP, the same queries cover its custom fields, flows and rules, because it all lives in one metadata model.

I wrote the human side of this, in Spanish, as why you never forget the senior who sat down to teach you.

Juan Manuel Garrido, founder of EGA Futura.