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.

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
- Go to Setup → Process Automation Settings and make sure Enable record locking and unlocking in Apex is checked. The test class needs it, and if it’s off, your existing lock step probably isn’t working either.
- Your Flow has to run after the record is saved. Before-save Flows can’t call Apex actions. If your lock step already works, you’re almost certainly fine.
- Do all of this in a sandbox first. Yes, even for two little classes.
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.
/**
* 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.
@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.
- In your sandbox, click the gear icon and open Developer Console.
- Go to File → New → Apex Class and name it exactly
CheckRecordLockStatus. - Delete the starter template, paste in the first snippet, and save.
- Repeat with a class named
CheckRecordLockStatusTestand the second snippet. Save the main class first, since the test class depends on it. - Go to Test → New Run, select
CheckRecordLockStatusTest, and click Run. All four tests should pass, andCheckRecordLockStatusshould 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.
- 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}). - 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. - 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.


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
- In the sandbox, create an Outbound Change Set with both Apex classes and your updated Flow, then upload it to production.
- In production, open Inbound Change Sets and click Deploy.
- Choose Run specified tests and enter
CheckRecordLockStatusTest. It’s much faster than running every test in your org. - 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.

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.
Learn Salesforce step by step, practice with real scenarios, and connect with other Salesforce professionals.
One Response