Salesforce Dictionary - Free Salesforce GlossarySalesforce Dictionary
All errors
Validation

ENTITY_IS_LOCKED: entity is locked for approval

The record is currently in an Approval Process and is locked for editing until the approval is granted, rejected, or recalled. The lock is enforced at the record level — even an admin can't edit without unlocking first.

Also seen asENTITY_IS_LOCKED·entity is locked for approval·ENTITY_IS_LOCKED: entity is locked·record locked approval

A sales rep clicks Save on an opportunity to update the close date and the platform throws back ENTITY_IS_LOCKED: entity is locked for approval. The opportunity is in the middle of a Discount Approval process; until the approver either approves or rejects, the record is frozen. The rep is irritated; the platform is doing exactly what the admin who built the approval process intended.

What the platform is enforcing

Salesforce's Approval Processes lock records while they're awaiting a decision. The lock blocks all DML on the locked record (insert is fine for new records, but update, delete, and undelete fail). The lock is enforced at the platform level regardless of who tries to edit: admins, integration users, and Apex code all hit the same wall.

The lock has three states from the user's perspective:

  • Locked: the record is currently in approval, no edits allowed.
  • Unlocked, approved: the approval completed and the record is editable again (any final field updates from the approval may have applied).
  • Unlocked, rejected: same as approved, but the record is unchanged from its pre-approval state.

The error ENTITY_IS_LOCKED fires only during the Locked state.

The broken example

A nightly job that adjusts opportunity close dates fails on any opportunity currently in approval:

public class OpportunityCloseDateAdjuster {
    public static void rollForwardCloseDates(Set<Id> oppIds) {
        List<Opportunity> opps = [SELECT Id, CloseDate FROM Opportunity WHERE Id IN :oppIds];
        for (Opportunity o : opps) {
            o.CloseDate = o.CloseDate.addDays(30);
        }
        update opps;   // Throws ENTITY_IS_LOCKED for any opp currently in approval
    }
}

The update succeeds for unlocked records and fails for locked ones. If you use the non-partial-success variant of update, the whole batch rolls back. If you use Database.update(opps, false), the locked records fail individually with ENTITY_IS_LOCKED while others succeed.

The fix: detect and skip locked records, or use Approval methods

Two paths depending on intent.

Path 1: Skip locked records. Query for the lock state and filter:

public static void rollForwardCloseDates(Set<Id> oppIds) {
    Map<Id, Opportunity> opps = new Map<Id, Opportunity>(
        [SELECT Id, CloseDate FROM Opportunity WHERE Id IN :oppIds]
    );

    Map<Id, Boolean> lockedMap = Approval.isLocked(opps.keySet());
    List<Opportunity> toUpdate = new List<Opportunity>();
    for (Opportunity o : opps.values()) {
        if (lockedMap.get(o.Id) == true) continue;
        o.CloseDate = o.CloseDate.addDays(30);
        toUpdate.add(o);
    }
    if (!toUpdate.isEmpty()) update toUpdate;
}

Approval.isLocked returns a Map of id to boolean indicating each record's lock state. Filter out the locked ones; update the rest. The job completes successfully for unlocked records.

Path 2: Programmatically unlock, edit, re-lock. When you genuinely need to edit a locked record, use the Approval class to unlock first:

Approval.UnlockResult[] unlocked = Approval.unlock(new List<Id>{ recordId });
// Edit the record
update record;
// Re-lock if appropriate
Approval.LockResult[] relocked = Approval.lock(new List<Id>{ recordId });

Unlock requires the calling user to have "Modify All" on the object, or to be a System Administrator. Use this path sparingly; bypassing approvals can violate audit assumptions.

The fixed example

A complete adjuster service with both paths:

public class OpportunityCloseDateAdjuster {
    public class Result {
        @AuraEnabled public Id opportunityId;
        @AuraEnabled public Boolean updated;
        @AuraEnabled public String skippedReason;
    }

    public static List<Result> rollForwardCloseDates(Set<Id> oppIds) {
        if (oppIds == null || oppIds.isEmpty()) return new List<Result>();

        Map<Id, Opportunity> opps = new Map<Id, Opportunity>(
            [SELECT Id, CloseDate FROM Opportunity WHERE Id IN :oppIds]
        );

        Map<Id, Boolean> lockedMap = Approval.isLocked(opps.keySet());
        List<Opportunity> toUpdate = new List<Opportunity>();
        List<Result> results = new List<Result>();
        Map<Id, Integer> resultIdx = new Map<Id, Integer>();

        for (Opportunity o : opps.values()) {
            Result r = new Result();
            r.opportunityId = o.Id;
            if (lockedMap.get(o.Id) == true) {
                r.updated = false;
                r.skippedReason = 'Record is in approval and locked';
                results.add(r);
                continue;
            }
            o.CloseDate = o.CloseDate.addDays(30);
            resultIdx.put(o.Id, results.size());
            results.add(r);
            toUpdate.add(o);
        }

        if (!toUpdate.isEmpty()) {
            Database.SaveResult[] saveResults = Database.update(toUpdate, false);
            for (Integer i = 0; i < saveResults.size(); i++) {
                Result r = results[resultIdx.get(toUpdate[i].Id)];
                if (saveResults[i].isSuccess()) {
                    r.updated = true;
                } else {
                    r.updated = false;
                    r.skippedReason = 'DML failed: ' + saveResults[i].getErrors();
                }
            }
        }
        return results;
    }
}

The service reports per-record outcomes. Locked records are skipped with a clear message; other failures (validation, permission) are also captured.

Approval methods at a glance

The Approval class exposes a small set of static methods:

MethodReturnsPurpose
Approval.lock(ids)LockResult[]Lock the records, regardless of approval process
Approval.unlock(ids)UnlockResult[]Unlock the records, regardless of approval process
Approval.isLocked(ids)Map<Id, Boolean>Check lock state for each id
Approval.process(approvalProcessSubmitRequest)ProcessResult[]Submit a record into an approval process

The lock and unlock methods are powerful and bypass approval state. Use them with care; the audit trail records the action.

When the lock is a feature, not a bug

A team's first reaction to ENTITY_IS_LOCKED is sometimes "the approval process is in the way; let's bypass it." Often the right response is the opposite: the approval process is doing its job, and the code that's trying to bypass it is the problem.

Three questions help calibrate the response:

  • Is the edit attempting to circumvent the approval's intent (changing the field the approver is reviewing)?
  • Is the edit incidental (changing a different field that has nothing to do with the approval)?
  • Is the edit part of the approval workflow itself (a final field update from a recalled approval)?

For incidental edits, the right fix is to skip locked records. For circumvention attempts, the right fix is usually "don't"; if the business genuinely needs to edit during approval, the approval design is probably wrong. For approval-workflow edits, the Approval class methods are the right tool.

Lock state vs the record's other state

ENTITY_IS_LOCKED is separate from:

  • Sharing settings (the user can see but not edit due to sharing rules): INSUFFICIENT_ACCESS_OR_READONLY.
  • Field-level security blocking specific fields: INSUFFICIENT_ACCESS.
  • Validation rules: FIELD_CUSTOM_VALIDATION_EXCEPTION.
  • Record-type restrictions: various, often specific to the rule.

The error message names the cause unambiguously. If you see ENTITY_IS_LOCKED, the cause is an approval lock. Other access-related errors have their own codes.

Auto-unlock on approval/recall

When an approval is approved or rejected, the platform automatically unlocks the record. Code that polls for "unlocked state" can use Approval.isLocked periodically; once the platform unlocks, the call returns false.

A common pattern: a Queueable that re-tries the update after a short delay, checking the lock state each time:

public class RetryUpdateQueueable implements Queueable {
    private Id recordId;
    private Integer attempts;

    public RetryUpdateQueueable(Id id) { this.recordId = id; this.attempts = 0; }

    public void execute(QueueableContext qc) {
        Boolean isLocked = Approval.isLocked(new List<Id>{recordId}).get(recordId);
        if (!isLocked) {
            // Now safe to update
            Opportunity o = [SELECT Id FROM Opportunity WHERE Id = :recordId];
            // Apply edit
            update o;
            return;
        }
        if (attempts < 5 && !Test.isRunningTest()) {
            attempts++;
            System.enqueueJob(this);   // Re-queue
        }
    }
}

The chain polls every few minutes until the approval clears, then applies the edit. Use this for time-sensitive edits where the approval might complete within minutes.

Apex tests and approval locks

Apex tests can submit records to approval processes via Approval.process() and then verify the lock state. The test runs in its own transaction; the lock is real but cleans up at Test.stopTest().

@isTest
static void test_lockedRecordSkipped() {
    Opportunity o = new Opportunity(Name = 'Test', StageName = 'Prospecting', CloseDate = Date.today());
    insert o;

    // Submit to approval (assumes the org has an active process for this object)
    Approval.ProcessSubmitRequest req = new Approval.ProcessSubmitRequest();
    req.setObjectId(o.Id);
    Approval.process(req);

    Test.startTest();
    List<OpportunityCloseDateAdjuster.Result> results = OpportunityCloseDateAdjuster.rollForwardCloseDates(new Set<Id>{o.Id});
    Test.stopTest();

    System.assertEquals(false, results[0].updated, 'Locked record should be skipped');
    System.assertNotEquals(null, results[0].skippedReason);
}

The test confirms the service handles the lock correctly.

A subtle bug in legacy code

Some older Apex code locks records explicitly via Approval.lock for non-approval reasons (e.g., as a manual freeze during data migration). If that code never unlocks, the lock persists indefinitely. Users see ENTITY_IS_LOCKED with no active approval process to explain it.

The diagnostic: query ProcessInstance to see if the record is in an active approval. If no, the lock is from Approval.lock and only Approval.unlock can release it. Audit for stray Approval.lock calls in the codebase.

Cascading locks via master-detail

Master-detail relationships have a configuration option called "Lock children" (in the master object's metadata). When enabled, locking a parent record automatically locks all its master-detail children. Trying to edit a child when its parent is in approval produces the same ENTITY_IS_LOCKED error.

The diagnostic: when a child record refuses an edit and there's no approval on the child itself, check the parent's approval state. If the parent is locked, the children inherit the lock.

The behavior is correct (you usually don't want children edited while a parent is in approval), but it can be surprising the first time you hit it. Document the cascade behavior in your approval process design.

Approval-aware UI patterns

For Lightning components that need to gracefully handle locked records, query the lock state and adjust the UI:

import { LightningElement, api, wire } from 'lwc';
import isLocked from '@salesforce/apex/RecordLockChecker.isLocked';

export default class EditableSummary extends LightningElement {
    @api recordId;
    @wire(isLocked, { recordId: '$recordId' }) lockState;

    get canEdit() {
        return this.lockState?.data === false;
    }

    get lockMessage() {
        return this.lockState?.data === true
            ? 'This record is in approval and cannot be edited.'
            : null;
    }
}

The component disables edit controls and shows a polite message when the record is locked. Users get clear feedback instead of seeing the save fail at submit time.

Long-running approvals

A poorly-designed approval process can lock records for days while approvers slowly act. The lock blocks all downstream edits, including legitimate non-approval updates. Two patterns help:

  • Set reasonable approval timeouts. Setup → Process Automation → Approval Processes → your process → Final Approval Actions includes timeout settings.
  • Audit pending approvals weekly. Build a report on ProcessInstance filtered to Status = 'Pending' and CreatedDate < LAST_N_DAYS:7. Long-pending approvals usually mean an approver has left the company or is unaware of the queue.

Healthy approval workflows complete in hours, not days. The lock duration is a signal of organizational hygiene.

When to bypass with elevated permissions

Sometimes a legitimate maintenance operation needs to edit a record regardless of approval state. The "Modify All Data" permission allows the user to use Approval.unlock, edit, and Approval.lock again. The user needs the permission; the bypass leaves an audit trail in the ProcessInstance history.

The pattern is appropriate for:

  • One-time data migrations that must touch records in approval.
  • Mass updates triggered by leadership decisions that supersede the approval.
  • Bug fixes on approval-process metadata that require touching in-flight records.

It's not appropriate for routine edits. The cost of bypassing the lock is auditability; use the elevated path sparingly and document each invocation.

Multi-step approval processes

Approval processes can have multiple steps. A record can be at step 2 of a 4-step approval, locked the whole time. The record stays locked until all steps complete (or the process is recalled).

For multi-step approvals, the lock applies from the first step submit through the last step's outcome. Recall by the submitter (if allowed by the process configuration) unlocks the record without completing the approval.

Logging and diagnostics

For production debugging, include the lock state in your error-handling output:

catch (DmlException ex) {
    Boolean isLocked = Approval.isLocked(new List<Id>{recordId}).get(recordId);
    System.debug(LoggingLevel.ERROR,
        'Update failed. Lock state: ' + isLocked
        + '; DML status: ' + ex.getDmlType(0)
        + '; Message: ' + ex.getMessage()
    );
}

The diagnostic distinguishes between "the record is locked" and other DML failures. If your code routinely catches DmlException without checking lock state, you might be misattributing approval-lock failures to other causes.

Pattern library: approval-aware service classes

A clean architecture for any service that updates records on objects with approval processes: every public method checks lock state and either skips or routes locked records to a separate retry queue. The caller sees a result tuple (success, skipped-due-to-lock, error) and can respond appropriately.

This pattern means no individual code path "fails on lock"; the failure is a known outcome with a documented response. The codebase becomes more predictable under approval-heavy workloads.

Further reading from Salesforce

Related dictionary terms

Share this fix

Share on LinkedInShare on X

Related Validation errors