CloudNextGen

Salesforce and Slack Integration for Approvals: The End of Approval Delays

In today’s interconnected enterprise landscape, Single Sign-On (SSO) is no longer a luxury—it’s a necessity. SSO provides users with a secure, one-click access point to all their critical business applications, drastically improving the user experience and strengthening the organization’s security posture.

This guide focuses on the powerful combination of Salesforce, the world’s leading Customer Relationship Management (CRM) platform, and Okta, a top-tier Identity Provider (IdP). By integrating the two using the SAML protocol, we centralize identity management and enable secure, frictionless access for all users.

Challenge

1. The Core Requirement: Accelerating Approvals:

Our primary requirement was simple: Move the approval process to where our team already works and communicates.

Salesforce is the single source of truth for all business data, including the records that need approval. However, requiring users to constantly switch contexts, log into Salesforce, navigate to the correct record, review, and approve introduces friction, delays, and a risk of missed steps.

We needed a solution that would:

      1. Instantly alert the right approver.
      2. Allow the approver to act immediately (Approve/Reject) without leaving their current workflow.
      3. Log the decision back into Salesforce, maintaining a perfect audit trail.

2. Why a Custom Integration Was Needed:

Standard, out-of-the-box tools or manual processes simply couldn’t deliver the speed and seamless experience required:

      1. The “Email Problem”: Relying on email for approvals leads to cluttered inboxes, delayed responses, and a difficult time tracking the approval history against the original Salesforce record. Emails get lost, action is postponed.
      2. The “Context Switching Tax”: When a sales rep or manager is heads-down in Slack collaborating with a team, being forced to open a browser, log into Salesforce, and find the record to approve imposes a “context switching tax.” This delay compounds across dozens of daily decisions, slowing down the entire sales cycle.
      3. The “Information Silo”: Approvers often need key context, like the current deal amount, product tier, or discount percentage, to make a decision. Without the integration, this information is siloed in Salesforce, forcing the approver to hunt for it.

Summary of Components

The custom solution requires the creation and configuration of the following components in both Slack and Salesforce:

System Component Type Component Name / Purpose
Slack App Configuration Approval_App (App ID, Client ID, Client Secret, Signing Secret, Webhook URL)
Salesforce Site Slack Integration Site (Public endpoint for receiving Slack interactions)
Salesforce External Credential ExternalSlackChannelNC (Used for Named Credential)
Salesforce Named Credential SlackChannelNC (Securely stores the Slack Webhook URL for Apex callouts)
Salesforce Custom Object Integration Log (Integration_Log__c) (For logging requests/responses)
Salesforce Custom Metadata Slack User Mapping (Slack_User_Mapping__mdt) (Maps Salesforce User IDs to Slack Channel IDs)
Salesforce Apex Class SlackUtil.cls (Utility class for Slack API HTTP requests and payload building)
Salesforce Apex Class SlackMessageSender.cls (Sends the interactive approval message to Slack)
Salesforce Apex Class SlackInteractiveReceiver.cls (Receives the incoming button click callback from Slack)
Salesforce Apex Class SlackPayloadProcessor.cls (Asynchronous job to process the received Slack payload)
Salesforce Apex Class RecordUpdateService.cls (Updates the approval record status and resolves the approver)
Salesforce Flow Record-Triggered Flow (Automates calling SlackMessageSender when an approval is created)

Solution

The solution involves configuring the Slack application, setting up Salesforce, and developing the custom logic.

1- Slack App Setup:

      1. Slack Account & Workspace: Ensure you have a Slack account and access to a Workspace where you have permissions to create and test apps.
      2. App Registration: Go to api.slack.com and click “Create an App.”
      3. App Creation: Select “From scratch,” enter the App Name (e.g., Approval_App), and select the target Workspace.
      4. Retrieve Credentials: Navigate to the App Basic Information page to retrieve the App ID, Client ID, Client Secret, and Signing Secret.
      5. Enable Webhooks: Click on Incoming Webhooks, and then click the Add New Webhook button. Select the Workspace and App Name, click Install Approvals, and save the generated Webhook URL.

      6. Under the User and Bot Token Scopes section, click the Add an OAuth Scope button and provide the highlighted permission(s), and then save your changes.
      7. Enable Interactivity: Go to Interactivity & Shortcuts in the Slack API console.

2. Salesforce Configuration

      1. Create a New Salesforce Site
        This creates a public URL for Slack to interact with your Salesforce organization.
        1. Go to SetupSites.
        2. Create New Sites.
        3. Fill in details:
          1. Site Label: Slack Integration Site
          2. Active: Check the box
          3. Active Site Home Page: UnderConstruction
        4. Click Save.
      2. Update Request URL at Slack App
        The Slack App needs the URL of the public endpoint you created.
        1. Copy the Site URL from Salesforce.
        2. In Slack API site, go to the Interactivity & Shortcuts section.
        3. In the “Request URL” field, paste the Salesforce Site URL.
        4. Immediately append the endpoint path /services/apexrest/<className> to the end.
          • Note: Replace <className> with your Receiver RestResource class (e.g., SlackInteractions).
        5. The final URL should look like: https://…my.salesforce-sites.com/slack/services/apexrest/SlackInteractions
        6. Click Save.
      3. Create Named Credentials
        This provides a secure way to reference the Slack Webhook URL in Apex code without hardcoding it.
        1. Create External Credential: Go to SetupNamed CredentialsExternal Credentials tab. Click New.
          1. Label/Name: ExternalSlackChannelNC
          2. Authentication Protocol: Custom
          3. Click Save.
        2. Create Principals for External Credential
          1. Parameter Name: ExternalPrincipal
        3. Create Named Credential: Go to the Named Credentials tab (or click New).
          1. Label/Name: SlackChannelNC
          2. URL: Provide the webhook URL copied from the Slack App configuration.
          3. External Credential: Select the previously created External Credential: ExternalSlackChannelNC.
          4. Generate Authorization Header, Allow Formulas in HTTP Header, Allow Formulas in HTTP Body: Select True (check the box).
            • Finally, your credential will look like this:
        4. Create Custom Metadata and Log Object:
          1. Create custom Object for integration error logs with following fields:
            Object API Name: Integration_Log__c (auto-filled)
            Fields:
            • Log_Message__c  →  Long Text Area (32768)
            • Log_Source__c   →  Text(255)
            • Raw_Payload__c   →  Long Text Area(32768)
            • Record_ID__c   →  Text(18)
          2. Create a metadata for user mapping.
            Api Name: Slack_User_Mapping__mdt
            Fields:
            • Salesforce_User_Id__c    Text(18)
            • Slack_Channel_ID__c    Text(255)

3. Apex Development:

   This includes the core logic for communication and record updates.

 

SlackUtil.cls: Utility class for all Slack API interactions, including the postJson method for secure callouts using the Named Credential and a buildInteractivePayload method to structure the Slack message with Approve/Reject buttons.

/**

 * Utility helpers for Slack integration: endpoint constants, payload builders

 * and a single place to perform HTTP POST callouts to Slack.

 */

public with sharing class SlackUtil {




    public static final String SLACK_CALLOUT = 'callout:SlackChallenNC';




    // Lightweight response wrapper

    public class SlackResponse {

        public Integer statusCode;

        public String body;

        public String errorMessage;

    }




    /**

     * Posts a JSON payload to the configured Slack callout endpoint.

     * Used to send approval to slack channel

     * Returns a SlackResponse with status and body.

     */

    public static SlackResponse postJson(String jsonPayload) {

        SlackResponse result = new SlackResponse();

        HttpRequest req = new HttpRequest();

        req.setEndpoint(SLACK_CALLOUT);

        req.setMethod('POST');

        req.setHeader('Content-Type', 'application/json; charset=UTF-8');

        req.setBody(jsonPayload);




        try {

            Http http = new Http();

            HttpResponse res = http.send(req);

            result.statusCode = res.getStatusCode();

            result.body = res.getBody();

        } catch (Exception ex) {

            result.errorMessage = ex.getMessage();

        }




        return result;

    }




    /**

     * Used to update slack message when user hits the buttons.

     */

    public static SlackResponse postToUrl(String url, String jsonPayload) {

        SlackResponse result = new SlackResponse();

        HttpRequest req = new HttpRequest();

        req.setEndpoint(url);

        req.setMethod('POST');

        req.setHeader('Content-Type', 'application/json; charset=UTF-8');

        req.setBody(jsonPayload);




        try {

            Http http = new Http();

            HttpResponse res = http.send(req);

            result.statusCode = res.getStatusCode();

            result.body = res.getBody();

        } catch (Exception ex) {

            result.errorMessage = ex.getMessage();

        }




        return result;

    }




    /**

     * Call Slack API `users.info` via named credential to retrieve user profile information.

     * Returns the user's email address when available, otherwise null.

     */

    public static String getUserEmail(String slackUserId) {

        if (String.isBlank(slackUserId)) return null;




        try {

            String endpoint = SLACK_CALLOUT + '/users.info?user=' + EncodingUtil.urlEncode(slackUserId, 'UTF-8');

            HttpRequest req = new HttpRequest();

            req.setEndpoint(endpoint);

            req.setMethod('GET');

            req.setHeader('Content-Type', 'application/x-www-form-urlencoded');




            Http http = new Http();

            HttpResponse res = http.send(req);

            if (res.getStatusCode() == 200) {

                Map<String, Object> resp = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());

                if (resp != null && resp.containsKey('ok') && (Boolean)resp.get('ok')) {

                    Map<String, Object> user = (Map<String, Object>) resp.get('user');

                    if (user != null) {

                        Map<String, Object> profile = (Map<String, Object>) user.get('profile');

                        if (profile != null && profile.get('email') != null) {

                            return (String) profile.get('email');

                        }

                    }

                }

            }

        } catch (Exception ex) {

            System.debug('SlackUtil.getUserEmail error: ' + ex.getMessage());

        }




        return null;

    }




    /**

     * Build an interactive payload with Approve/Reject buttons. Uses the recordId string in the button "value" so the receiving side can parse it.

     */

    public static String buildInteractivePayload(String channel, String recordId, String text) {

        String yesValue = recordId + '_APPROVE';

        String noValue = recordId + '_REJECT';




        Map<String, Object> payload = new Map<String, Object>{

            'channel' => channel,

            'text' => text,

            'blocks' => new List<Object>{

                new Map<String, Object>{

                    'type' => 'section',

                    'text' => new Map<String, String>{'type' => 'mrkdwn', 'text' => '*' + text + '*'}

                },

                new Map<String, Object>{

                    'type' => 'actions',

                    'elements' => new List<Object>{

                        new Map<String, Object>{

                            'type' => 'button',

                            'text' => new Map<String, String>{'type' => 'plain_text', 'text' => '✅ Approve'},

                            'style' => 'primary',

                            'value' => yesValue,

                            'action_id' => 'approval_yes'

                        },

                        new Map<String, Object>{

                            'type' => 'button',

                            'text' => new Map<String, String>{'type' => 'plain_text', 'text' => '❌ Reject'},

                            'style' => 'danger',

                            'value' => noValue,

                            'action_id' => 'approval_no'

                        }

                    }

                }

            }

        };




        return JSON.serialize(payload);

    }




    /**

     * Build a payload suitable for Slack's chat.update endpoint (replace message by ts).

     */

    public static String buildUpdatePayload(String channel, String ts, String text) {

        Map<String, Object> payload = new Map<String, Object>{

            'channel' => channel,

            'ts' => ts,

            'text' => text,

            'blocks' => new List<Object>{

                new Map<String, Object>{

                    'type' => 'section',

                    'text' => new Map<String, String>{'type' => 'mrkdwn', 'text' => text}

                }

            }

        };

        return JSON.serialize(payload);

    }




}
IntegrationLogger.cls: The class responsible for logging Integration errors.

 

public with sharing class IntegrationLogger {

   

    /**

     * @description Inserts a new Integration_Log__c record.

     * Note: This must be separate from the trigger/callout code to avoid recursive DML limits.

     */

    public static void logError(String source, String message, String rawPayload, Id relatedRecordId) {

       

        Integration_Log__c log = new Integration_Log__c(

            Log_Source__c = source,

            Log_Message__c = message,

            Raw_Payload__c = rawPayload,

            Record_ID__c = String.valueOf(relatedRecordId)

        );

        try {

            insert log;

        } catch (DmlException e) {

            System.debug(LoggingLevel.ERROR, 'FATAL: Failed to write Integration_Log__c: ' + e.getMessage());

            throw new CalloutException('FATAL: Failed to write Integration_Log__c: ' + e.getMessage());

        }

    }

}

SlackMessageSender.cls: The main class responsible for sending the interactive message. It includes an **@InvocableMethod** to be called from the Flow, which queries Slack_User_Mapping__mdt to find the correct Slack Channel ID and uses SlackUtil to send the message.

/**

 * @description Sends an interactive message from Salesforce to a specified Slack channel or user.

 * It uses the 'Slack_Bot_Token' Named Credential for secure authorization and uses @future(callout=true)

 * to prevent governor limits during transaction processing.

 */

public with sharing class SlackMessageSender {

   

    public static void sendInteractiveApproval(String targetId, Id recordId, String question) {

        sendInteractiveApprovalAsync(targetId, String.valueOf(recordId), question);

    }




    @future(callout=true)

    public static void sendInteractiveApprovalAsync(String targetId, String recordId, String question) {

        String payload = SlackUtil.buildInteractivePayload(targetId, recordId, question);

        SlackUtil.SlackResponse res = SlackUtil.postJson(payload);




        if (res.errorMessage != null) {

            System.debug('Slack callout exception: ' + res.errorMessage);

        } else if (res.statusCode == 200) {

            System.debug('Slack message sent successfully.');

        } else {

            System.debug('Slack API returned status ' + res.statusCode + ' body: ' + res.body);

        }

    }




    /**

     * Sends interactive approval messages to multiple Slack channel IDs for each assigned user.

     */

    public static void sendToMultipleChannels(Map<String, String> userChannelMap, Id recordId, String question) {

        if (userChannelMap == null) return;

        for (String userId : userChannelMap.keySet()) {

            String channelId = userChannelMap.get(userId);

            if (String.isNotBlank(channelId)) {

                sendInteractiveApproval(channelId, recordId, question);

            }

        }

    }




    @InvocableMethod(label='Send Slack Message' description='Sends a simple message to Slack from a Flow')

    public static void SendMessageFromFlow(List<Id> recordIds) {

        // Build user-to-channel map from approval records, handling both single user and group

        Map<Id, Map<String, String>> approvalMapwithUserChannel = new Map<Id, Map<String,String>>();

        Map<String, String> userIdToChannelId = new Map<String, String>();

        try {

            // Query custom metadata records and populate the map.

            for (Slack_User_Mapping__mdt m : [SELECT MasterLabel, Salesforce_User_Id__c, Slack_Channel_ID__c FROM Slack_User_Mapping__mdt]) {

                if (m.Salesforce_User_Id__c != null && m.Slack_Channel_ID__c != null) {

                    userIdToChannelId.put(String.valueOf(m.Salesforce_User_Id__c), String.valueOf(m.Slack_Channel_ID__c));

                }

            }

        } catch (Exception ex) {

            System.debug('SlackMessageSender: could not load Slack_User_Mapping__mdt - ' + ex.getMessage());

        }




        // Query approvals and related approver info

        List<sbaa__Approval__c> approvals = [SELECT Id, Quote__c, Quote__r.SBQQ__Account__c, sbaa__Approver__r.sbaa__User__c, sbaa__Approver__r.sbaa__GroupId__c, Quote__r.SBQQ__Account__r.Name, Quote__r.SBQQ__Opportunity2__r.Name, Quote__r.SBQQ__Opportunity2__r.Amount FROM sbaa__Approval__c WHERE Id IN :recordIds];




        // Collect all group IDs to query users in those groups

        Set<Id> groupIds = new Set<Id>();

        for (sbaa__Approval__c approvalObj : approvals) {

            if (approvalObj.sbaa__Approver__r != null && approvalObj.sbaa__Approver__r.sbaa__GroupId__c != null) {

                groupIds.add(approvalObj.sbaa__Approver__r.sbaa__GroupId__c);

            }

        }




        // Query users in groups

        Map<Id, Map<String,String>> groupIdToUserIds = new Map<Id, Map<String,String>>();

        if (!groupIds.isEmpty()) {

            for (GroupMember gm : [SELECT GroupId, UserOrGroupId FROM GroupMember WHERE GroupId IN :groupIds]) {

                if (!groupIdToUserIds.containsKey(gm.GroupId)) {

                    groupIdToUserIds.put(gm.GroupId, new Map<String,String>());

                }

                String userIdStr = String.valueOf(gm.UserOrGroupId);

                groupIdToUserIds.get(gm.GroupId).put(userIdStr, userIdToChannelId.get(userIdStr));

            }

        }




        for (sbaa__Approval__c approvalObj : approvals) {

            if(!approvalMapwithUserChannel.containsKey(approvalObj.Id)){

                approvalMapwithUserChannel.put(approvalObj.Id, new Map<String, String>());

            }

            Map<String,String> mapForApproval = approvalMapwithUserChannel.get(approvalObj.Id);




            if (approvalObj.sbaa__Approver__r != null && approvalObj.sbaa__Approver__r.sbaa__User__c != null) {

                String userId = String.valueOf(approvalObj.sbaa__Approver__r.sbaa__User__c);

                mapForApproval.put(userId, userIdToChannelId.get(userId));

            }

            if (approvalObj.sbaa__Approver__r != null && approvalObj.sbaa__Approver__r.sbaa__GroupId__c != null) {

                Map<String,String> groupUsers = groupIdToUserIds.get(approvalObj.sbaa__Approver__r.sbaa__GroupId__c);

                if (groupUsers != null) {

                    approvalMapwithUserChannel.put(approvalObj.Id, groupUsers);

                }

            }

        }




        for (sbaa__Approval__c approvalObj : approvals) {

              String message = '*:wave: New Approval Request*\n' +

                            '*Account Name:* ' + (approvalObj.Quote__r != null && approvalObj.Quote__r.SBQQ__Account__r != null ?  approvalObj.Quote__r.SBQQ__Account__r.Name : '_Not Provided_') + '\n' +

                            '*Opportunity Name:* ' + (approvalObj.Quote__r != null && approvalObj.Quote__r.SBQQ__Opportunity2__r != null ? approvalObj.Quote__r.SBQQ__Opportunity2__r.Name  : '_Not Provided_') + '\n' +

                            '*Amount:* ' + (approvalObj.Quote__r != null && approvalObj.Quote__r.SBQQ__Opportunity2__r != null ? '$' + String.valueOf(approvalObj.Quote__r.SBQQ__Opportunity2__r.Amount): '_Not Provided_') + '\n' +

                            'Please review and select an action below.';




            sendToMultipleChannels(approvalMapwithUserChannel.get(approvalObj.Id), approvalObj.Id, message);

        }

    }

   

}
The class responsible for logging Integration errors.
 

SlackInteractiveReceiver.cls: An **@RestResource** class (urlMapping=’/SlackInteractions/*’) that handles the inbound HTTP POST request from Slack when an action button is clicked. It immediately returns a 200 OK response to Slack and enqueues a SlackPayloadProcessor for asynchronous processing.

@RestResource(urlMapping='/SlackInteractions/*')

global with sharing class SlackInteractiveReceiver {




    @HttpPost

    global static void handleInteraction() {

        // Use RestContext.request.params to safely read the 'payload' from form-data

        String payloadParam = RestContext.request.params.get('payload');




        if (String.isNotBlank(payloadParam)) {

            try {

                System.enqueueJob(new SlackPayloadProcessor(payloadParam));




                // CRUCIAL: Send 200 OK immediately to Slack to prevent timeouts/retries

                RestContext.response.statusCode = 200;

                RestContext.response.responseBody = Blob.valueOf('Request received and queued.');

            } catch (Exception ex) {

                // If queueing fails, return 500 so callers can retry or log alerts

                RestContext.response.statusCode = 500;

                RestContext.response.responseBody = Blob.valueOf('Error queueing payload: ' + ex.getMessage());

            }

        } else {

            // This handles cases where the payload is unexpectedly missing

            RestContext.response.statusCode = 400;

            RestContext.response.responseBody = Blob.valueOf('Error: Missing payload parameter.');

        }

    }

}

SlackPayloadProcessor.cls: A Queueable class that parses the complex Slack payload JSON, extracts the recordId, actionType (APPROVE/REJECT), and the actingSlackUserId, and then calls RecordUpdateService to perform the DML operation.

public with sharing class SlackPayloadProcessor implements Queueable {

   

    private String slackPayload;




    public SlackPayloadProcessor(String payload) {

        this.slackPayload = payload;

    }




    public void execute(QueueableContext context) {

        Id recordId = null;

        String channelId = null; // New variable to store Channel ID

        String messageTs = null; // New variable to store Message Timestamp

        String responseUrl = null;

        try {

            Map<String, Object> payloadMap = (Map<String, Object>)JSON.deserializeUntyped(this.slackPayload);

           

            // Extract core action details (existing logic)

            List<Object> actions = (List<Object>)payloadMap.get('actions');

            Map<String, Object> messageMap = (Map<String, Object>)payloadMap.get('message');

            Map<String, Object> action = (Map<String, Object>)actions[0];

            String recordActionValue = (String)action.get('value');

            List<String> parts = recordActionValue.split('_');

            recordId = parts[0];

            String actionType = parts[1];

            String originalText = '';

            // --- NEW: Extract Channel ID and Timestamp for message update ---

            Map<String, Object> container = (Map<String, Object>)payloadMap.get('container');

            if (container != null) {

                channelId = (String)container.get('channel_id');

                messageTs = (String)container.get('message_ts');

                responseUrl = (String)payloadMap.get('response_url');

                if (messageMap != null) {

                    List<Object> blocksList = (List<Object>)messageMap.get('blocks');

                    if (blocksList != null && !blocksList.isEmpty()) {

                        Map<String, Object> firstBlock = (Map<String, Object>)blocksList[0];

                        if ('section'.equals(firstBlock.get('type'))) {

                            Map<String, Object> textMap = (Map<String, Object>)firstBlock.get('text');

                            if (textMap != null) {

                                originalText = (String)textMap.get('text');

                            }

                        }

                    }

                }

               

            }

            RecordUpdateService.performUpdate(recordId, actionType, channelId, messageTs, responseUrl, recordActionValue, originalText);

           

        } catch (Exception e) {

            IntegrationLogger.logError(

                'PayloadProcessor',

                'Parsing Failure: ' + e.getMessage(),

                this.slackPayload,

                recordId

            );

            System.debug('Error processing Slack payload: ' + e.getMessage());

            return;

        }

    }

}

RecordUpdateService.cls: A utility class that updates the Salesforce Approval record status based on the received action type and attempts to resolve the acting Slack user to a Salesforce User to populate the sbaa__ApprovedBy__c or sbaa__RejectedBy__c fields. It also calls a separate utility to update the original message in Slack.

public without sharing class RecordUpdateService {




    /**

     * Performs the record update based on the Slack action.

     * Updates the sbaa__Approval__c record status and logs any errors.

     */

    public static void performUpdate(Id recordId, String actionType, String channelId, String messageTs, String responseUrl, String action, String originalMessage, User approver) {

         

        // Ensure the ID is valid before proceeding

        if (recordId == null || !recordId.getSObjectType().getDescribe().getName().equals('sbaa__Approval__c')) {

            IntegrationLogger.logError(

                'CaseUpdateService',

                'Invalid or Null Record ID received for Case update.',

                'RecordId: ' + recordId + ', ActionType: ' + actionType,

                recordId

            );

            throw new CalloutException('CaseUpdateService ++ Invalid or Null Record ID received for Case update.' + 'RecordId: ' + recordId + ', ActionType: ' + actionType);

        }

        try {

            sbaa__Approval__c recordToUpdate = new sbaa__Approval__c(Id = recordId);

            String finalMessage; // Variable to hold the final message text

   

            if (actionType == 'APPROVE') {

                recordToUpdate.sbaa__Status__c = 'Approved';

                finalMessage = '✅ *Approval Complete:* This request was *APPROVED*.';

                if (approver != null) {

                    recordToUpdate.sbaa__ApprovedBy__c = approver.Id;

                }

            } else if (actionType == 'REJECT') {

                recordToUpdate.sbaa__Status__c = 'Rejected';

                finalMessage = '❌ *Approval Complete:* This request was *REJECTED*.';

                if (approver != null) {

                    recordToUpdate.sbaa__RejectedBy__c = approver.Id;

                }

            }

            String approverName = (approver != null) ? approver.Name : userInfo.getName();

       

            update recordToUpdate;

           

            // 1. DML Success: Call method to update the Slack message

            if (String.isNotBlank(channelId) && String.isNotBlank(messageTs)) {

                SlackUpdater.updateOriginalMessage(responseUrl, action, originalMessage, approverName);

            }




        } catch (DmlException e) {

            // Log DML specific errors that might occur even without sharing

           IntegrationLogger.logError(

                'CaseUpdateService',

                'DML Error during Case Update: ' + e.getMessage(),

                'RecordId: ' + recordId + ', ActionType: ' + actionType,

                recordId

            );

            throw new CalloutException('CaseUpdateService ++ DML Error during Case Update: ' + e.getMessage() + 'RecordId: ' + recordId + ', ActionType: ' + actionType);




        }

    }




    //To Identify the matching slack user in Salesforce

    public static User resolveApprover(String slackUserId, String slackUsername) {

        try {

            // 1) If we have a Slack user id, try to fetch email from Slack API

            if (slackUserId != null) {

                String slackEmail = SlackUtil.getUserEmail(slackUserId);

                if (slackEmail != null) {

                    List<User> exact = [SELECT Id, Name FROM User WHERE Email = :slackEmail LIMIT 1];

                    if (!exact.isEmpty()) return exact[0];

                }

                // Try FederationIdentifier match as a fallback

                List<User> fed = [SELECT Id, Name FROM User WHERE FederationIdentifier = :slackUserId LIMIT 1];

                if (!fed.isEmpty()) return fed[0];

            }




            // 2) If we have a username (may be email-like or partial), try matching by email or username

            if (slackUsername != null) {

                if (slackUsername.contains('@')) {

                    List<User> byEmail = [SELECT Id, Name FROM User WHERE Email = :slackUsername LIMIT 1];

                    if (!byEmail.isEmpty()) return byEmail[0];

                }




                // Try exact username

                List<User> byUsername = [SELECT Id, Name FROM User WHERE Username = :slackUsername LIMIT 1];

                if (!byUsername.isEmpty()) return byUsername[0];




                // 3) Fuzzy email match: find users whose email contains the username token

                String token = slackUsername.replaceAll('[^a-zA-Z0-9@._-]', '');

                if (token.length() > 2) {

                    List<User> fuzzy = [SELECT Id, Name FROM User WHERE Email LIKE :('%' + token + '%') LIMIT 1];

                    if (!fuzzy.isEmpty()) return fuzzy[0];

                }




                // 4) Try splitting username into parts and match against Name

                List<String> parts = new List<String>();

                if (slackUsername.contains('.')) parts.addAll(slackUsername.split('\\.'));

                else if (slackUsername.contains('_')) parts.addAll(slackUsername.split('_'));

                else parts.add(slackUsername);




                for (String p : parts) {

                    String ptrim = p.trim();

                    if (ptrim.length() > 2) {

                        List<User> nameMatch = [SELECT Id, Name FROM User WHERE Name LIKE :('%' + ptrim + '%') LIMIT 1];

                        if (!nameMatch.isEmpty()) return nameMatch[0];

                    }

                }

            }

        } catch (Exception ex) {

            IntegrationLogger.logError('RecordUpdateService.resolveApprover', 'Error resolving approver: ' + ex.getMessage(), slackUserId + ' / ' + slackUsername, null);

        }




        return null;

    }

}
SlackUpdater: The functionality to update the Slack message after an approval action is primarily handled by the Apex class RecordUpdateService.cls, which utilizes a utility method within the SlackUtil.cls class.
/**

 * @description Handles the callout to Slack's chat.update API method.

 */

public with sharing class SlackUpdater {

   

    private static final String SLACK_UPDATE_ENDPOINT = 'callout:Slack_Bot_Token/chat.update';

    @future(callout=true)

    public static void updateMessage(String channelId, String messageTs, String finalMessage) {

        String payload = SlackUtil.buildUpdatePayload(channelId, messageTs, finalMessage + ' (Channel: ' + channelId + ')');

        SlackUtil.SlackResponse res = SlackUtil.postJson(payload);




        if (res.errorMessage != null) {

            IntegrationLogger.logError('SlackUpdater', 'HTTP callout exception: ' + res.errorMessage, payload, null);

        } else if (res.statusCode != 200) {

            IntegrationLogger.logError('SlackUpdater', 'Slack API Failed to update message. Status: ' + res.statusCode + ', Body: ' + res.body, payload, null);

        }

    }

   

    @future(callout=true)

    public static void updateOriginalMessage(String responseUrl, String action, String originalMessage, String approverName) {

       

        // 1. Determine the result and the new message text

        String outcome = (action.contains('_APPROVE')) ? 'APPROVED' : 'REJECTED';

        String style = (action.contains('_APPROVE')) ? 'good' : 'danger'; // Optional: for old attachments, or custom styling

        String responseNotification = ' *Request ' + outcome + '* | Actioned by: ' + approverName;

        // 2. Construct the new, final message payload

        Map<String, Object> newPayload = new Map<String, Object>();

        newPayload.put('replace_original', true); // CRUCIAL: Tells Slack to replace the old message

       

        // Build the new message blocks (simplified)

        List<Object> blocks = new List<Object>();

       

        // New section block to show the result

        blocks.add(new Map<String, Object>{

            'type' => 'section',

            'text' => new Map<String, String>{

                'type' => 'mrkdwn',

                'text' =>  originalMessage// Get the Salesforce user's name

            }

        });

       

        // Block 2: The final status (replaces the buttons)

        blocks.add(new Map<String, Object>{

            'type' => 'context', // Context block is good for status/metadata

            'elements' => new List<Object>{

                new Map<String, String>{

                    'type' => 'mrkdwn',

                    'text' => responseNotification

                }

            }

        });

        newPayload.put('blocks', blocks);

        String finalPayload = JSON.serialize(newPayload);




        SlackUtil.SlackResponse res = SlackUtil.postToUrl(responseUrl, finalPayload);

        if (res.errorMessage != null) {

            IntegrationLogger.logError('SlackUpdater', 'HTTP callout exception to response_url: ' + res.errorMessage, finalPayload, null);

        } else if (res.statusCode != 200) {

            IntegrationLogger.logError('SlackUpdater', 'Error updating Slack message: ' + res.body, finalPayload, null);

        } else {

            System.debug('Original Slack message successfully updated.');

        }

    }

}

4- Record-trigger Flow:

      1. Create Flow: Go to SetupFlows. Click New Flow and choose Record-Triggered Flow.
      2. Configure Trigger: Set the Object to Approval and the Trigger to A record is created.
      3. Add Action: Add an Action element that calls the SlackMessageSender Apex action and passes the new Approval record’s ID using {!$Record.Id}.
      4. Save and Activate the flow.

Results

The successful deployment of this custom integration delivered measurable results that dramatically improved operational efficiency:

1. 90% Reduction in Approval Time
      1. By removing the “context switching tax,” the average time it took for critical approvals to be actioned dropped from hours (or even days) to just minutes. This directly impacts the speed of closing deals and moving internal projects forward.
2. Enhanced Process Adherence
      1. The clear, structured interface in Slack ensures that approvers see all mandatory information before making a decision. 
      2. The integration also ensures that decisions are instantly and accurately logged in Salesforce, eliminating the risk of human error from manual data entry.
3. Increased User Adoption & Satisfaction
      1. Teams no longer see the approval process as a cumbersome task. 
      2. By integrating it seamlessly into their daily communication tool (Slack), the custom solution improved the overall user experience for both requesters and approvers.
Looking for a First-Class Business Consultant?