JAWC
  1. Home
  2. Notes
  3. How to read a ten-year-old Salesforce org

How to read a ten-year-old Salesforce org

6 min readsalesforcemetadatasurveyautomationcli

A first-week survey of an old Salesforce org. Pull the metadata, find dead fields, map every automation in order, and write the keep, kill, and rebuild lists.

Nobody in the building can tell you what the org does. The admin who built the first flows left in year four. The consultants who added the next layer left in year seven. The people who remain know which buttons to press and which errors to ignore. The metadata is the only witness left, so the first week of a survey is spent reading it. This is how we do that week, in order.

Day one: pull everything and commit it

Retrieve the whole org as source. The Salesforce CLI can write a manifest from the org itself, then retrieve against it.

sf project generate manifest --from-org old-org \
  --output-dir manifest
sf project retrieve start --manifest manifest/package.xml \
  --target-org old-org

A large org exceeds a single retrieve. Split the manifest by type and retrieve in passes: objects and fields first, then flows and Apex, then layouts, then everything else. Reports, dashboards, and email templates sit in folders, so list the folders or the retrieve skips them.

Commit the result to a repository before you change anything. The point is grep. Every question that follows is a search over text, and a search over text is repeatable in a way that clicking through Setup is not. Tag the commit as the baseline. Everything you cut later is measured against it.

Fields: find what nothing reads

Old orgs carry hundreds of custom fields that no screen, report, or flow touches. Build the inventory per object. The Tooling API answers faster than Setup does.

sf data query --target-org old-org --use-tooling-api \
  --result-format csv \
  --query "SELECT DeveloperName, CreatedDate, LastModifiedDate
           FROM CustomField WHERE TableEnumOrId = 'Opportunity'"

For a custom object, TableEnumOrId holds the object’s ID, not its name. Query EntityDefinition first to get the IDs.

Three checks per field, in this order. First, references. The “Where is this used?” button on the field in Object Manager lists layouts, flows, Apex, formulas, validation rules, and reports. It does not see integrations, so grep the retrieved source and the integration code as well: grep -rl "Legacy_Score__c" force-app/. A field with no hits in either place is a candidate.

Second, population. A field nobody reads may still be written, and a field nobody writes is dead twice. One aggregate query answers both.

SELECT COUNT(Id), COUNT(Legacy_Score__c) FROM Opportunity

SELECT COUNT(Id), COUNT(Legacy_Score__c) FROM Opportunity
WHERE CreatedDate = LAST_N_DAYS:365

COUNT(field) counts non-null values. Compare the two counts. Zero in the last year means nothing writes it. Add a date filter on objects with millions of rows, or the query times out.

Third, the owner. Ask the person closest to the object what the field means. If no one can say, it goes on the kill list even if it is populated. Record the three answers in one sheet per object: references, population, owner.

Automation: every object, in order

An old org runs four generations of automation on the same object, and the order they fire in decides what the record looks like after save. Map it per object.

Record-triggered flows run before save, then Apex before triggers, then validation rules, then Apex after triggers, then Workflow Rules, then Process Builder, then record-triggered flows that run after save. A Workflow Rule field update re-fires the triggers once more. Write that down per object with the real names in each slot.

Flow Trigger Explorer in Setup shows the record-triggered flows per object and the order they run. In source, <triggerOrder> in each flow file carries the number. Flows with a value from 1 to 1,000 run first, in ascending order. Flows without a value run next, oldest first. Set a value on every flow that matters, or the order changes the day someone clones one.

Workflow Rules and Process Builder still run. You cannot create new ones. List them per object from workflows/ and flows/ (a process is a flow with <processType>Workflow</processType>), and mark each one for rebuild as a flow or for the kill list.

Apex triggers: grep -l "on Opportunity" force-app/main/default/triggers/*.trigger. Two triggers on one object fire in an order Salesforce does not promise. The fix is one trigger per object that calls a handler class, and the handler decides the order. If the org already has that pattern, keep it. If it has five triggers on Case, that is a rebuild item.

Validation rules and the flows they fight

List every validation rule from objects/*/validationRules/. For each rule, grep the fields in its formula against the fields that flows and triggers write. A flow that sets a field to a value a validation rule rejects fails every time, and the user sees an error about a field they never touched. The ticket says “random error on save”. The cause is a rule from year three fighting a flow from year eight. Each pair goes on the rebuild list with both names.

Hard-coded IDs

Record type IDs, queue IDs, user IDs, and profile IDs pasted into formulas, flows, and Apex break the moment the metadata is deployed to another org, and they already break every time someone leaves. Grep the source for ID shapes by key prefix. Users are 005, queues and groups 00G, record types 012, profiles 00e, roles 00E, permission sets 0PS.

ids='\b(005|00G|012|00e|00E|0PS|001)[0-9A-Za-z]{12}([0-9A-Za-z]{3})?\b'
grep -rEn "$ids" force-app/

Every hit is a rebuild item. Record types become a lookup by DeveloperName. Queues become a lookup by DeveloperName. Users become a custom metadata record with a real owner.

The last twelve months of tickets

The tickets tell you what hurts, and the metadata tells you why. Export every support ticket from the last year. Tag each one with the object it concerns and the cause: data, automation, permissions, integration, or training. Count by object. The top three objects carry most of the rebuild value, and the ticket text gives you the test cases for it.

Profiles and permission sets

Count active users per profile.

SELECT Profile.Name, COUNT(Id) FROM User
WHERE IsActive = true GROUP BY Profile.Name

A profile with one user is a permission set someone built the old way. Then list assignments: SELECT PermissionSet.Name, COUNT(Id) FROM PermissionSetAssignment GROUP BY PermissionSet.Name. Drift looks like this: an object permission that lives in four profiles and two permission sets, field-level security set on the profile for half the fields and on a permission set for the rest, and Modify All Data on a profile that was meant for one person years ago. The target state is a few thin profiles and permission set groups per job. The survey only has to show the distance.

Integrations and who owns them

Every connected app, named credential, and integration user is a door into the org. List them, then find out who holds each key.

  • Connected apps: SELECT Name, CreatedBy.Name, CreatedDate FROM ConnectedApplication. Then SELECT AppName, User.Username, LastUsedDate, UseCount FROM OauthToken to see which ones anyone still uses.
  • Named and external credentials from namedCredentials/ and externalCredentials/ in the source.
  • Integration users: SELECT Username, Profile.Name, LastLoginDate FROM User WHERE IsActive = true AND UserType = 'Standard', then read the usernames. API-only users and users named after a vendor are the ones to chase.

For each one, write three facts: what system sits on the other end, who on the business side owns it, and whose account holds the credential. An integration with no business owner stays on the list until someone claims it, or until it is switched off in a sandbox for a week with no complaint.

The output: keep, kill, rebuild

The survey ends in a written read, not a slide deck. Three lists, each row with the evidence that put it there. The rows below are examples. The real lists run to pages.

List Item (example) Evidence (example)
Keep Opportunity trigger handler, one trigger per object Clean pattern, test coverage, no tickets in twelve months
Keep Case assignment flow Fires in order, two tickets, both training
Kill 41 custom fields on Account No references, zero writes in the last year, no owner
Kill Three Workflow Rules on Lead Duplicated by a record-triggered flow since year seven
Rebuild Five Case triggers Unordered, two of them set the same field
Rebuild Quote approval in Process Builder Hard-coded user IDs, fights two validation rules

The keep list is the base. The kill list goes into a sandbox first, then production, one object at a time. The rebuild list becomes the plan we price. That is what the first week is for. See Salesforce rebuilds for what happens after.

Related service: Salesforce rebuilds. All notes.

Sound familiar?

Have a system like this?

Tell us what fails, and how often. We answer in one business day with a first read and a clear next step.