Salesforce Dictionary - Free Salesforce GlossarySalesforce Dictionary
All errors
Security

INVALID_CROSS_REFERENCE_KEY: invalid cross reference id

You 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 deleted, or refers to a record the running user can't see.

Also seen asINVALID_CROSS_REFERENCE_KEY·invalid cross reference id·INVALID_CROSS_REFERENCE_KEY: invalid cross

A Flow that creates a Task and assigns it to an Account owner has been running silently for months. This morning it starts failing with INVALID_CROSS_REFERENCE_KEY: invalid cross reference id. The accounts haven't changed. The flow hasn't changed. Something in the data has shifted and the platform is rejecting the reference.

What the platform is actually checking

INVALID_CROSS_REFERENCE_KEY fires when one record references another record by Id (or by an external id used as a foreign key) and the platform cannot resolve the target. The reference is "invalid" in one of several ways: the referenced record doesn't exist, the user lacks access to it, the id is malformed, or the id points to the wrong object type.

The platform doesn't trust your code to know whether a target exists. Every cross-reference write (an AccountId on a Contact, an OwnerId on a Task, a ParentId on a Custom Object, a polymorphic WhoId on a Task) is checked at write time. If the check fails, the entire DML is rejected.

The error message is generic. The cause is usually one of four specific things.

The broken example

A Flow setting a Task's WhatId to a value that has been deleted:

Task t = new Task(
    Subject = 'Follow up on opportunity',
    WhatId = '006xx000004CABC',
    OwnerId = '005xx000001DEFG',
    ActivityDate = Date.today().addDays(7)
);
insert t;

If the Opportunity 006xx000004CABC was deleted or merged before this code ran, the insert throws INVALID_CROSS_REFERENCE_KEY: invalid cross reference id. The reference is invalid because the target no longer exists.

A second shape, polymorphic confusion:

Task t = new Task(
    Subject = 'Call about the contract',
    WhoId = '001xx000003DGb1',  // This is an Account Id
    ActivityDate = Date.today()
);
insert t;

WhoId on Task is polymorphic but only accepts Contact and Lead ids. Putting an Account Id (which starts with 001) in WhoId throws the same error. Account Ids belong in WhatId, not WhoId.

A third shape, an id from a different org:

Account a = new Account(
    Name = 'Acme Subsidiary',
    ParentId = '001a0000004BdEf'  // Id from a different sandbox
);
insert a;

If you copied an Id from another org (a developer pasted from a test sandbox into a production deploy), the production org's Account table doesn't have that record. The reference fails.

Three paths to a fix

The three causes ranked by frequency:

The referenced record was deleted. This is the most common cause when the error suddenly appears in previously-working code. Use the Recycle Bin to confirm. In SOQL, query IsDeleted = true records with ALL ROWS to find deleted records by id. If the deletion was intentional and your code references that record by id stored elsewhere (a custom field, a cached list, a queued job's payload), the stored reference is stale. Either undelete the target, or update the code to handle missing references gracefully.

The user running the code lacks access to the record. Sharing rules, role hierarchy, and FLS can hide a record from the running user even when the record exists. The platform reports this as INVALID_CROSS_REFERENCE_KEY rather than INSUFFICIENT_ACCESS to avoid leaking information about records the user can't see. The fix is to either grant access via sharing or run the code as a user who has access (consider with sharing vs without sharing carefully).

The id type doesn't match the field's allowed targets. Polymorphic fields like WhoId, WhatId, and OwnerId accept only specific object types. Check the field's referenceTo metadata to see which types are allowed. Use a Schema.getGlobalDescribe() lookup or describe the specific field to verify before insert.

The fixed example

A defensive insert that handles all three classes of failure:

public class TaskCreationService {
    public class TaskCreationException extends Exception {}

    public static Id createFollowupTask(Id targetId, Id ownerId, String subject) {
        if (targetId == null || ownerId == null) {
            throw new TaskCreationException('targetId and ownerId are required');
        }

        // Validate the target exists and is visible to the running user.
        List<SObject> targetCheck = Database.query(
            'SELECT Id FROM ' + targetId.getSObjectType().getDescribe().getName()
            + ' WHERE Id = :targetId LIMIT 1'
        );
        if (targetCheck.isEmpty()) {
            throw new TaskCreationException(
                'Target record ' + targetId + ' not found or not visible.'
            );
        }

        Schema.SObjectType targetType = targetId.getSObjectType();
        String prefix = targetType.getDescribe().getKeyPrefix();
        Task t = new Task(
            Subject = subject,
            OwnerId = ownerId,
            ActivityDate = Date.today().addDays(7)
        );

        // Route the id to the right field based on the target type.
        if (prefix == '003') {
            t.WhoId = targetId;  // Contact
        } else if (prefix == '00Q') {
            t.WhoId = targetId;  // Lead
        } else {
            t.WhatId = targetId;  // everything else (Account, Opportunity, etc)
        }

        try {
            insert t;
            return t.Id;
        } catch (DmlException dml) {
            throw new TaskCreationException(
                'Task insert failed: ' + dml.getMessage()
            );
        }
    }
}

Three guards in one method: null check on inputs, existence check on the target, type-correct routing of the id. The wrapping exception carries a meaningful message to the caller.

The key-prefix trick

Every Salesforce object has a three-character key prefix that identifies its type. The prefix is the first three characters of every record id of that type. A few common ones:

  • 001 Account
  • 003 Contact
  • 00Q Lead
  • 006 Opportunity
  • 00T Task
  • 005 User
  • 00G Group
  • 0F9 Public Group / Queue
  • 500 Case

The prefix lets you sanity-check ids before using them. If you expect a Contact and the id starts with 001, you have an Account, and the cross-reference will fail.

public static Boolean isContactId(Id recordId) {
    return recordId != null
        && String.valueOf(recordId).startsWith('003');
}

This is cheap and catches a class of integration bugs early.

When OwnerId is the troublemaker

OwnerId on most objects accepts either User ids or Queue ids. Queue ids have a key prefix that varies by Salesforce version (commonly 00G for group-based queues). If your code assumes OwnerId is always a user, queue ownership will surprise you.

Validate appropriately:

Id ownerId = task.OwnerId;
String ownerType = ownerId.getSObjectType().getDescribe().getName();
if (ownerType == 'User') {
    // Personal owner; reassignment via UserId logic.
} else if (ownerType == 'Group') {
    // Queue owner; reassignment via group membership.
}

A task assigned to a queue that gets deleted will surface INVALID_CROSS_REFERENCE_KEY on the next update that touches OwnerId. Watch for this in migration scripts.

When the id format is wrong

Ids come in two flavors: 15-character (case-sensitive) and 18-character (case-insensitive). Most APIs return the 18-character form. Both work as inputs, but truncating or padding an id breaks it.

A 15-character id has 15 characters of alphanumerics. An 18-character id has the same 15 followed by a 3-character checksum that lets the system handle case-insensitive comparisons.

Id a18 = '001xx000003DGb1AAAA';  // 18 chars; valid
Id a15 = '001xx000003DGb1';       // 15 chars; valid (case sensitive)
Id aBad = '001xx000003DGb';       // 14 chars; invalid; throws on Id assignment

If your code receives ids from a CSV file or a JSON payload, validate the length and shape before constructing an Id value. The cast itself will throw System.StringException: Invalid id for malformed values, which is its own neighbor error.

The sharing visibility trap

Sharing in Apex is governed by class-level with sharing and without sharing declarations. A with sharing class restricts SOQL to records the running user can see. The same restriction applies to cross-reference checks: if the user can't see the target, the insert fails with INVALID_CROSS_REFERENCE_KEY.

This shows up most painfully in user-context Apex (Lightning Component handlers, Aura controllers) where the running user is whoever's clicking the button. A user without permission to read Opportunity won't be able to create a Task linked to an Opportunity, even if they have full permission on Task.

The fix has three flavors:

Use sharing rules or manual sharing to grant the user access to the targets they need.

Run the cross-reference write in without sharing mode if business rules allow it (the action genuinely should bypass sharing). Be conservative; bypassing sharing is a security-sensitive decision.

Restructure to avoid the cross-reference entirely. If the user doesn't need to see the target, maybe the task should reference something they can see.

Test patterns

Three tests catch most of the failure modes:

A happy-path test that creates the target record, inserts the task, asserts the task was created and references the target.

A deleted-target test that creates a target, deletes it, then attempts the task insert. Confirms the failure mode (either an exception or a graceful skip).

A cross-type test that passes the wrong type of id to a polymorphic field. Confirms the validation rejects it before the DML.

@isTest
static void taskCreation_failsForDeletedTarget() {
    Opportunity opp = new Opportunity(
        Name = 'Test Opp',
        StageName = 'Prospecting',
        CloseDate = Date.today().addDays(30)
    );
    insert opp;
    Id oppId = opp.Id;
    delete opp;

    Test.startTest();
    try {
        TaskCreationService.createFollowupTask(oppId, UserInfo.getUserId(), 'Follow up');
        System.assert(false, 'Expected TaskCreationException for deleted target');
    } catch (TaskCreationService.TaskCreationException ex) {
        System.assertNotEquals(null, ex.getMessage());
    }
    Test.stopTest();
}

When the error fires in bulk DML

A single bad reference in a list-based DML can either fail the whole operation or skip just the bad row, depending on whether you use the Database.insert(list, false) partial-success form or the bare insert list all-or-nothing form.

Database.SaveResult[] results = Database.insert(taskList, false);
List<Integer> failedIndexes = new List<Integer>();
for (Integer i = 0; i < results.size(); i++) {
    if (!results[i].isSuccess()) {
        for (Database.Error e : results[i].getErrors()) {
            if (e.getStatusCode() == StatusCode.INVALID_CROSS_REFERENCE_KEY) {
                failedIndexes.add(i);
            }
        }
    }
}
System.debug('Cross-reference failures at indexes: ' + failedIndexes);

For bulk loads where some failures are expected, the partial-success form preserves the good records and gives you a list of the bad ones to retry or fix.

Related errors and how to tell them apart

The reference-failure family has cousins worth recognizing:

  • INVALID_CROSS_REFERENCE_KEY: invalid cross reference id (the target is missing, deleted, or invisible)
  • MALFORMED_ID: invalid id (the id string isn't a valid Salesforce id at all)
  • INSUFFICIENT_ACCESS_OR_READONLY (the user can see the target but lacks edit permission)
  • FIELD_FILTER_VALIDATION_EXCEPTION (the target record exists but doesn't satisfy a lookup filter on the field)
  • INVALID_TYPE_ON_FIELD_IN_RECORD (the id is a real Salesforce id but points to the wrong object type)

Each has its own remediation. The lookup-filter case is especially sneaky: the target exists, but a filter set in field configuration ("Account must be Active") prevents it being used as the lookup target.

The lookup filter case

Lookup filters (configured in Setup, then the object, then the field, then Lookup Filter) restrict which records can be selected as the target of a lookup. They're enforced at write time. A code path that worked yesterday can fail today if an admin added a lookup filter that the existing data references don't satisfy.

If FIELD_FILTER_VALIDATION_EXCEPTION fires, investigate which filter the target record fails. Often you'll find it via Setup, then the field, then Lookup Filter. Either widen the filter, fix the target record, or change the reference.

Stored ids in custom fields

A common pattern: store a related record's id in a Text field for later reference. This works but introduces a maintenance burden. If the referenced record is deleted, the stored id is orphaned. There's no automatic cleanup.

The reliable pattern is to use a Lookup field for the relationship, which gets cascade-aware behavior from the platform (deletion of the parent either prevents deletion or sets the lookup to null, depending on field configuration). Use Text fields only when the relationship is to an external system, not to another Salesforce record.

If you must use Text-stored ids, periodically audit and clean up dangling references with a scheduled batch.

Habits that prevent this

Validate id format on every external input. A short helper like Id parsed = Id.valueOf(input) will throw if the input isn't a valid id, catching the malformed case early.

Don't store hard-coded ids in source code. Production and sandbox ids differ. Use developer name lookups for static metadata: [SELECT Id FROM RecordType WHERE DeveloperName = 'Internal' AND SObjectType = 'Account'].

When introducing a new lookup field, audit existing data for references before deploying. A migration script can flag dangling references before they become production errors.

Further reading from Salesforce

Related dictionary terms

Share this fix

Share on LinkedInShare on X

Related Security errors