INSUFFICIENT_ACCESS_OR_READONLY: insufficient access rights on cross-reference id
The running user can see the record they're trying to update, but doesn't have edit access to it (or to a record it depends on). The error message looks scarier than it is — usually a sharing problem on the parent, not anything wrong with the SQL.
Also seen asINSUFFICIENT_ACCESS_OR_READONLY·insufficient access rights on cross-reference id·INSUFFICIENT_ACCESS_OR_READONLY: insufficient access
A nightly integration that creates Cases on behalf of partner users has been working for six months. After last week's permission cleanup, the job starts failing for one specific user with INSUFFICIENT_ACCESS_OR_READONLY: insufficient access rights on cross-reference id. The Cases the job tries to create all reference the same Account. The integration user owns no Cases on this Account, never has, and yet the API rejects the insert. The team needs to find what changed.
What the platform is checking
The error message contains a clue most developers miss the first time: the phrase "cross-reference id". The platform isn't complaining about the record you're trying to create. It's complaining about a record you referenced as a lookup or foreign key on that record. Salesforce checks access on every record involved in a DML, not only the primary target.
When you insert a Case with AccountId = 001xx000003DGb2, the platform asks two questions. Does the current user have Create permission on Case? Yes. Does the current user have at least Read access to that specific Account record? If the answer to the second question is no, the platform returns INSUFFICIENT_ACCESS_OR_READONLY: insufficient access rights on cross-reference id.
The "cross-reference id" is the Account record. The user can create Cases in general, but cannot reference that particular Account because the Account's sharing rules don't grant them access. The DML is rejected before any record is written.
The same logic applies to OwnerId on any object, ParentId on Account, AccountId on Contact, ContractId on Order, and every other lookup field. If you write a foreign key, the runtime checks your access to the referenced record.
The broken example
A batch process that creates support Cases for partner Accounts:
public class PartnerCaseCreator {
public static void createCasesForPartner(Id partnerAccountId, List<String> subjects) {
List<Case> cases = new List<Case>();
for (String subject : subjects) {
cases.add(new Case(
AccountId = partnerAccountId,
Subject = subject,
Status = 'New',
Origin = 'Web'
));
}
insert cases;
}
}
The class is correct in isolation. The error only fires when the running user lacks Read access to partnerAccountId. In sandbox, the integration user had a Modify All Data permission set that masked the underlying sharing model. In production, the permission set was scoped down during the cleanup, and the user now relies on standard sharing rules.
A second shape: a trigger handler that reassigns Cases to a queue the user cannot see.
Case c = new Case(Id = caseId, OwnerId = '00Gxx0000004CFkEAM');
update c;
If the running user has no membership in the destination queue and no sharing rule that exposes it, the cross-reference check on OwnerId fails. The user can update Cases they own, but cannot reassign one to a queue they cannot read.
A third shape: a flow that sets ManagerId on a User record to a manager the running user cannot view. User access is governed by the User Sharing settings; if the org has external User Sharing off and the running user lacks visibility to the target manager, the cross-reference check fails.
Why "READONLY" is in the message
The message reads INSUFFICIENT_ACCESS_OR_READONLY. The "OR_READONLY" branch fires for a different but related cause: the record you are trying to modify is itself read-only to you. Read-only state can come from several sources. The record might be in a closed Approval Process step. The record might be locked by a record-locking flow. The record might belong to an inactive user and the org disallows updates to records owned by inactive users.
When triaging, ask both questions. Is there a cross-reference to a record the user cannot see? Or is the primary record itself locked? Either condition produces this error.
The fix, three paths
Grant the user sharing access to the referenced record. The most common cure is adjusting the sharing model so the integration user can see the Accounts it references. Options include adding the user to a public group that owns a sharing rule on the parent records, transferring ownership to the integration user, or adding manual sharing in cases where rule-based access is impractical.
For the batch example, the partner Accounts are owned by partner users. A sharing rule that exposes all partner Accounts to a "Partner Integration" public group, with the integration user as a member, fixes the access path without granting Modify All Data.
Use the System Mode escape hatch. Apex classes annotated without sharing run with full record visibility regardless of the user's sharing model. The cross-reference check still runs, but the running context bypasses sharing.
public without sharing class PartnerCaseCreator {
public static void createCasesForPartner(Id partnerAccountId, List<String> subjects) {
// Insert proceeds even when the calling user cannot see the Account
}
}
Use this judiciously. without sharing removes the sharing layer entirely for the duration of the call. The class becomes a security boundary; everything inside it can read and write every record. Reserve the pattern for trusted system code with a narrow surface area.
Restructure to avoid the cross-reference. If the user genuinely should not see the parent record, you may need to redesign. Perhaps the Cases should be created by a system context (a scheduled Apex job running as a dedicated integration user with broader access) rather than by the partner user's session. Perhaps the AccountId should be deferred and populated by a trigger that runs in system mode.
The fixed example
A version that uses a dedicated service class with explicit sharing semantics:
public without sharing class PartnerCaseService {
public static List<Case> createForPartner(Id partnerAccountId, List<String> subjects) {
if (partnerAccountId == null) {
throw new IllegalArgumentException('partnerAccountId is required');
}
Account parent = [
SELECT Id, OwnerId, Industry
FROM Account
WHERE Id = :partnerAccountId
LIMIT 1
];
List<Case> cases = new List<Case>();
for (String subject : subjects) {
cases.add(new Case(
AccountId = parent.Id,
Subject = subject,
Status = 'New',
Origin = 'Partner Portal'
));
}
insert cases;
return cases;
}
}
The class is without sharing. The query inside is allowed because the class context bypasses sharing rules. The insert is allowed for the same reason. Calling code that invokes PartnerCaseService.createForPartner from a with sharing context delegates only this narrow operation to system mode.
Edge case: object permissions versus record access
INSUFFICIENT_ACCESS_OR_READONLY is about record-level access, not object-level. If the user lacks Create on Case entirely, the error reads INSUFFICIENT_ACCESS_ON_CROSS_REFERENCE_ENTITY or a profile-level message. The cross-reference message means the object permissions are fine; the specific record is the problem.
When triaging, run a quick test. Can the user create a Case with no AccountId? If yes, the cross-reference is the issue. If no, the profile or permission set lacks Create on Case and that is the first thing to fix.
Edge case: implicit sharing on related records
Salesforce grants implicit Read access to parent Account when a user owns a Contact, Opportunity, or Case on that Account. The implicit grant flows in one direction only. Owning a Case on an Account grants Read on that Account, but owning the Account does not automatically grant access to every Case.
For integrations that bootstrap related records, the dependency order matters. Insert the parent first (which establishes ownership and implicit access), then insert children. Inserting a child that references a parent you cannot yet see is the textbook reproduction of this error.
Edge case: cross-reference inside a managed package
When you reference a record owned by a managed package's namespaced object, the package's sharing model applies. If the package uses with sharing internally and your running user lacks access, the cross-reference fails. Packages can opt into Customer License sharing settings that grant visibility, but those settings live in Setup under the package's profile or permission set.
If the error fires after installing or upgrading a managed package, check the package's release notes for sharing-model changes and the post-install steps the publisher recommends.
Test patterns
Tests for sharing-sensitive code should run as the constrained user, not as the test executor.
@IsTest
static void caseCreationFailsWithoutAccountAccess() {
User integrationUser = TestUtil.createUser('Partner Integration');
Account hiddenAccount = TestUtil.createAccount(); // owned by another user
System.runAs(integrationUser) {
try {
insert new Case(AccountId = hiddenAccount.Id, Subject = 'Test');
System.assert(false, 'Expected DML exception');
} catch (DmlException e) {
System.assert(e.getMessage().contains('INSUFFICIENT_ACCESS_OR_READONLY'));
}
}
}
The test sets up a user who explicitly lacks access, then runs the DML inside System.runAs. The assertion confirms the platform produces the expected exception. A second test should verify the positive case: after a sharing rule is in place, the same insert succeeds.
Diagnosing in production
A live incident usually requires a sandbox refresh or a targeted permission grant. The investigation steps:
- Capture the failing record's payload (which lookup fields are populated, what values they hold).
- Run a SOQL query as the affected user:
SELECT Id FROM Account WHERE Id = '...'. If no row returns, the user cannot see that parent record. - Inspect the user's profile, permission sets, role hierarchy position, and applicable sharing rules.
- Identify the gap. Often the user belongs to a role whose hierarchy access was changed, or a permission set was unassigned.
- Add a sharing rule, public group membership, or manual share that closes the gap.
For partner and customer community users, also verify the External Sharing Model on the parent object. Community users have separate internal and external sharing settings.
Defensive habits
Document which classes run with sharing versus without sharing, and why. A code review checklist that asks "does this DML reference a record the running user may not see" catches many of these issues before they ship.
Prefer System Mode Apex over scattered cross-reference grants. A single without sharing service class with a narrow contract is easier to audit than a sharing model with twenty special-case rules.
For integration users, design the permission set to grant Read on every object the integration writes lookups to. Even when the integration uses without sharing service classes, having the underlying access in place provides a safety net if a future refactor inadvertently changes the sharing context.
Add automated tests that run as a typical end user. Many production incidents come from code that works in the developer's sandbox where every test user has Modify All Data and fails in production where users have realistic profiles.
Quick recovery checklist
When the error fires:
- Identify which record the cross-reference message refers to (the lookup field on the failing DML).
- Run a query as the affected user to confirm the user cannot see that record.
- Choose a fix: grant sharing access, move the DML to a
without sharingclass, or restructure. - Add a regression test that runs as a non-admin user.
- Deploy and verify in a sandbox that mirrors production permissions before pushing to production.
Most incidents resolve quickly once the cross-reference is identified. The longer ones involve sharing models that have drifted over time and need a focused audit.
Further reading from Salesforce
Related dictionary terms
Share this fix
Related Security errors
ENTITY_FAILED_IFLASTMODIFIED_ON_UPDATE_CHECK: record has been modified since you retrieved it
SecurityYou sent an update with an `If-Unmodified-Since` header (or the API equivalent), and the record was modified by someone else after your read…
Insufficient Privileges. You do not have the level of access necessary to perform the operation you requested.
SecurityObject-level access — the user lacks Read/Edit/Create/Delete on the object itself, not on a particular row. Different from "You don't have a…
INVALID_CROSS_REFERENCE_KEY: invalid cross reference id
SecurityYou set a relationship field to an Id that the platform rejects — either the Id doesn't exist, points at the wrong object type, has been del…
invalid_grant: <reason>
SecuritySalesforce rejected the OAuth credentials in your token request. The `error_description` field tells you which part — refresh token revoked/…
invalid_grant: invalid_grant - challenge required / TWO_FACTOR_REQUIRED
SecuritySalesforce required Multi-Factor Authentication for the login but the integration didn't supply it. Most common with Username-Password OAuth…