TL;DR
- Declared relationships are not behavioural coupling. A lookup tells you Contact can point at Account. It does not tell you that saving an Account updates its Contacts.
- Schema Builder, the Dependency API and ERD tools all answer the schema question. The question that breaks your change is which objects move together when a record is saved, and that is established by Flows and Apex.
- You can derive it: parse Active Flows and Apex, record which objects each component reads and writes, then aggregate into object pairs. Each pair gets a weight (how many components establish it), the operations involved, and the components responsible.
- Weight is the signal. A pair established by one class is a detail. A pair established by a trigger, a Flow and two classes is a process, and changing either object will surprise someone.
- Coverage matters more than the graph. Active Flows are parsed; inactive ones are not. A run that reached 2 of 112 Flows and reports "2 of 112" is telling you something true. One that reports a clean graph and stays quiet about what it skipped is not.
- Everything below runs read-only against an org, and the worked example is real output, not an illustration.
What You'll Learn
- Why the schema view and the automation view disagree, and which one predicts breakage
- How to derive a coupling graph from Flows and Apex, and what weight, operations and confidence each pair carries
- How to read the result on a real org, including the pairs that surprise you
- Why the coverage figure is the first number to look at, and what "2 of 112" is telling you
- Where the technique runs out, and what it cannot see
The Problem
You have inherited a Salesforce org. Someone asks whether it is safe to change how Cases are closed.
The honest answer requires knowing what else moves when a Case moves. So you open Schema Builder, and it shows you that Case has a lookup to Contact and to Account. True, and not the answer. The lookup is a declared relationship: it says these records may reference each other. It says nothing about behaviour.
You try the Dependency API, or a metadata dependency report. Better: now you can see that a Flow references the Case object. But the dependency is a reference, not a direction and not an operation. It will tell you Flow X depends on Case. It will not tell you that the Flow fires on Case insert and creates a Task, so that every closed Case quietly generates work for someone.
That last fact is the one that breaks your change. And it is not in the schema, because nobody declared it. It exists only inside the automation.
This is the gap: the objects your org treats as coupled are defined by its automation, not by its data model. Two objects with no relationship at all can be tightly coupled because a trigger writes one whenever the other changes. Two objects joined by master-detail can be entirely independent in practice.
Quick Answer
Parse the automation and aggregate what it touches.
For every Active Flow and every Apex class and trigger, record which objects it reads and which it writes. Then for each pair of objects that appear together in the same component, emit a coupling with:
- a weight: how many distinct components establish this pair
- the operations involved: create, read, update, delete
- the contributing components that established it, so you can go and look
- a confidence level: whether the object was identified from parsed structure or from a regex fallback over source
That is a coupling graph. It answers "what moves together" directly, and it points at the code responsible.
The sf intel map command in @cclabsnz/sf-orgintel does this, and everything in the worked example below is its real output. You can do the same analysis by hand with the Tooling API if you would rather not add a plugin; the technique is the point, not the tool.
Comprehensive Guide
1. What the two views actually show
Put them side by side on the same org:
| Question | Answered by | Example answer |
|---|---|---|
| Can these records reference each other? | Schema: lookups, master-detail | Case has a lookup to Contact |
| Does this component reference that object? | Dependency API | Flow Case_Triage depends on Case |
| Do these objects move together on save? | Parsing the automation | A Flow on Case creates Task, weight 3, create and read |
Only the third one predicts breakage, because only the third one is about behaviour.
2. Deriving the graph
The analysis has three stages.
Retrieve. Pull Active Flow definitions and their metadata, and Apex classes and triggers. For Apex, the Tooling API's SymbolTable gives you parsed structure (the objects a class actually references) without you writing a parser. Where the SymbolTable is unavailable (managed packages withhold bodies, and some classes simply have no symbol table), fall back to a regex over the source and mark the result as lower confidence.
Attribute. For each component, determine the objects it touches and the operation. A Flow's recordCreates, recordUpdates, recordLookups and recordDeletes elements each name an object and imply an operation. Apex DML and SOQL do the same. Record each as a touches edge from the component to the object.
Aggregate. Wherever one component touches two or more objects, those objects are coupled. Emit one edge per pair, accumulate the weight, union the operations, and keep the list of components that established it.
That last step is where the value is. A single component touching Account and Contact is unremarkable. The same pair established independently by four components is a process, and processes are what break.
3. Reading a real graph
Here is actual output from a Developer Edition org seeded with six sample components: three Apex classes, one trigger and two Flows, all touching standard objects.
The run analysed 2 Flows, 25 Apex classes and 1 trigger, and produced seven couplings:
| Pair | Weight | Operations |
|---|---|---|
Account to Contact |
3 | create, read, update |
Case to Task |
3 | create, read |
Account to Opportunity |
1 | read, update |
Account to User |
1 | create |
Case to Contact |
1 | read |
Contact to Task |
1 | create, read |
Contact to User |
1 | create |
Three things are worth pulling out.
Account to Contact at weight 3 with all three operations. Three separate components establish it: a Flow that syncs a field down to related Contacts, a class that creates Contacts from Accounts, and a class that updates them. This is the pair you brief someone on before they touch either object.
Case to Task at weight 3, from two different mechanisms. A trigger creates a Task on high-priority Cases, a Flow creates one on Case insert, and a class creates follow-up Tasks. Three paths to the same outcome, which is exactly the pattern that produces duplicate records and the ticket that follows. You would not find this by reading any one component.
Account to User and Contact to User. Neither is a declared relationship you would think about. They appear because components set OwnerId while touching those objects. Whether that is interesting depends on your org, but it is the kind of thing you want surfaced rather than discovered during a migration.
Notice what the weight-1 rows are doing: they are not noise, they are the long tail. Case to Contact at weight 1, read-only, is a component reading a Contact while processing a Case. Low risk. The graph lets you sort by weight and stop reading when the rows stop mattering.
4. The number to look at first
Before any of that, look at coverage.
That run reported: 2 of 112 Flows analysed. 25 of 28 Apex classes. 1 of 1 triggers.
Two of a hundred and twelve. The org holds 112 Flow definitions and 2 of them are Active, so the analysis reached a small fraction of what exists. The coupling graph above is correct, every pair in it is real, but it is built from 2 Flows, and any conclusion of the form "Account and Case are not coupled" would be unsupported.
This is the distinction that matters, and it is worth naming precisely:
- A census is how many exist.
SELECT COUNT(Id) FROM FlowDefinitionreturns 112. - Analysis coverage is how many were examined. The parser reached 2.
They answer different questions, and neither is a defective version of the other. A tool that reports only the census implies it looked at all of them. A tool that reports only what it analysed implies that is all there is. Reporting the pair, "2 of 112", is the only honest option, and it changes how you read everything downstream.
Ask this of any org-analysis tool you use, including your own scripts: what did it not look at, and does it tell you?
5. Why only Active Flows
Because an inactive Flow does not run, and a coupling that cannot fire is not a coupling.
That is defensible, and it is also a real limit. If you are auditing an org for potential behaviour, say, before someone activates a batch of Flows that have been sitting inactive for a year, the Active-only view understates your risk considerably. Most tools in this space let you opt into inactive Flows; make sure you know which mode you are in before you draw a conclusion.
The same applies to Apex the analysis could not read. A managed-package class with a withheld body and no SymbolTable contributes nothing to the graph. It is not that the class touches no objects; it is that nobody could see.
6. Confidence, and why the fallback matters
Where the SymbolTable is available, an object reference is parsed structure: the class genuinely references that object. Mark it high.
Where the analysis falls back to a regex over source, an object name appearing in a string, a comment, or a dynamic SOQL fragment can produce a match that is not a real reference. Mark it approximate.
The important rule is what happens when a pair is established by a mix. An edge should be high only when every contributing component is high. If nine regex guesses and one SymbolTable hit collapse into a single high label, the graph is reporting inference as fact, and the person reading it has no way to tell.
Advanced Techniques
Sorting by what will actually hurt
Weight alone is a decent proxy, but combine it with two other things you already have:
- Operations. A pair established only by
readedges is observation. A pair withcreateandupdatefrom multiple components is a write path, and write paths break loudly. - Record volume. Couple the graph with 90-day record counts per object. A weight-4 coupling between two objects holding 300 records each is less urgent than a weight-2 coupling on your highest-volume object.
Running it in CI
The analysis is read-only and deterministic, the same org produces the same graph. That makes it viable as a scheduled job: run it weekly, diff the coupling graph against last week's, and alert on new pairs. A new coupling appearing between two objects nobody expected to be related is a genuinely useful signal, and it is much easier to act on the week it appears than a year later.
Diff the coverage figures too. If analysed Flows jump from 2 to 40, someone activated a lot of automation, and your previous conclusions are now stale.
Frequently Asked Questions
Q: Is this not just what the Dependency API gives me?
No. The Dependency API tells you a component references an object. It does not tell you the direction, the operation, or that two objects are joined through that component. Coupling is a property of object pairs; dependencies are a property of single components. You can build the former from the latter, which is roughly what this technique does, but the aggregation step is where the answer appears.
Q: Does this work on a production org?
The analysis is read-only, SOQL, Tooling and Metadata reads and describes. No write path exists. Whether you point a tool at production is your call, but the technique itself does not modify anything. Check that whatever you run makes the same guarantee, and that it does not send org data anywhere.
Q: Why not just read the code?
On a small org, do that. The technique earns its keep when there are more components than one person can hold: 25 classes is readable, 400 is not, and the pairs established by three components in three different files are precisely the ones a human reader misses.
Q: What about Process Builder and Workflow Rules?
Both still exist in plenty of orgs, and both establish couplings. Whether a given tool covers them varies; check. An analysis that silently omits Workflow Rules on an org that leans on them will produce a confident graph with a hole in it, which is worse than no graph.
Q: The graph shows a coupling I do not believe. What now?
Look at the contributing components, that is what they are for. Either the reference is real and you have learned something, or it was an approximate match from a regex hit on a comment or a dynamic query. The confidence label tells you which is likely, and the component list tells you where to look.
Key Takeaways
- Declared relationships and behavioural coupling are different questions. Schema Builder answers the first; only parsing the automation answers the second.
- Weight is the signal. One component establishing a pair is a detail; four is a process, and processes are what break when you change them.
- The same coupling arriving from a trigger and a Flow is a finding, not a duplicate row. It is usually where duplicate records come from.
- Read the coverage figure before the graph. "2 of 112 Flows analysed" changes what every conclusion below it is worth.
- A census and analysis coverage answer different questions. Any tool reporting one while implying the other is misleading you, including a script you wrote yourself.
- Confidence must degrade to its weakest input. An edge is only as trustworthy as the least certain component that established it.
What's Next?
Recommended Reading:
- Salesforce CPU timeout: advanced debugging and architecture for what happens when too much automation fires on one save
- Apex test quality beyond code coverage for testing the couplings you find
- Connect the sf CLI to an org if you are setting up the tooling for the first time
- Salesforce license audit: finding unused licenses for the same auditing instinct applied to licences
Action Items:
- Run a coupling analysis against a sandbox and read the coverage figure first. If the analysed count is a small fraction of the census, find out why before reading the graph.
- Take your three heaviest couplings and check whether each one is deliberate. A pair established by a trigger and a Flow doing the same thing is usually an accident nobody has noticed.
- Schedule the analysis weekly and diff the result. A new coupling appearing between two objects nobody expected is worth a conversation the week it shows up.
Resources & References
- Tooling API: SymbolTable (Salesforce Developers)
- Tooling API: ApexClass
- FlowDefinitionView (Object Reference)
- Flow Metadata API Reference
- MetadataComponentDependency (Dependency API)
- @cclabsnz/sf-orgintel on npm
The worked example, including the full graph fragment this post quotes, is published at cclabsnz/sf-orgintel under examples/. It is real output from a real run, with the org id substituted, so you can see the shape before pointing anything at your own org.
Responses
Checking your session.
Loading responses.