Approval processes are one of those features that looks simple in a demo and complex in production. A single-level approval for a discount request is easy. But when a Fortune 500 company needs a five-tier approval chain that changes based on deal size, region, and product category — with parallel legal and finance sign-off at level three — you need to think architecturally.
I’ve designed approval systems for everything from simple expense reimbursements to multi-million dollar contract approvals. Here’s how I think about building approval architectures that scale.
The Anatomy of a Scalable Approval Process
Before designing anything, I map out four dimensions:
- Who approves? — Static users, roles, queues, formula-derived approvers, or manager hierarchy
- In what order? — Sequential steps, parallel steps, or a mix
- Under what conditions? — Entry criteria, step criteria, routing rules
- What triggers escalation? — Timeout actions, rejection handling, recall rules
Getting clarity on all four dimensions before touching Setup prevents the painful rework of rebuilding a process because you didn’t account for the parallel legal review at step three.
Animated Approval Flow
Problem: A sales team’s discount approval process broke silently for three weeks after a regional manager left the company. Step 2 had that user’s name hardcoded as the approver. Discount requests sat in limbo with no notification to anyone.
Solution: Replace the hardcoded user with a queue-based approver for every step. Create a Discount_Approvers queue per region, assign the correct members, and point the approval step at the queue. When team members change, queue membership is updated — the approval process never breaks.
Multi-Level Sequential Approvals
The simplest pattern: approvals route through a chain, one step at a time. Each step can use a different approver source.
Approver source options per step:
- Automatically assigned approver — uses the “Assigned Approver” field (which can be a formula)
- Related user field — e.g., the Account’s Owner, the Opportunity’s Manager
- User, queue, or related user — pick a specific person or group
- Manager in the role hierarchy — automatically escalates up the hierarchy
The hierarchy option is powerful for expense approvals: automatically routes to the submitter’s manager, then their manager’s manager, without any hardcoded user IDs.
I see this mistake constantly: a single user’s name embedded as an approver in step 2. That person leaves, and the process silently fails or routes nowhere. Always use role-based queues, the manager hierarchy, or a formula field that resolves to the correct user based on record criteria.
Parallel Approval Steps
Salesforce supports parallel approval in a roundabout way — you add multiple approvers to a single step and configure the unanimous or first-response logic.
True Parallel Steps Implementation (Finance AND Legal simultaneously)
True parallel steps (Finance AND Legal simultaneously, and then VP after both approve) require a different approach:
- Create two separate Approval Processes with entry criteria that differentiate them
- Use a Process Builder (or Flow) to trigger both simultaneously
- Track each approval outcome in custom fields
- A third Approval Process (or Flow) triggers only when both custom approval fields are set to “Approved”
This is admittedly complex. Salesforce does not natively support parallel steps that converge into a single next step within one Approval Process definition. The workaround above works, but document it meticulously.
Dynamic Approvers via Formula Fields
One of my favorite patterns: a formula field on the object called Approver__c that resolves to the correct approver based on deal size, region, or category. The Approval Process references this field as the “Assigned Approver.”
/* Apex formula equivalent for Approver__c */
IF(Amount >= 100000,
[VP_of_Sales_User_Id],
IF(Amount >= 25000,
[Regional_Director_User_Id],
OwnerId /* manager of submitter handles small deals */
)
)In the actual formula field, you’d reference User lookup fields or queue IDs. When business rules change — say the threshold moves from $25K to $50K — you update one formula field, not every approval step.
Combining Approval Processes with Flow
Flow can both trigger and interact with approval processes. Here’s how I use Flow alongside approvals:
Triggering a Submission
Instead of button-click submissions, I use a Flow to submit records for approval programmatically — useful for submitting from a community, an Experience Cloud site, or a mobile component.
// Apex called from Flow via @InvocableMethod
Approval.ProcessSubmitRequest req = new Approval.ProcessSubmitRequest();
req.setObjectId(recordId);
req.setSubmitterId(UserInfo.getUserId());
req.setProcessDefinitionNameOrId('Discount_Approval');
req.setSkipEntryCriteria(false);
Approval.ProcessResult result = Approval.process(req);Post-Approval Automation in Flow
Use a Record-Triggered Flow on the object, triggered when the Approval_Status__c field changes to ‘Approved’. This is cleaner than Approval Process final approval actions when the logic is complex — Flow gives you access to all the tools (subflows, collection operations, external callouts via Apex actions).
Apex Approval Submission Patterns
For bulk operations — approving 500 records at once during a migration or integration scenario — the Apex Approval class handles this efficiently:
List<Approval.ProcessWorkitemRequest> approvals = new List<Approval.ProcessWorkitemRequest>();
for (ProcessInstanceWorkitem workItem : [
SELECT Id FROM ProcessInstanceWorkitem
WHERE ProcessInstance.TargetObjectId IN :recordIds
AND ActorId = :UserInfo.getUserId()
]) {
Approval.ProcessWorkitemRequest req = new Approval.ProcessWorkitemRequest();
req.setWorkitemId(workItem.Id);
req.setAction('Approve');
req.setComments('Bulk approved via integration');
approvals.add(req);
}
List<Approval.ProcessResult> results = Approval.process(approvals, false);
for (Approval.ProcessResult r : results) {
if (!r.isSuccess()) {
System.debug('Failed: ' + r.getErrors());
}
}The false parameter in Approval.process() means a failure on one item won’t roll back the rest — important for bulk operations where partial success is acceptable.
An approval process without time-based escalation is a business process that can stall indefinitely. Configure at least two time triggers on every step: a reminder email at 24 hours, and a reassignment or escalation at 48 hours. Without these, a single approver going on leave can silently block deals, contracts, or purchase orders for days. Escalation paths are what separate a demo-ready process from a production-ready one.
Recall and Rejection Handling
Rejection with Correction
When a record is rejected, often the submitter needs to correct it and resubmit. Configure the rejection action to:
- Update the record back to “Draft” (custom field, not the locked standard status)
- Unlock the record for editing (approval processes lock records by default)
- Send a notification email with the rejection comments
In the final rejection action, add a Field Update to an Approval_Comments__c text area that stores the last rejection reason — submitters shouldn’t have to dig through the approval history to understand what needs fixing.
Escalation on Timeout
Every step should have a Time-Based Action (Approval Process terminology for escalation). If an approver doesn’t respond in 48 hours:
- Send a reminder email
- If still no response in another 24 hours, reassign to a backup approver (update the
Approver__cformula’s fallback logic) - Optionally, auto-approve or auto-reject based on business rules
Configuring escalation paths is what separates a process that works in a demo from one that works in a live business.
Testing Approval Processes in Sandbox
@isTest
static void testApprovalSubmission() {
// Create and insert the test record
My_Object__c obj = new My_Object__c(Amount__c = 50000, Status__c = 'Pending Approval');
insert obj;
Test.startTest();
Approval.ProcessSubmitRequest req = new Approval.ProcessSubmitRequest();
req.setObjectId(obj.Id);
Approval.ProcessResult result = Approval.process(req);
Test.stopTest();
System.assert(result.isSuccess(), 'Approval submission failed: ' + result.getErrors());
System.assertEquals('Pending', result.getInstanceStatus());
}In test context, approval processes run synchronously. The process must be active and the submitting user must meet the entry criteria.
The Pattern I Use Most
For complex orgs, my go-to architecture is:
- Dynamic approver formula fields on every approvable object
- Queue-based step assignees (never individual users)
- Record-triggered Flow handles all post-approval automation
- Escalation time triggers configured on every step
- A custom
Approval_Status__cpicklist field (separate from the locked Salesforce approval status) that Flow updates — this makes reporting and triggering automation far easier
The approval process itself handles routing and audit trail. The Flow handles everything else.
What’s the most complex approval workflow you’ve had to build in Salesforce, and how did you handle the parallel steps? If you’ve found a cleaner native approach to true parallel-converging approvals, I’d genuinely love to hear it — drop it in the comments.
How did this article make you feel?
Comments
Salesforce Tip