This guide will help you customize your Agentforce agent with custom Slack actions. Implementing custom Slack actions in your agent allows it to carry out certain tasks in Slack via the Slack Web API, such as sending a message to a channel or creating a canvas with content from the agent's response. To use standard Slack actions in your agent, refer to the Slack Agent Actions documentation.
Prerequisites
This guide assumes you have a connected Salesforce org and Slack org, as well as an Agentforce agent that can be modified. If you don't, use the Connect Salesforce and Slack guide and the Salesforce Create Agents guide to get set up.
In order to use the Slack Web API in custom actions in Agentforce, you first need to set up a way for your agents to get proper Slack credentials. You do this by creating a Slack app and using an Auth. Provider
in the Salesforce Platform to manage the authentication flow between your Slack app and Salesforce users.
Start by logging into your Slack org and create a new app. In the Create an app modal, select From a manifest, then select a workspace in which to develop your app and click Next.
Highlight the contents of the placeholder JSON and replace it with this:
{
"display_information": {
"name": "Agentforce Custom Actions"
},
"features": {
"bot_user": {
"display_name": "Agentforce Custom Actions",
"always_online": false
}
},
"oauth_config": {
"scopes": {
"bot": [
"chat:write",
"chat:write.public",
"mpim:read",
"reactions:write",
"channels:history",
"canvases:write",
"channels:join",
"channels:read",
"team:read"
]
}
},
"settings": {
"org_deploy_enabled": true,
"socket_mode_enabled": false,
"token_rotation_enabled": false
}
}
Click Next then Create.
Select Install App in the sidebar. Click Install to Organization. Allow the app access to the org.
Select Basic Information in the sidebar. Copy down your Client ID and Client Secret; we'll use these later.
To use your Slack app credentials in your agent, you need to first create an auth provider, allowing you to complete the authentication flow needed for the Salesforce Platform to get the proper credentials.
Log into your Salesforce org and open Setup
from the gear icon in the upper right. Use the Quick Find to search for Auth. Providers
, click on it, then click the New
button above the list of existing providers to create a new provider. Enter the values for the fields listed below.
Field | Value |
---|---|
Provider Type |
Open ID Connect |
Name |
Slack |
URL Suffix |
Leave the value created by entering a Name |
Consumer Key |
Client ID of your Slack app |
Consumer Secret |
Client Secret of your Slack app |
Authorize Endpoint URL |
https://slack.com/oauth/v2/authorize |
Token Endpoint URL |
https://slack.com/api/oauth.v2.access |
Click Save.
Scroll down the page and copy the Callback URL
.
Navigate back to your Slack app settings and add the callback URL to your app in the OAuth & Permissions settings page.
Once you have an auth provider, it’s time to set up an external credential. External credentials are the actual record of credentials for external services. This is what stores your tokens and connects them to a Principal for use in permission sets or user profiles.
Within your Salesforce org setup, search for and click on Named Credentials
; then within the settings click the External Credentials
tab, then click the New
button located above the list of external credentials. Enter the following values for the fields listed below:
Field | Value |
---|---|
Label |
Slack |
Name |
Slack |
Authentication Protocol |
OAuth 2.0 |
Authentication Flow Type |
Browser Flow |
Scope |
Leave blank |
Identity Provider |
Select Authentication Provider as the type and select the auth provider you created in the previous step |
Save the external credential.
In the settings for the external credential you created, under Principals
, click New
. Enter the following values for the fields listed below:
Field | Value |
---|---|
Parameter Name |
Enter your Slack app name (Agentforce Custom Actions ) |
Sequence Number |
1 |
Identity Type |
Named Principal |
Scope |
chat:write , chat:write.public , mpim:read , reactions:write , channels:history , canvases:write , channels:join , channels:read , team:read |
Note on Named Principal
We want to use a Named Principal
when we want to share credentials (i.e. agent actions on the agent's behalf) and a User Principal
when we want to keep credentials scoped to a user (i.e. agent takes actions on your behalf).
Save your changes.
Click the drop-down toggle in the Actions
section for your new Principal.
Click Authenticate
to start the browser authentication flow with your Slack app. When the Slack authentication browser page opens, be sure to use the workspace picker in the header to switch to installation at the org level. Problems can occur authenticating if the browser is used to log in to any other Salesforce or Slack orgs, so it's safest to create a separate browser profile or use an incognito browser if you run into issues.
Now we can enable external credentials for the Einstein Agent User
and System Administrator
profiles.
Profiles
, then select it from the options.Einstein Agent User
profile and select it. In the Apps
section, select External Credentials Principal Access
.
Enable External Credential Principal Access
section, click Edit
.System Administrator
profile.Named Credential
Now that you have an external credential to allow proper access to your Slack app, you’ll need a Named Credential
to finalize your API configuration with the proper base URL and any custom headers you may want to include. This is what will be used in Apex classes for interacting with the Slack Web API.
Named Credentials
in Salesforce Setup.Named Credentials
tab, click New
.
Slack API
.Slack_API
.https://slack.com/api
.Slack
from the drop-down.There are two methods of developing with Apex: using the Code Builder
developer environment within the Salesforce Platform or using VSCode with the proper Salesforce extensions installed. This guide focuses on using VSCode as your developer environment.
sf --version
in the terminal of your choice.Salesforce Extension Pack
—You can install the extended pack for Salesforce extensions which has all these packages included (and a few others that aren’t needed for this guide).Prettier
ESLint
XML
From the VSCode command palette (Cmd/Ctrl + Shift + P
), search for SFDX:Create Project
and select it from the list of options.
For the template type, leave Standard
selected and press Enter
.
Give your project a name like Custom Slack Actions
and press Enter
.
Choose a destination on your computer for the project and click Create Project
. If you see an error about Apex not being able to find a Java runtime, then you’ll need to install OpenJDK version 21 (if not already installed) and set the path in your workspace settings. See Install JDK below.
If you see an error about Apex not being able to find the Java runtime, follow these steps:
.dmg
(for macOS) or .msi
(for Windows) for the latest version for your set up..dmg
or .msi
, leaving all defaults as is.java: home
.Salesforcedx-vscode-apex › Java: Home
property, enter /Library/Java/JavaVirtualMachines/zulu-21.jdk/Contents/Home
for macOS or C:\Program-Files\Zulu\zulu-21\
for Windows.Cmd/Ctrl + Shift + P
, search for and select Developer: Reload Window
).From the VSCode command palette, search for SFDX: Authorize an Org
and select it from the list of options.
Project Default
selected for login URL source and press Enter
.customActionsOrg
or leave the default and press Enter
.Username
and Password
and click Log In
.Allow
.Now that your credentials and developer environment are ready, we move on to the real fun—creating custom actions.
Apex actions use the Invocable Method
annotation syntax to define how the class can be used in platform features that support actions, like Flow and Agentforce. You'll use invocable methods that call out to the Slack API using the named credentials you created to authenticate. For this guide, you'll create two actions:
GetSlackChannelAction
(from the auth.teams.list
and conversations.list
methods) to look up channels by name and workspace.SendSlackMessageAction
(from the chat.postMessage
method) to allow the agent to send messages to a given channel.Here is an example request with a named credential:
request.setEndpoint('callout:Slack_API/chat.postMessage');
Why do I need two actions to send a message?
Technically you don't. You could have one action take the channel name, the org, and the message content to complete the task. However, it's useful to think of actions as composable steps.
Looking up a channel by it's name is useful in a lot of use cases so it makes sense to be it's own action! Then we can rely on the instructions in our topics to inform how the agent uses these actions together, in this case allowing users to provide channel names when sending messages, and the agent can use the get channel action to get the proper data needed to send the message in Slack.
From the VSCode command palette, search for SFDX: Create Apex Class
and select it from the list of options.
Name your class GetSlackChannelAction
and press Enter
.
Leave the default path for the destination and press Enter
.
Replace the contents of the file with the code shown here, then save the file.
/**
* GetSlackChannelAction
*
* This class provides a Flow-invocable action to search for a Slack channel by name within a specific workspace
* in a Slack Enterprise Grid environment. It handles the pagination of results and provides detailed channel information.
*
* The process:
* 1. Takes a workspace name and channel name as input
* 2. Queries auth.teams.list to find the workspace ID from the name
* 3. Uses the workspace ID to search for the channel using conversations.list
* 4. Returns channel details if found
*/
public class GetSlackChannelAction {
/**
* Input class for the Flow action
* Requires both workspace name and channel name to perform the search
*/
public class ChannelSearchInput {
@InvocableVariable(required=true description='Name of the channel to search for')
public String channel_name;
@InvocableVariable(required=true description='Name of the Slack workspace')
public String workspace_name;
}
/**
* Output class containing channel details or error information
* Returns basic channel information including ID, name, member count, and topic
*/
public class ChannelSearchOutput {
@InvocableVariable(description='Channel ID')
public String channel_id;
@InvocableVariable(description='Channel Name')
public String channel_name;
@InvocableVariable(description='Number of members in the channel')
public Integer num_members;
@InvocableVariable(description='Channel topic')
public String topic;
@InvocableVariable(description='Error message if search failed')
public String error_message;
}
// ------------------------
// API Response Structures
// ------------------------
/**
* Response structure for auth.teams.list endpoint
* Used to find workspace/team ID from workspace name
*/
private class TeamsListResponse {
public Boolean ok;
public List<Team> teams;
public ResponseMetadata response_metadata;
public String error;
}
/**
* Structure representing a Slack workspace/team
*/
private class Team {
public String id;
public String name;
}
/**
* Response structure for conversations.list endpoint
* Contains list of channels and pagination metadata
*/
private class SlackResponse {
public Boolean ok;
public List<Channel> channels;
public ResponseMetadata response_metadata;
public String error;
}
/**
* Metadata structure containing pagination information
*/
private class ResponseMetadata {
public String next_cursor;
}
/**
* Structure representing a Slack channel
* Contains only the fields we need for our output
*/
private class Channel {
public String id;
public String name;
public Integer num_members;
public Topic topic;
}
/**
* Structure representing a channel's topic
*/
private class Topic {
public String value;
}
/**
* Main invocable method for the Flow action
* Processes a list of inputs (bulk processing support) and returns corresponding outputs
*/
@InvocableMethod(label='Get Slack Channel'
description='Searches for a Slack channel by name in specified workspace and returns its details')
public static List<ChannelSearchOutput> searchChannel(List<ChannelSearchInput> inputs) {
List<ChannelSearchOutput> outputs = new List<ChannelSearchOutput>();
// Process each input in the list (supporting bulk operations)
for(ChannelSearchInput input : inputs) {
ChannelSearchOutput output = new ChannelSearchOutput();
try {
// Step 1: Get the workspace ID from the workspace name
String teamId = getWorkspaceId(input.workspace_name);
if (teamId == null) {
output.error_message = 'Workspace "' + input.workspace_name + '" not found';
outputs.add(output);
continue;
}
// Step 2: Search for the channel in the identified workspace
Channel foundChannel = searchAllChannels(input.channel_name, teamId);
// Step 3: Process results
if (foundChannel != null) {
// Channel found - populate output with channel details
output.channel_id = foundChannel.id;
output.channel_name = foundChannel.name;
output.num_members = foundChannel.num_members;
if (foundChannel.topic != null) {
output.topic = foundChannel.topic.value;
}
} else {
// Channel not found
output.error_message = 'Channel not found';
}
} catch(Exception e) {
// Handle any errors that occur during processing
output.error_message = 'Error: ' + e.getMessage();
System.debug('Error details: ' + e.getStackTraceString());
}
outputs.add(output);
}
return outputs;
}
/**
* Gets the workspace ID for a given workspace name using auth.teams.list
* This method handles the Enterprise Grid workspace lookup
*
* @param workspaceName The name of the workspace to find
* @return The workspace ID if found, null if not found
*/
private static String getWorkspaceId(String workspaceName) {
// Initialize HTTP request
Http http = new Http();
HttpRequest request = new HttpRequest();
request.setEndpoint('callout:Slack_API/auth.teams.list');
request.setMethod('GET');
// Make the API call
HttpResponse response = http.send(request);
// Check for successful response
if (response.getStatusCode() != 200) {
throw new CalloutException('Teams list failed with status code: ' + response.getStatusCode());
}
// Parse the response
TeamsListResponse teamsResponse = (TeamsListResponse)JSON.deserialize(
response.getBody(),
TeamsListResponse.class
);
if (!teamsResponse.ok) {
throw new CalloutException('Teams list not OK: ' + teamsResponse.error);
}
// Search for matching workspace name
for (Team team : teamsResponse.teams) {
if (team.name.equalsIgnoreCase(workspaceName)) {
return team.id;
}
}
// No matching workspace found
return null;
}
/**
* Searches for a channel by name within a specific workspace
* Handles pagination to search through all available channels
*
* @param channelName The name of the channel to find
* @param teamId The ID of the workspace to search in
* @return Channel object if found, null if not found
*/
private static Channel searchAllChannels(String channelName, String teamId) {
String cursor = null;
do {
// Construct endpoint URL with team_id and pagination cursor
String endpoint = 'callout:Slack_API/conversations.list?team_id=' + teamId;
if (String.isNotBlank(cursor)) {
endpoint += '&cursor=' + EncodingUtil.urlEncode(cursor, 'UTF-8');
}
// Initialize HTTP request
Http http = new Http();
HttpRequest request = new HttpRequest();
request.setEndpoint(endpoint);
request.setMethod('GET');
// Make the API call
HttpResponse response = http.send(request);
// Check for successful response
if (response.getStatusCode() != 200) {
throw new CalloutException('Failed with status code: ' + response.getStatusCode());
}
// Parse the response
SlackResponse slackResponse = (SlackResponse)JSON.deserialize(
response.getBody(),
SlackResponse.class
);
if (!slackResponse.ok) {
throw new CalloutException('Slack API response not OK: ' + slackResponse.error);
}
// Search through current page of channels
for (Channel channel : slackResponse.channels) {
if (channel.name.equalsIgnoreCase(channelName)) {
return channel;
}
}
// Get cursor for next page of results
cursor = (slackResponse.response_metadata != null) ?
slackResponse.response_metadata.next_cursor : null;
} while (String.isNotBlank(cursor)); // Continue while there are more pages
// Channel not found after searching all pages
return null;
}
}
SFDX: Deploy This Source to Org
. You can also access this command by right-clicking in the file and selecting the option from the menu.From the VSCode file explorer, navigate to scripts/apex/hello.apex
.
Replace the contents of the file with this testing script. Be sure to change workspaceName
and channelName
to yours.
GetSlackChannelAction.ChannelSearchInput input = new GetSlackChannelAction.ChannelSearchInput();
input.workspaceName = 'SDO'; // Change this to match your workspace
input.channelName = 'general'; // Change this to match your channel
List<GetSlackChannelAction.ChannelSearchInput> inputs = new List<GetSlackChannelAction.ChannelSearchInput>();
inputs.add(input);
List<GetSlackChannelAction.ChannelSearchOutput> results = GetSlackChannelAction.searchChannel(inputs);
// Print results
GetSlackChannelAction.ChannelSearchOutput result = results[0];
if(String.isNotBlank(result.errorMessage)) {
System.debug('Error: ' + result.errorMessage);
} else {
System.debug('Success! Channel found:');
System.debug('Channel ID: ' + result.channelId);
System.debug('Channel Name: ' + result.channelName);
System.debug('Members: ' + result.numMembers);
System.debug('Topic: ' + result.topic);
}
From the VSCode command palette (Cmd/Ctrl + Shift + P
), search for and select SFDX: Execute Anonymous Apex with Editor Contents
to execute the script.
In VSCode’s OUTPUT
tab, check to ensure you were successfully able to look up a channel by name.
SendSlackMessageAction
.
From the VSCode command palette, search for SFDX: Create Apex Class
and select it from the list of options.
Name your class SendSlackMessageAction
and press Enter
.
Leave the default path for the destination and press Enter
.
Replace the contents of the file with the code shown here, then save the file.
/**
* SendSlackMessageAction
*
* Invocable Apex action that sends a message to a Slack channel.
* Requires direct Channel ID and Workspace ID rather than names.
* Uses Named Credential 'Slack_API' for authentication.
*/
public class SendSlackMessageAction {
/**
* Invocable method input class
*/
public class MessageInput {
@InvocableVariable(required=true description='ID of the channel to send to')
public String channel_id;
@InvocableVariable(required=true description='ID of the workspace to send to')
public String workspace_id;
@InvocableVariable(required=true description='Message text to send')
public String text;
}
/**
* Invokable method output class returned to Flow/Process Builder
* Provides message send status, timestamp for reference, and any error details
* Message timestamp (ts) can be used as a message identifier for later updates/deletion
*/
public class MessageOutput {
@InvocableVariable(description='True if message was sent successfully')
public Boolean is_success;
@InvocableVariable(description='Timestamp of sent message')
public String message_ts;
@InvocableVariable(description='Error message if send failed')
public String error_message;
}
/**
* Main invocable method to send a message to Slack
* Handles a list of inputs for bulk processing but typically receives one input
* Returns a corresponding list of outputs with success/failure details
*
* @param inputs List of MessageInput objects containing message details
* @return List<MessageOutput> Results of the message send operation(s)
*/
@InvocableMethod(label='Send Slack Message To Channel')
public static List<MessageOutput> sendMessage(List<MessageInput> inputs) {
List<MessageOutput> outputs = new List<MessageOutput>();
// Process each input (usually just one)
for(MessageInput input : inputs) {
MessageOutput output = new MessageOutput();
output.is_success = false; // Default to false until success confirmed
try {
// Setup HTTP request to Slack API
Http http = new Http();
HttpRequest request = new HttpRequest();
request.setEndpoint('callout:Slack_API/chat.postMessage');
request.setMethod('POST');
request.setHeader('Content-Type', 'application/json');
// Construct message payload
Map<String, String> body = new Map<String, String>{
'channel' => input.channel_id,
'text' => input.text,
'team_id' => input.workspace_id
};
request.setBody(JSON.serialize(body));
// Send message to Slack
HttpResponse response = http.send(request);
// Parse the JSON response
Map<String, Object> responseBody = (Map<String, Object>)JSON.deserializeUntyped(response.getBody());
// Check for success and process results
if(response.getStatusCode() == 200 && responseBody.get('ok') == true) {
output.is_success = true;
output.message_ts = (String)responseBody.get('ts');
} else {
// Capture API error message if present
output.error_message = 'API Error: ' + responseBody.get('error');
}
} catch(Exception e) {
// Handle any exceptions (callout errors, parsing errors, etc)
output.error_message = 'Error: ' + e.getMessage();
System.debug('Error details: ' + e.getStackTraceString());
}
outputs.add(output);
}
return outputs;
}
}
Deploy the class to your org by running the following command in the VSCode command palette: SFDX: Deploy This Source to Org
. You can also access this command by right-clicking in the file and selecting the option from the menu.
From the VSCode file explorer, navigate to scripts/apex/hello.apex
.
Replace the contents of the file with this testing script. Be sure to change workspaceName
and channelName
to yours.
SendSlackMessageAction.MessageInput input = new SendSlackMessageAction.MessageInput();
input.workspace_id = 'T06HLNFMU22'; // Replace with your workspace ID
input.channel_id = 'C06HT8GNC03'; // Replace with your channel ID
input.text = 'Test message from Salesforce ' + Datetime.now();
List<SendSlackMessageAction.MessageInput> inputs = new List<SendSlackMessageAction.MessageInput>();
inputs.add(input);
List<SendSlackMessageAction.MessageOutput> results = SendSlackMessageAction.sendMessage(inputs);
// Print results
SendSlackMessageAction.MessageOutput result = results[0];
if(result.is_success) {
System.debug('Message sent successfully!');
System.debug('Message Timestamp: ' + result.message_ts);
} else {
System.debug('Failed to send message');
System.debug('Error: ' + result.error_message);
}
From the VSCode command palette (Cmd/Ctrl + Shift + P
), search for and select SFDX: Execute Anonymous Apex with Editor Contents
to execute the script.
In VSCode’s OUTPUT
tab, check to ensure you were successfully able to look up a channel by name.
Now that you have your Apex classes deployed to the org, it’s time to create the custom actions for use in Agent Builder.
Profiles
in Search Setup
and select it from the list of options.Einstein Agent User
from the list of profiles.Apps
section, select Apex Class Access
.Apex Class Access
section, select Edit
.Save
. You need to save per page of classes, and your classes may appear on different pages.Agent Actions
and select the setting.+ New Agent Action
.
Reference Action Type
select Apex
.Reference Action
, search for Get Slack Channel
. It may take a few moments for this option to show up; give it a moment!Agent Action Label
and Agent Action API Name
leave the default values.Next
.Agent Action Configuration
form:
Agent Action Instructions
and the Inputs
and Outputs
instructions.Outputs
, check the Show in conversation
checkbox.Finish
.Send Slack Message
Apex class you created. In the Quick Find search in Setup, search for Agent Actions
and select the setting.+ New Agent Action
.
Reference Action Type
select Apex
.Reference Action
, search for Send Slack Message
.Agent Action Label
and Agent Action API Name
leave the default values.Next
.Agent Action Configuration
form:
Agent Action Instructions
and the Inputs
and Outputs
instructions.Outputs
, check the Show in conversation
checkbox.Finish
.Agents
settings in Setup, click on the agent of your choice and select Open in Builder
.Topics
tab in the left sidebar, click the New
button and select New Topic
:
Topic Label
, enter Send a Slack Message
.Classification Description
, enter: Helps the user send messages in Slack to specific channels. Can also look up a Slack channel by name and workspace to assist in sending messages.
Scope
, enter: You assist users with sending messages in Slack. You may look up Slack channels by name and workspace to help you identify any IDs needed to send Slack messages.
Instructions
, enter the following instructions (you may need to click Add instructions
to enter them as separate instructions):
If a user asks you to send a message to a channel and provides the channel name instead of an ID, use the Get Slack Channel action to look up the appropriate channel ID.
When users provide channel names, remove the ‘#’ at the beginning if they include one.
Next
.Finish
.Conversation Preview
sidebar to refresh your agent.Activate
button to start the agent.Look at that; you built an agent that works with Slack! Let's deploy it there next.
Follow these steps to deploy your agent to Slack.
Install for All Users
Einstein Bots
. Select Einstein Bots
under Einstein Platform
, then switch the toggle in the upper right to On
.Connections
tab in the sidebar nav; it looks like a stack of squares. Under Connections
, click Add
.API
and give it a descriptive name in the Integration Name
field. This must be distinct from other agent connections you've created.Connected App
field. Click on "Slack" as it appears in the autocomplete.Save
the connection.Active
before moving on to the next steps.Agents
under Agent Studio
.
Find your desired agent in the list of agents and click the arrow button on the far right, then select Edit
.Connections
tab, under Connections
, click the Add
button.API
and give it a descriptive name in the Integration Name
field. This must be distinct from other agent connections you've created.Connected App
field. Click on "Slack" as it appears in the autocomplete.Save
the connection.Active
before moving on to the next steps.Salesforce
> Agentforce
Requested Agents
find the agent you added your Slack actions to and click Review Agent
. Only agents with the word Agent
in their label will show up in your Slack org for review and installation.Install Agent
Choose Workspaces
and add to any workspaces you want the agent in, click Next
, then Next
againWho can use this agent?
, select Everyone
Add Agent
Agentforce
tab in your Slack workspace.
Preferences
.Navigation
, select the checkboxes for Show app agents
and the name of your agent.Preferences
and reload Slack. You should now see Agentforce
in the side nav. Click on it.While this guide explored just two Slack Web API methods for getting a channel and sending a message, there are a wealth of methods available to customize your agent's capabilities even more. Check out the full scope of the Slack API here, create Apex actions for them as outlined in this guide, and use the created actions as topics in your agent!
Looking to customize your agent without the code? Explore the available standard Slack actions here.