How to add a Record Lock Status decision element in Flow

Here’s a fun one. You have a record-triggered Flow that locks a Case when it closes, so nobody can edit it afterward. Well, nobody except admins. Admins can edit locked records, so when an admin reopens a closed Case and changes it, nothing ever unlocks it. The Case is open again, but it’s still locked.

Then someone closes it again. The Flow tries to lock a record that’s already locked, Salesforce throws an UNABLE_TO_LOCK_RECORD error, the whole Flow fails, and whoever clicked Save gets a cheerful “We hit a snag” instead of a closed Case. An unlock step can trip the same way on a record that isn’t locked. The fix for both: check the record’s lock status first, then use a Decision element so Flow only locks or unlocks when it actually needs to.

Salesforce 'We hit a snag' error showing UNABLE_TO_LOCK_RECORD when closing an already-locked Case
What happens when an admin reopens a locked Case and then tries to close it again.
Prefer to watch? Here’s the whole fix, start to finish.

Flow doesn’t have a built-in element for checking whether a record is locked, but Apex does. So we’re going to build a tiny invocable Apex action that Flow can call. Both code snippets are below with copy buttons, so you don’t have to write a single line yourself.

Before you start

Step 1: Create the Apex action

This class uses Approval.isLocked() to return a simple True or False. It works on Cases or any other lockable object, and it’s bulk-safe, so it won’t fall over when your Flow runs on a pile of records at once. In Flow Builder, it shows up as Check If Record Is Locked under the Approvals category.

CheckRecordLockStatus.cls
/**
 * Invocable action for Flow: returns whether a record is currently locked
 * (approval-process lock or Approval.lock()).
 *
 * Works for Cases or any other lockable object, so it can be reused.
 * Bulk-safe: when the flow runs for many records in one transaction,
 * Flow batches the calls into a single isLocked() check.
 */
public with sharing class CheckRecordLockStatus {

    public class Request {
        @InvocableVariable(label='Record ID' description='ID of the record to check (e.g. the Case ID)' required=true)
        public Id recordId;
    }

    public class Result {
        @InvocableVariable(label='Is Locked' description='True if the record is currently locked')
        public Boolean isLocked;
    }

    @InvocableMethod(
        label='Check If Record Is Locked'
        description='Returns True if the record is already locked, False if it is not.'
        category='Approvals'
    )
    public static List<Result> checkLock(List<Request> requests) {
        // Collect IDs for one bulk lock check
        List<Id> recordIds = new List<Id>();
        for (Request req : requests) {
            if (req != null && req.recordId != null) {
                recordIds.add(req.recordId);
            }
        }

        Map<Id, Boolean> lockStatusById = recordIds.isEmpty()
            ? new Map<Id, Boolean>()
            : Approval.isLocked(recordIds);

        // Results must come back in the same order as the requests
        List<Result> results = new List<Result>();
        for (Request req : requests) {
            Result res = new Result();
            res.isLocked = (req != null && req.recordId != null && lockStatusById.containsKey(req.recordId))
                ? lockStatusById.get(req.recordId)
                : false;
            results.add(res);
        }
        return results;
    }
}

Step 2: Create the test class

Salesforce won’t let Apex into production without test coverage, so here’s the test class. It has four tests: an unlocked Case, a locked Case, a mixed batch where the order of results matters, and a blank ID. Together they give the action 100% coverage.

CheckRecordLockStatusTest.cls
@IsTest
private class CheckRecordLockStatusTest {

    @TestSetup
    static void setup() {
        insert new List<Case>{
            new Case(Subject = 'Unlocked Case', Status = 'New', Origin = 'Phone'),
            new Case(Subject = 'Locked Case',   Status = 'New', Origin = 'Phone')
        };
    }

    @IsTest
    static void returnsFalseForUnlockedCase() {
        Case c = [SELECT Id FROM Case WHERE Subject = 'Unlocked Case' LIMIT 1];

        Test.startTest();
        List<CheckRecordLockStatus.Result> results =
            CheckRecordLockStatus.checkLock(new List<CheckRecordLockStatus.Request>{ buildRequest(c.Id) });
        Test.stopTest();

        System.assertEquals(1, results.size());
        System.assertEquals(false, results[0].isLocked, 'Unlocked case should return false');
    }

    @IsTest
    static void returnsTrueForLockedCase() {
        Case c = [SELECT Id FROM Case WHERE Subject = 'Locked Case' LIMIT 1];
        Approval.lock(c.Id);

        Test.startTest();
        List<CheckRecordLockStatus.Result> results =
            CheckRecordLockStatus.checkLock(new List<CheckRecordLockStatus.Request>{ buildRequest(c.Id) });
        Test.stopTest();

        System.assertEquals(true, results[0].isLocked, 'Locked case should return true');
    }

    @IsTest
    static void handlesBulkMixedAndPreservesOrder() {
        Case unlockedCase = [SELECT Id FROM Case WHERE Subject = 'Unlocked Case' LIMIT 1];
        Case lockedCase   = [SELECT Id FROM Case WHERE Subject = 'Locked Case' LIMIT 1];
        Approval.lock(lockedCase.Id);

        Test.startTest();
        List<CheckRecordLockStatus.Result> results = CheckRecordLockStatus.checkLock(
            new List<CheckRecordLockStatus.Request>{
                buildRequest(lockedCase.Id),
                buildRequest(unlockedCase.Id)
            }
        );
        Test.stopTest();

        System.assertEquals(2, results.size());
        System.assertEquals(true,  results[0].isLocked);
        System.assertEquals(false, results[1].isLocked);
    }

    @IsTest
    static void nullIdReturnsFalse() {
        Test.startTest();
        List<CheckRecordLockStatus.Result> results =
            CheckRecordLockStatus.checkLock(new List<CheckRecordLockStatus.Request>{ buildRequest(null) });
        Test.stopTest();

        System.assertEquals(false, results[0].isLocked);
    }

    private static CheckRecordLockStatus.Request buildRequest(Id recordId) {
        CheckRecordLockStatus.Request req = new CheckRecordLockStatus.Request();
        req.recordId = recordId;
        return req;
    }
}

Step 3: Add both classes to your sandbox

You don’t need VS Code for this. The Developer Console works fine.

  1. In your sandbox, click the gear icon and open Developer Console.
  2. Go to File → New → Apex Class and name it exactly CheckRecordLockStatus.
  3. Delete the starter template, paste in the first snippet, and save.
  4. Repeat with a class named CheckRecordLockStatusTest and the second snippet. Save the main class first, since the test class depends on it.
  5. Go to Test → New Run, select CheckRecordLockStatusTest, and click Run. All four tests should pass, and CheckRecordLockStatus should show 100% coverage.

If the locked-Case tests fail with a record locking error, go back and turn on Enable record locking and unlocking in Apex, then run them again.

Step 4: Add the Record Lock Status decision to your Flow

Now for the actual Flow fix. Open your Flow and add these two elements right before your existing lock step.

  1. Add an Action element and search for Check If Record Is Locked. Give it a label like Check if case is locked, and set Record ID to the triggering Case’s ID ({!$Record.Id}).
  2. Add a Decision element called Is case locked? with an outcome named Already locked, where the action’s Is Locked output equals {!$GlobalConstant.True}. Rename the default outcome to Not yet locked.
  3. Leave the Already locked path empty so it goes straight to the end, because there’s nothing left to do. Move your existing lock step onto the Not yet locked path.
Flow Builder showing the Check If Record Is Locked action with Record ID set to Triggering Case > Case ID and the Is Locked output resource
The Check If Record Is Locked action, with Record ID set to the triggering Case’s ID. Its Is Locked output is what the Decision checks next.
Flow Builder Decision element 'Is case locked?' with the Already locked outcome set to the Check if case is locked output Equals True, branching to Already locked and Not yet locked paths
The finished Flow. Already locked goes straight to the end, and only Not yet locked runs the LockCase step.

Unlocking works the same way, just flipped. Use the same Decision, but put your unlock step on the Already locked path and leave Not yet locked empty, since there’s nothing to unlock.

Save, then test the exact scenario that broke things: close a Case, reopen it as an admin, make a change, and close it again. It should close without a peep.

Step 5: Move it to production

  1. In the sandbox, create an Outbound Change Set with both Apex classes and your updated Flow, then upload it to production.
  2. In production, open Inbound Change Sets and click Deploy.
  3. Choose Run specified tests and enter CheckRecordLockStatusTest. It’s much faster than running every test in your org.
  4. The Flow version arrives inactive, so activate it in production when you’re ready.

What about seeing the lock in Lightning?

Part of why this bug sneaks up on people is that you can’t easily tell a Case is locked. In Salesforce Classic, it’s right there: a little padlock icon, plus an Unlock Record button for admins, sitting next to Edit and Close Case.

Salesforce Classic Case detail page showing the padlock icon and Unlock Record button on a locked Case
Salesforce Classic shows a padlock and an Unlock Record button right on the page.

Lightning doesn’t make it nearly that obvious, which is how so many of us find out a Case is locked the hard way: by hitting the error above.

So I fixed that too. Show Record Lock Status in Lightning (Plus an Unlock Button) walks you through a small component that puts a Locked/Unlocked badge right on the record page, with an Unlock button for the people you trust with one.

Does this only work for Cases?

Nope. The action takes any record ID, so you can drop it into Flows on Opportunities, custom objects, or anything else that can be locked. Build it once and reuse it everywhere.

That’s it: one Apex action, one test class, and one Decision element between your users and a very confusing error message.

⚡ Keep Learning
Want more Flow fixes like this in your back pocket?

Learn Salesforce step by step, practice with real scenarios, and connect with other Salesforce professionals.

🎓 Explore Salesforce Masterclass →👥 Join the Community →

One Response