I am attempting to recall an approval process with the following Apex code:

Approval.ProcessWorkitemRequest pwr = new
Approval.ProcessWorkitemRequest();  pwr.setAction('Removed');
Approval.ProcessResult result = Approval.process(pwr);

It works fine when I run it as an administrator or when the user running it is the submitter of the approval process.

But it doesn't work for any other users; I get this error:

Process failed. First exception on row 0; first error: INSUFFICIENT_ACCESS_ON_CROSS_REFERENCE_ENTITY, insufficient access rights on cross-reference id

How can I make this work for any user?

To complete my question, I created a visualforce page to add a custom button on the object page layout calling the apex method below ManageButton.documentCustomGetApprovalProcess

public without sharing class ManageButton 
    // Constructor
    public ManageButton(ApexPages.StandardController stdController) 

     * method used by the Get Approval Process button
    public PageReference documentCustomGetApprovalProcess()
        string docId = ApexPages.currentPage().getParameters().get('id');

        ProcessInstanceWorkitem[] workItems = [
             SELECT Id
               FROM ProcessInstanceWorkitem 
              WHERE ProcessInstance.TargetObjectId = :docId
                AND ProcessInstance.Status = 'Pending'];

        Approval.ProcessWorkitemRequest pwr = new Approval.ProcessWorkitemRequest();
        Approval.ProcessResult result = Approval.process(pwr);

        // Redirect the user back to the original page
        PageReference pageRef = new PageReference('/' + docId);
        return pageRef;

Even specifing "without sharing" on the class I have the error I mentioned.

  • Have the code that is getting called in a class that executes 'without sharing'.
    – dphil
    Nov 2, 2015 at 16:10
  • Hello, I tried this but it has no impact. I fould the link link stipulating the without sharing has no impact on this error. Nov 2, 2015 at 16:40
  • The link you posted there, actually says it solved this error. In my experience, it has solved it as well.
    – dphil
    Nov 2, 2015 at 16:43
  • You are right the link says it should work adding without sharing. I tried it and I still have the issue. Nov 2, 2015 at 16:51
  • You should edit your question with that code instead of placing it in comments.
    – dphil
    Nov 2, 2015 at 17:05

4 Answers 4


By design, Approval Processes only allow either a System Administrator and/or the Initial Submitter to recall the submission.

Per Salesforce documentation:

enter image description here

However, to answer your question How can I make this work for any user? you will need your Apex code to change the context user to be a System Administrator.

One way to achieve this is by using Apex Email Service and sending an email to its service address from your Apex code. Email Services let you specify a context user that the code will run under and so in that apex code you can try performing your recall action.

Side note, I've used this Apex Email Service technique to run code as a specific user in other projects like Salesforce Chatter Bot for Feeds.

enter image description here

  • 4
    Interesting idea that.
    – Adrian Larson
    Feb 4, 2017 at 9:20

A more modern approach (though similar in principle to Doug's answer) is to use Platform Events.

In my code I checked the approval and fired like this:

if(thisApprovalRequest.approvalAction == 'Removed' && thisWorkItem.ActorId == thisUserId && thisWorkItem.ProcessInstance.SubmittedById != thisUserId) {
    approvalRecalls.add(new Approval_Recall__e(
            Process_Instance_Work_Item_Id__c = thisWorkItem.Id,
            Comment__c = thisApprovalRequest.comment



Then, an Apex Platform Event trigger can do the recall. Since it runs as Automated Process User, it has the same sort of admin permission to do the recall

  • For the record I tried this approach. The Platform Event runs as Automated Process but unfortunately hits the same issue: Process failed. First exception on row 0; first error: INVALID_CROSS_REFERENCE_KEY, invalid cross reference id: [] Sep 22, 2021 at 21:59
  • I can't really tell why that didn't work for you, but I have this working in a production org, so it definitely can work
    – Aidan
    Sep 23, 2021 at 10:26
  • 1
    This approach worked great for me. Thank you @Aidan!
    – Tyler
    Mar 22 at 17:52

Based on @Doug suggestion below is one way to achieve recall of an approval process without compromising security or being the submitter.

Code Excerpt:

global virtual with sharing class ID_Item extends cms.ContentTemplateController implements Messaging.InboundEmailHandler  {


global static Map<String, Object> mApprovalRecallAction(String sContentID, String sName) {
    System.debug('>>> ID_Item.mApprovalRecallAction(...)');


    // submitter is the person recalling, so we have permission to do so (otherwise we need to be an admin: https://success.salesforce.com/ideaView?id=08730000000iY1aAAE)
    if (oApproval.CreatedById == UserInfo.getUserId()) {        
        Approval.ProcessWorkitemRequest oPWR = new Approval.ProcessWorkitemRequest(); 
        oPWR.setComments('Recalled by `' + oApproval.CreatedBy.Name + '`');
        if (!Test.isRunningTest()) { // can't seem to create ProcessInstanceWorkItem records in tests
            Approval.ProcessResult oPR = Approval.process(oPWR);

        // Next the approval process needs to have a recalled action that sets `cms__Approval__c.Status = 'Recalled'` which is levraged by OrchestraCMS - but just incase these were deleted we are changing the status in apex as well
        oApproval.cms__Status__c = 'Recalled';
        oApproval.Content_Status__c = 'Recalled';
        update oApproval;
        System.debug('=== ID_Item.mApprovalRecallAction(...): `JSON.serialize(oPWR)=' + JSON.serialize(oPWR) + '`');
        return new Map<String, Object>{'bSuccess' => true, 'sPWR' => JSON.serialize(oPWR)};
    // we assume `this.bApprovalRecalVisible` did our security checks  
    else {
        // we leverage the functionality that an email service allows us to run under a adming user
        Messaging.SingleEmailMessage oMessage = new Messaging.SingleEmailMessage();
        try {
            // find our email service to use, as convention we prefixed it with class name 
            EmailServicesAddress oEmailServiceAddress = [
                SELECT e.SystemModstamp, e.RunAsUserId, e.LocalPart, e.LastModifiedDate, e.LastModifiedById, e.IsActive, e.Id, e.FunctionId, e.EmailDomainName, e.CreatedDate, e.CreatedById, e.AuthorizedSenders 
                FROM EmailServicesAddress e
                WHERE e.IsActive = true AND e.LocalPart LIKE 'ID_Item%' // `LIKE` is case-insensetive, so we keep capitialization same as class name as it's easier for search and replace 
            oMessage.toAddresses = new String[] {
                oEmailServiceAddress.LocalPart + '@' + oEmailServiceAddress.EmailDomainName
        catch (Exception e) {
            System.debug('=== ID_Item.mApprovalRecallAction(...): ERROR: no email service setup, `e' + String.valueOf(e) + '`');
            return new Map<String, Object>{'bSuccess' => true, 'e' => JSON.serialize('ERROR: no email service setup, `e' + String.valueOf(e) + '`')};
        Map<String, Object> oParameterMap = new Map<String, Object>{'sAction' => 'mApprovalRecallActionWithAdminPermissions', 'sApprovalID' => oApproval.Id};
        oMessage.subject = '=== ID_Item.mApprovalRecallAction(...): `JSON.serialize(oParameterMap)=' + JSON.serialize(oParameterMap) + '`'; // using subject is not safe because SF can modify it sometimes (ex. prefixing `Sandbox`  
        oMessage.plainTextBody = JSON.serialize(oParameterMap);
        Messaging.SendEmailResult[] oResults = Messaging.sendEmail(new List<Messaging.SingleEmailMessage> {oMessage});

        System.debug('=== ID_Item.mApprovalRecallAction(...): `oResults' + String.valueOf(oResults) + '`');

        System.debug('<<< ID_Item.mApprovalRecallAction(...)');
        if (oResults[0].success) {
            return new Map<String, Object>{'bSuccess' => true, 'oResults' => JSON.serialize(String.valueOf(oResults))};
        } else {
            return new Map<String, Object>{'bSuccess' => false, 'oResults' => JSON.serialize(String.valueOf(oResults))};

global Messaging.InboundEmailResult handleInboundEmail(Messaging.inboundEmail oInboundEmail, Messaging.InboundEnvelope oInboundEnvelope){
    System.debug('>>> ID_Item.handleInboundEmail(...)');
    Messaging.InboundEmailResult oInboundEmailResult = new Messaging.InboundEmailResult();

    Map<String,Object> oParameterMap = new Map<String, Object>{'sAction' => 'UNKNOWN'};
    try {
        oParameterMap = (Map<String, Object>)JSON.deserializeUntyped(oInboundEmail.plainTextBody); 
        // expected string input format is "{'sAction': 'fooBar', 'sApprovalID': 'foo', ...}"
    catch (QueryException e) {
        System.debug('=== ID_Item.handleInboundEmail(...): ERROR: `e=' + e + '`');
        System.debug('<<< ID_Item.handleInboundEmail(...)');
        return oInboundEmailResult;
    try {
        if (oParameterMap.get('sAction') == 'mApprovalRecallActionWithAdminPermissions') {
            String sApprovalID = (String)oParameterMap.get('sApprovalID');
            cms__Approval__c oApproval = [Select ID, c.cms__Content__c FROM cms__Approval__c c WHERE c.Id = :sApprovalID].get(0);
            List<ProcessInstanceWorkItem> oWorkItemList = [Select ProcessInstance.TargetObjectId, ActorId From ProcessInstanceWorkitem where ProcessInstance.TargetObjectId = :oApproval.ID];

            Approval.ProcessWorkitemRequest oPWR = new Approval.ProcessWorkitemRequest(); 
            oPWR.setComments('Recalled by `' + oInboundEnvelope.fromAddress + '`');

            if (Test.isRunningTest() == false) {
                Approval.ProcessResult oPR = Approval.process(oPWR);


    catch (QueryException e) {
         System.debug('=== ID_Item.handleInboundEmail(...): ERROR: `e=' + e + '`');

    oInboundEmailResult.success = true;
    System.debug('<<< ID_Item.handleInboundEmail(...)');
    return oInboundEmailResult;



  • usually answers should have the system.debug lines removed
    – cropredy
    Nov 27, 2017 at 20:04

Older thread but in case it helps others. I was able to get this working by running a flow as system and calling an apex action. Seemed cleaner than creating a platform event.

Edit: This approach will only work if the user initiating the flow has the modify all permission on the object that is being approved.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.