INSUFFICIENT_ACCESS_ON_CROSS_REFERENCE_ENTITY on an Apex trigger
You are prepping an org for a data load, you go to switch off the triggers, and one of them refuses. The error is long, unhelpful, and sounds like a permissions problem. It usually isn't.
The short answer
In almost every case, the trigger you are trying to deactivate belongs to an installed managed package. Managed package code is locked in the subscriber org. No profile, permission set, or Modify All Data grant will change that, because this is not a permission problem — it is an ownership problem.
Check it in one query. If NamespacePrefix comes back non-null, you have your answer:
Why Salesforce throws this specific error
The wording is generic because the error is generic. Salesforce raises INSUFFICIENT_ACCESS_ON_CROSS_REFERENCE_ENTITY whenever you have rights to perform an operation on record A, but that operation requires access to a related record B that you don't have rights to. The "cross-reference" is the related record.
On an ApexTrigger, the chain looks like this. Deactivating a trigger through the Tooling API is an update to the Status field, and the Tooling API requires you to send the Body alongside Status in the same PATCH — you cannot update one without the other. So even though you only want to flip a boolean-ish status, you are technically submitting the trigger's source code back to Salesforce.
For packaged code, that submission is rejected at the package boundary. The subscriber org holds a read-only copy of the publisher's code. Writing to it — even writing back byte-identical source — is not allowed, so the platform fails the related-entity check and returns this error.
If some triggers in your bulk operation succeed and others fail with this error, sort the failures by namespace. They will line up almost perfectly with your installed packages.
What does not work — stop trying these
| Attempt | Why it fails |
|---|---|
| Granting Modify All Data | Permission was never the constraint. Package ownership is. |
| Author Apex permission | Lets you write your own Apex. Doesn't grant write access to someone else's packaged Apex. |
| Retrying the call | The check is deterministic and server-side. The result will not change. |
| Switching Tooling API → Metadata API | Same package boundary, different transport. Metadata API deployment of a packaged trigger is rejected too. |
| Raising the API version | Not a version-gated behaviour. |
| Sending Status without Body | Returns a different failure. The Tooling API requires both fields together on ApexTrigger. |
What actually works
1. Detect and skip, so the rest of the run succeeds
If you are disabling automations in bulk before a load, the practical move is to filter packaged triggers out of the batch up front rather than letting them fail mid-run. Query NamespacePrefix, partition the list, act on the local triggers, and report the packaged ones separately so nobody thinks they were switched off when they weren't.
This matters more than it sounds. A half-completed disable run is worse than none at all — you go into the load believing automation is off when part of it is still firing.
2. Use the package's own bypass
Most mature ISV packages ship a documented bypass, precisely because their customers hit this during migrations. The common patterns:
- A hierarchy custom setting with a "disable triggers" checkbox you can set at the user level, so only your load user bypasses the logic.
- A custom permission the package's trigger checks before running, which you assign to the migration user.
- A custom metadata type holding per-object or per-trigger toggles.
Check the package's admin guide for "bypass", "disable triggers", or "data loading". Setting a hierarchy custom setting on just the integration user is far safer than an org-wide switch, because your other users keep their validations while the load runs.
3. Ask the publisher
If there is no documented bypass, the publisher's support team can usually tell you whether one exists undocumented, or whether their logic is safe to run during a bulk load. For Salesforce-owned packages this goes through Salesforce Support.
4. Design the load around it
Sometimes the honest answer is that the trigger stays on. In that case, adjust the load rather than the org: smaller batch sizes to stay inside governor limits, staged loads so required lookups exist before dependent records, and a post-load reconciliation pass to catch anything the trigger rewrote.
The other cause: a local trigger in production
If NamespacePrefix is null and you are still blocked, you are hitting a different rule. Salesforce does not permit Apex to be created or modified directly in a production org — that includes flipping a trigger's Status. Sandboxes allow it, production doesn't, which is why this so often surprises people who tested the exact same script in UAT.
The supported path in production is a deployment: mark the trigger inactive in a sandbox or in your source, then deploy via change set, Metadata API, or your CI pipeline, with tests passing. Because that round trip is slow, many teams instead build a bypass into their own triggers — a hierarchy custom setting checked in the first line of the handler — so future loads need no deployment at all. If you own the code, adding that switch once is worth more than solving this error repeatedly.
Frequently asked
Does this error ever appear on things other than triggers?
Yes. It is a general platform error and shows up whenever an operation depends on a related record you can't touch — packaged validation rules, packaged flows, lookups to records outside your sharing access. The diagnostic instinct is the same: find the related entity, ask who owns it.
Can I delete the managed package trigger instead?
No. Packaged components can only be removed by uninstalling the package, which will take its data and dependencies with it. That is not a migration-prep step.
Will the trigger fire during my data load?
Yes, unless the package offers a bypass you have enabled. Plan for it — check governor limit headroom and whether the trigger rewrites any field you are loading.
Why did my bulk script report success?
Some clients treat an HTTP 204 as success without inspecting the per-record result. Always read the response body per record; a silent 204 on a Tooling API PATCH does not guarantee the field changed.
Is there a way to see every packaged automation in the org at once?
Query each metadata type filtering on non-null NamespacePrefix, or use a tool that groups automations by package for you. Knowing the packaged set before a migration is far more useful than discovering it error by error.
Where OrgKit fits
SF Automation Switch was built after hitting exactly this on a Vlocity org mid-migration. It detects packaged automations up front, badges them, and excludes them from bulk operations instead of failing halfway through — so what you think is disabled actually is. It also snapshots the exact pre-migration state so you can revert precisely afterward.
Free, runs in the browser, nothing stored server-side.
Open SF Automation Switch