sf-approval-processes

Use when designing Salesforce Apex approval processes, multi-step approvals, or Flow-integrated submissions. Do NOT use for general Apex or Flow-only work.

Salesforce Approval Processes

@../_reference/APPROVAL_PROCESSES.md

When to Use

  • Designing multi-step approval workflows (discounts, expenses, contracts)
  • Building Apex-driven approval submissions or programmatic approvals/rejections
  • Integrating approval logic with Flows (Spring '26 Integrated Approval Screen component)
  • Troubleshooting approval routing, locked records, or missing approval history
  • Migrating from email-based approvals to in-app approval workflows

Approval Process Architecture

┌─────────────────────────────────────────────────────┐
│                 Approval Process                     │
├─────────────────────────────────────────────────────┤
│  Entry Criteria     │ WHO can submit? WHEN?          │
│  Initial Submitter  │ Record owner, specific users   │
├─────────────────────────────────────────────────────┤
│  Step 1: Manager    │ Approver: Manager field         │
│  Step 2: VP         │ Approver: Related user field    │
│  Step 3: Finance    │ Approver: Queue                 │
├─────────────────────────────────────────────────────┤
│  Initial Actions    │ Lock record, set Status         │
│  Approval Actions   │ Unlock, update field, email     │
│  Rejection Actions  │ Unlock, set Status to Rejected  │
│  Recall Actions     │ Unlock, clear approval fields   │
└─────────────────────────────────────────────────────┘

Entry Criteria

Amount__c > 10000 AND Status__c = 'Draft'
RecordType.Name = 'Enterprise' AND Discount__c > 20

Formula:
AND(Amount__c > 10000, ISPICKVAL(Status__c, 'Draft'), NOT(ISBLANK(OwnerId)))

Approval Steps

PropertyOptions
ApproverUser field, Manager field, Queue, Related user
CriteriaAll records OR filter criteria (step-specific)
Reject behaviorFinal rejection OR go to previous step
UnanimityAll must approve OR first response

Parallel Approvals

  • Unanimous: All assigned approvers must approve
  • First Response: First approver's decision applies
  • For true parallel independent steps (Legal AND Finance simultaneously), use multiple processes or custom Apex

Approval Actions

Initial Submission Actions

  • Field Update: Status__c = "Pending Approval"
  • Email Alert: Notify approver(s)
  • Record Lock: Prevent edits during approval
  • Outbound Message: Notify external system

Final Approval Actions

  • Field Update: Status__c = "Approved"
  • Email Alert: Notify submitter
  • Record Unlock: Allow edits again

Final Rejection Actions

  • Field Update: Status__c = "Rejected"
  • Email Alert: Notify submitter with rejection reason
  • Record Unlock

Apex Approval Processing

Submitting a Record for Approval

public class ApprovalService {
    public static void submitForApproval(Id recordId, String comments) {
        Approval.ProcessSubmitRequest request = new Approval.ProcessSubmitRequest();
        request.setObjectId(recordId);
        request.setSubmitterId(UserInfo.getUserId());
        request.setComments(comments);

        Approval.ProcessResult result = Approval.process(request);
        if (result.isSuccess()) {
            System.debug('Submitted. Instance ID: ' + result.getInstanceId());
        } else {
            for (Database.Error err : result.getErrors()) {
                System.debug(LoggingLevel.ERROR, 'Failed: ' + err.getMessage());
            }
        }
    }
}

Approving or Rejecting Programmatically

public static void approveRecord(Id workItemId, String comments) {
    Approval.ProcessWorkitemRequest request = new Approval.ProcessWorkitemRequest();
    request.setWorkitemId(workItemId);
    request.setAction('Approve');
    request.setComments(comments);

    Approval.ProcessResult result = Approval.process(request);
    if (!result.isSuccess()) {
        throw new ApprovalException('Approval failed: ' + result.getErrors());
    }
}

public static void rejectRecord(Id workItemId, String comments) {
    Approval.ProcessWorkitemRequest request = new Approval.ProcessWorkitemRequest();
    request.setWorkitemId(workItemId);
    request.setAction('Reject');
    request.setComments(comments);

    Approval.ProcessResult result = Approval.process(request);
    if (!result.isSuccess()) {
        throw new ApprovalException('Rejection failed: ' + result.getErrors());
    }
}

Querying Approval Status

// Pending work items for current user
List<ProcessInstanceWorkitem> pendingItems = [
    SELECT Id, ProcessInstance.TargetObjectId, ProcessInstance.Status,
           ProcessInstance.TargetObject.Name, CreatedDate
    FROM ProcessInstanceWorkitem
    WHERE ActorId = :UserInfo.getUserId()
    ORDER BY CreatedDate DESC
];

// Approval history for a record
List<ProcessInstanceStep> history = [
    SELECT StepStatus, Comments, Actor.Name, CreatedDate
    FROM ProcessInstanceStep
    WHERE ProcessInstance.TargetObjectId = :recordId
    ORDER BY CreatedDate ASC
];

// Active work item for a specific record
ProcessInstanceWorkitem workItem = [
    SELECT Id FROM ProcessInstanceWorkitem
    WHERE ProcessInstance.TargetObjectId = :recordId
      AND ProcessInstance.Status = 'Pending'
    LIMIT 1
];

Flow Integration

Submitting for Approval from Flow

Use the Submit for Approval action element: specify the record ID, optionally the approval process name, submitter, and comments.

Spring '26 — Integrated Approval Screen Component

Screen Flows can include an Integrated Approval component for in-flow approve/reject:

Screen Flow:
  1. Show Record Details
  2. Integrated Approval Component (history, Approve/Reject buttons, comments)
  3. Decision: Check outcome
  4. Update Records based on outcome

Dynamic Approvers via Flow

Autolaunched Flow (invoked by Approval Process):
  1. Get Records: record's Region__c and Amount__c
  2. Decision: Route by amount and region
     - > 500K → VP Finance
     - > 100K → Regional Manager
     - Otherwise → Direct Manager
  3. Return approver User ID

Design Patterns

Delegation and Backup Approvers

  • Configure Delegated Approvers in user settings for vacation coverage
  • Use Queues as approvers so any queue member can approve
  • Set approval step timeout for auto-escalation

Approval History Tracking

public static List<Map<String, Object>> getApprovalHistory(Id recordId) {
    List<Map<String, Object>> trail = new List<Map<String, Object>>();
    for (ProcessInstance pi : [
        SELECT Id, Status, CreatedDate,
            (SELECT StepStatus, Comments, Actor.Name, CreatedDate
             FROM StepsAndWorkitems ORDER BY CreatedDate)
        FROM ProcessInstance
        WHERE TargetObjectId = :recordId
        ORDER BY CreatedDate DESC
    ]) {
        for (ProcessInstanceHistory step : pi.StepsAndWorkitems) {
            trail.add(new Map<String, Object>{
                'status' => step.StepStatus,
                'approver' => step.Actor.Name,
                'comments' => step.Comments,
                'date' => step.CreatedDate
            });
        }
    }
    return trail;
}

Testing Approval Processes

These tests require an approval process to be deployed in the org's metadata. Approval processes cannot be created programmatically in tests.

@isTest
static void testApprovalSubmission() {
    Account testAccount = new Account(Name = 'Test Account', AnnualRevenue = 50000);
    insert testAccount;

    Test.startTest();
    Approval.ProcessSubmitRequest request = new Approval.ProcessSubmitRequest();
    request.setObjectId(testAccount.Id);
    request.setSubmitterId(UserInfo.getUserId());
    Approval.ProcessResult result = Approval.process(request);
    Test.stopTest();

    System.assert(result.isSuccess(), 'Approval submission should succeed');
    System.assertEquals('Pending', result.getInstanceStatus());
}

@isTest
static void testApprovalProcess() {
    Account testAccount = new Account(Name = 'Test', AnnualRevenue = 50000);
    insert testAccount;

    Approval.ProcessSubmitRequest submitReq = new Approval.ProcessSubmitRequest();
    submitReq.setObjectId(testAccount.Id);
    Approval.ProcessResult submitResult = Approval.process(submitReq);

    Id workItemId = submitResult.getNewWorkitemIds()[0];

    Approval.ProcessWorkitemRequest approveReq = new Approval.ProcessWorkitemRequest();
    approveReq.setWorkitemId(workItemId);
    approveReq.setAction('Approve');
    approveReq.setComments('Looks good');

    Test.startTest();
    Approval.ProcessResult approveResult = Approval.process(approveReq);
    Test.stopTest();

    System.assert(approveResult.isSuccess());
    System.assertEquals('Approved', approveResult.getInstanceStatus());
}

Metadata Deployment

approvalProcesses/
  Discount_Approval.approvalProcess-meta.xml

Include in package.xml:

<types>
    <members>Opportunity.Discount_Approval</members>
    <name>ApprovalProcess</name>
</types>

Approval processes reference users, queues, and email templates. Ensure dependencies exist in the target org before deployment.


Related

  • Constraints: sf-apex-constraints — Governor limits and Apex safety rules