Apex Approval Process is reshaping how Salesforce professionals work — and this article breaks down everything you need to know.
Let’s be real: standard point-and-click approval processes are great until they aren’t. We’ve all been in that spot where a client asks for a specific piece of logic that the standard builder just can’t touch. That is exactly where an Apex Approval Process comes in handy. It gives you the control to automate things that would otherwise require a ton of manual clicks or messy workarounds.
I’ve spent plenty of nights debugging why a record didn’t route correctly, and honestly, most of the time it’s because we try to force Flow to do too much. While I’m a big fan of staying declarative when possible, knowing when to use Apex over Flow is a skill that separates the juniors from the seniors. If you need to trigger approvals from a custom button, a batch job, or some complex integration, Apex is your best friend.
When to skip Flow for an Apex Approval Process
So why bother with code? Here’s the thing: standard tools have limits. You might need an Apex Approval Process if you’re dealing with logic that spans across multiple related objects that aren’t directly connected. Or maybe you’re getting signals from an external system that need to trigger an immediate approval or rejection without a user ever touching the record.
- You need to submit records in bulk from a scheduled job.
- The approver is determined by a complex matrix stored in a custom metadata type.
- You want to provide a custom UI, like a Lightning Web Component, for a better user experience.
The Approval Object Model
Before you start writing code, you have to understand the objects involved. It’s a bit of a maze if you’re looking at it for the first time. Here’s the breakdown of what actually matters:
- ProcessInstance: This is the “envelope” for the entire approval journey of a single record.
- ProcessInstanceStep: Think of this as the history. It shows who approved what and when.
- ProcessInstanceWorkitem: This is the most important one for developers. It represents the current pending request. If you want to approve or reject something, you need the ID from this object.
- Approval Class: This is the system class we use to actually “do” the work in our code.
Submitting a Record for Approval
To kick things off, you’ll use the Approval.ProcessSubmitRequest class. It’s pretty straightforward. You tell it which record you’re submitting, add a comment so the approver knows what’s up, and let it rip. If you don’t specify an approver, Salesforce just uses the one defined in your standard approval process setup.
public class ApprovalHandler {
public static void submitForReview(Id recordId) {
Approval.ProcessSubmitRequest req = new Approval.ProcessSubmitRequest();
req.setComments('Submitting this via Apex because the business logic is complex.');
req.setObjectId(recordId);
// This is where the magic happens
Approval.ProcessResult result = Approval.process(req);
if (result.isSuccess()) {
System.debug('Record is now in the queue.');
} else {
System.debug('Something went wrong: ' + result.getErrors()[0].getMessage());
}
}
}Approving or Rejecting via Code
Now, what if you need to close the loop? Maybe you’ve built a custom “Approve All” button for a manager. To do this, you first have to find the ProcessInstanceWorkitem associated with the record. I’ve seen teams get tripped up here by forgetting that a record can’t be approved if there isn’t an active work item for it. Always check for that first.
public class ApprovalHandler {
public static void processStep(Id recordId, String action) {
// Find the pending work item first
ProcessInstanceWorkitem[] workItems = [SELECT Id FROM ProcessInstanceWorkitem
WHERE ProcessInstance.TargetObjectId = :recordId
LIMIT 1];
if (workItems.isEmpty()) return;
Approval.ProcessWorkitemRequest req = new Approval.ProcessWorkitemRequest();
req.setComments('Actioned via custom Apex logic.');
req.setAction(action); // 'Approve', 'Reject', or 'Removed'
req.setWorkItemId(workItems[0].Id);
Approval.process(req);
}
}Best practices for your Apex Approval Process
Writing the code is the easy part. Making it work at scale is where people usually struggle. If you’re managing async limits or dealing with high-volume data, you have to be careful.
- Bulkify everything: Don’t put your
Approval.process()calls inside a loop. Collect your requests into a list and process them all at once. Your governor limits will thank you. - Check your context: Remember that approvals run under the permissions of the user executing the code. If your code is running “without sharing,” you might accidentally allow someone to approve their own request.
- Handle the errors: Approval processes can fail for dozens of reasons-missing approvers, record locks, or validation rules. Always use the
isSuccess()check and log your errors properly.
In my experience, the biggest headache with an Apex Approval Process is record locking. When a record is in an approval process, it’s usually locked. If your code tries to update that same record later in the same transaction, you’re going to hit a wall. Plan your execution order carefully.
Key Takeaways
- Use Apex when the standard declarative approval tool hits a wall with complex logic.
- The
ProcessInstanceWorkitemis your “key” to approving or rejecting records programmatically. - Always bulkify your requests to avoid hitting CPU and DML limits.
- Test your logic with different users to make sure sharing rules and permissions don’t break the flow.
At the end of the day, an Apex Approval Process is just another tool in your kit. It’s not always necessary, but when you need that extra level of flexibility, it’s a lifesaver. Just keep an eye on those record locks and keep your code bulk-safe, and you’ll be fine.








Leave a Reply