Developing automations requires a Business+ or Enterprise Grid plan. Don't have one? Join the Developer Program and provision a sandbox with access to all Slack features for free.
Welcome to the Welcome Bot tutorial! We hope you enjoy your stay here!
What a nice message to read! It sure would be nice if everyone joining a Slack channel received such a message!
In this tutorial you'll learn how to create a Slack app that sends a friendly welcome message, similar to the one at the top of this page, to a user when they join a channel. A user in the channel will be able to create the custom message from a form.
Before we begin, ensure you have the following prerequisites completed:
slack auth list
and ensure your workspace is listed.You need the proper equipment before you can start building your app.
Create a blank app with the Slack CLI using the following command:
slack create welcome-bot-app --template https://github.com/slack-samples/deno-blank-template
A new app folder will be created. Once you have your new project ready to go, change into your project directory. You'll be bouncing between a few folders, so we recommend using an editor that streamlines switching between files.
Welcome to your Slack app! There may not be a welcoming message, but do not fret, you can make yourself at home here. Slack apps are built around their flexibility; don't be afraid to run wild!
For now though, just make three folders within your app folder. Each folder will contain a fundamental building block of a Slack app:
functions
workflows
triggers
With the setup complete, you can get building!
If you want to follow along without placing the code yourself, use the pre-built Welcome Bot app:
slack create welcome-bot-app --template https://github.com/slack-samples/deno-welcome-bot
Once you have your new project ready to go, change into your project directory.
An app manifest is where you define the key attributes of your Slack app.
The app manifest provides a sneak peak at what you'll be building throughout the rest of this tutorial. The recipe for the Welcome Bot app calls for:
MessageSetupWorkflow
SendWelcomeMessageWorkflow
WelcomeMessageDatastore
Put that all together and your manifest.ts
file will look like:
// /manifest.ts
import { Manifest } from "deno-slack-sdk/mod.ts";
import { WelcomeMessageDatastore } from "./datastores/messages.ts";
import { MessageSetupWorkflow } from "./workflows/create_welcome_message.ts";
import { SendWelcomeMessageWorkflow } from "./workflows/send_welcome_message.ts";
export default Manifest({
name: "Welcome Message Bot",
description:
"Quick way to setup automated welcome messages for channels in your workspace.",
icon: "assets/default_new_app_icon.png",
workflows: [MessageSetupWorkflow, SendWelcomeMessageWorkflow],
outgoingDomains: [],
datastores: [WelcomeMessageDatastore],
botScopes: [
"chat:write",
"chat:write.public",
"datastore:read",
"datastore:write",
"channels:read",
"triggers:write",
"triggers:read",
],
});
We've provided you all this upfront to streamline the tutorial, but you would likely build up your manifest as you add workflows and datastores to your app.
Workflows are a sequenced set of steps, interactions and functions chained together.
In this step we'll be creating a workflow named MessageSetupWorkflow
. This workflow will contain the functions needed for someone in the channel to create a welcome message with a form.
Create a file named create_welcome_message.ts
within the workflows
folder. There you'll add the following workflow definition:
// /workflows/create_welcome_message.ts
import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";
import { WelcomeMessageSetupFunction } from "../functions/create_welcome_message.ts";
/**
* The MessageSetupWorkflow opens a form where the user creates a
* welcome message. The trigger for this workflow is found in
* `/triggers/welcome_message_trigger.ts`
*/
export const MessageSetupWorkflow = DefineWorkflow({
callback_id: "message_setup_workflow",
title: "Create Welcome Message",
description: " Creates a message to welcome new users into the channel.",
input_parameters: {
properties: {
interactivity: {
type: Schema.slack.types.interactivity,
},
channel: {
type: Schema.slack.types.channel_id,
},
},
required: ["interactivity"],
},
});
The input_parameters
you need are interactivity
and channel
. The interactivity
parameter enables interactive elements, like the form you'll set up next.
You add functions to workflows by using the addStep
method. In this case, you'll be adding the form the user will interact with.
This is done using a Slack function. Slack functions give you the ability to add common Slack functionality without the need to do so from scratch.
The Slack function to use here is the OpenForm
function. Add it to your create_welcome_message.ts
workflow like so:
// /workflows/create_welcome_message.ts
/**
* This step uses the OpenForm Slack function. The form has two
* inputs -- a welcome message and a channel id for that message to
* be posted in.
*/
const SetupWorkflowForm = MessageSetupWorkflow.addStep(
Schema.slack.functions.OpenForm,
{
title: "Welcome Message Form",
submit_label: "Submit",
description: ":wave: Create a welcome message for a channel!",
interactivity: MessageSetupWorkflow.inputs.interactivity,
fields: {
required: ["channel", "messageInput"],
elements: [
{
name: "messageInput",
title: "Your welcome message",
type: Schema.types.string,
long: true,
},
{
name: "channel",
title: "Select a channel to post this message in",
type: Schema.slack.types.channel_id,
default: MessageSetupWorkflow.inputs.channel,
},
],
},
},
);
This creates a form that will show the following fields:
The user can then submit the form.
When the user submits the form, they'll want confirmation that it is submitted.
You can do this by using the Slack SendEphemeralMessage
function. Add the following step to your create_welcome_message.ts
workflow:
// /workflows/create_welcome_message.ts
/**
* This step takes the form output and passes it along to a custom
* function which sets the welcome message up.
* See `/functions/setup_function.ts` for more information.
*/
MessageSetupWorkflow.addStep(WelcomeMessageSetupFunction, {
message: SetupWorkflowForm.outputs.fields.messageInput,
channel: SetupWorkflowForm.outputs.fields.channel,
author: MessageSetupWorkflow.inputs.interactivity.interactor.id,
});
/**
* This step uses the SendEphemeralMessage Slack function.
* An ephemeral confirmation message will be sent to the user
* creating the welcome message, after the user submits the above
* form.
*/
MessageSetupWorkflow.addStep(Schema.slack.functions.SendEphemeralMessage, {
channel_id: SetupWorkflowForm.outputs.fields.channel,
user_id: MessageSetupWorkflow.inputs.interactivity.interactor.id,
message:
`Your welcome message for this channel was successfully created! :white_check_mark:`,
});
export default MessageSetupWorkflow;
This function takes the provided message
text and sends it to the specified user and channel, both pulled from the OpenForm
function step above.
Wonderful! Now let's build functionality to handle that welcome message once its submitted by a user.
Datastores provide a place for your app to store and retrieve data.
The message data needs to be accessible at a later time (when a user joins the channel), so it needs to be stored somewhere, like a datastore.
Within your datastores
folder, create a file named messages.ts
. Within it, define the datastore:
// /datastores/messages.ts
import { DefineDatastore, Schema } from "deno-slack-sdk/mod.ts";
/**
* Datastores are a Slack-hosted location to store
* and retrieve data for your app.
* https://api.slack.com/automation/datastores
*/
export const WelcomeMessageDatastore = DefineDatastore({
name: "messages",
primary_key: "id",
attributes: {
id: {
type: Schema.types.string,
},
channel: {
type: Schema.slack.types.channel_id,
},
message: {
type: Schema.types.string,
},
author: {
type: Schema.slack.types.user_id,
},
},
});
Each attribute
is a type of information you want to store. In this case, it's the information from the form submission. Next, you'll fill the datastore with that information.
You've already used Slack functions, but you can also create custom functions. Custom functions are unique building blocks of automation that you build to use in your workflows.
Within your functions
folder, create a file named create_welcome_message.ts
. This is where you'll define this custom function.
The custom function you'll add here will take the form input the user provided and store that information in the created datastore.
Add the function definition to the create_welcome_message.ts
file:
// /functions/create_welcome_message.ts
import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";
import { SlackAPIClient } from "deno-slack-sdk/types.ts";
import { SendWelcomeMessageWorkflow } from "../workflows/send_welcome_message.ts";
import { WelcomeMessageDatastore } from "../datastores/messages.ts";
/**
* This custom function will take the initial form input, store it
* in the datastore and create an event trigger to listen for
* user_joined_channel events in the specified channel.
*/
export const WelcomeMessageSetupFunction = DefineFunction({
callback_id: "welcome_message_setup_function",
title: "Welcome Message Setup",
description: "Takes a welcome message and stores it in the datastore",
source_file: "functions/create_welcome_message.ts",
input_parameters: {
properties: {
message: {
type: Schema.types.string,
description: "The welcome message",
},
channel: {
type: Schema.slack.types.channel_id,
description: "Channel to post in",
},
author: {
type: Schema.slack.types.user_id,
description:
"The user ID of the person who created the welcome message",
},
},
required: ["message", "channel"],
},
});
This function provides three properties
as input_parameters
. These are the three pieces of information you want to pass to the datastore: the welcome message, the channel to post in, and the user ID of the person who created the message.
The actual functionality involves taking those input parameters and putting them into a datastore. Put this right below your function definition within create_welcome_message.ts
:
// /functions/create_welcome_message.ts
export default SlackFunction(
WelcomeMessageSetupFunction,
async ({ inputs, client }) => {
const { channel, message, author } = inputs;
const uuid = crypto.randomUUID();
// Save information about the welcome message to the datastore
const putResponse = await client.apps.datastore.put<
typeof WelcomeMessageDatastore.definition
>({
datastore: WelcomeMessageDatastore.name,
item: { id: uuid, channel, message, author },
});
if (!putResponse.ok) {
return { error: `Failed to save welcome message: ${putResponse.error}` };
}
// Search for any existing triggers for the welcome workflow
const triggers = await findUserJoinedChannelTrigger(client, channel);
if (triggers.error) {
return { error: `Failed to lookup existing triggers: ${triggers.error}` };
}
// Create a new user_joined_channel trigger if none exist
if (!triggers.exists) {
const newTrigger = await saveUserJoinedChannelTrigger(client, channel);
if (!newTrigger.ok) {
return {
error: `Failed to create welcome trigger: ${newTrigger.error}`,
};
}
}
return { outputs: {} };
},
);
Add the custom function you created as a step in the workflow. This connection allows you to use inputs and outputs from previous steps, which is how you'll get the specific pieces of information.
Pivot back to your create_welcome_message.ts
workflow file. Add the following step:
// /workflows/create_welcome_message.ts
/**
* This step takes the form output and passes it along to a custom
* function which sets the welcome message up.
* See `/functions/setup_function.ts` for more information.
*/
MessageSetupWorkflow.addStep(WelcomeMessageSetupFunction, {
message: SetupWorkflowForm.outputs.fields.messageInput,
channel: SetupWorkflowForm.outputs.fields.channel,
author: MessageSetupWorkflow.inputs.interactivity.interactor.id,
});
export default MessageSetupWorkflow;
Now you've created a workflow that will:
Triggers are what activate workflows.
You need to create a trigger that will start the workflow, which provides a user the form to fill out.
This app will use a specific type of trigger called a link trigger. Link triggers kick off workflows when a user clicks on their link.
Within your triggers folder, create a file named create_welcome_message_shortcut.ts
. Place this trigger definition within that file:
// triggers/create_welcome_message_shortcut.ts
import { Trigger } from "deno-slack-api/types.ts";
import MessageSetupWorkflow from "../workflows/create_welcome_message.ts";
import { TriggerContextData, TriggerTypes } from "deno-slack-api/mod.ts";
/**
* This link trigger prompts the MessageSetupWorkflow workflow.
*/
const welcomeMessageTrigger: Trigger<typeof MessageSetupWorkflow.definition> = {
type: TriggerTypes.Shortcut,
name: "Setup a Welcome Message",
description: "Creates an automated welcome message for a given channel.",
workflow: `#/workflows/${MessageSetupWorkflow.definition.callback_id}`,
inputs: {
interactivity: {
value: TriggerContextData.Shortcut.interactivity,
},
channel: {
value: TriggerContextData.Shortcut.channel_id,
},
},
};
export default welcomeMessageTrigger;
This defines a trigger that will kick off the provided workflow, message_setup_workflow
, along with an added bonus: it'll pass along the channel ID of the channel it was started in.
The workflow to send a message to a user needs to be invoked after the message is created in the workflow. It also needs to be invoked whenever a new user joins the channel.
This calls for using a different type of trigger: an event trigger. Event triggers are only invoked when a certain event happens. In this case, our event is user_joined_channel
.
Think of your setup
function as priming everything needed for that message to send. The final piece to set up is this trigger.
Since it runs at a certain point in a workflow, you'll actually place it within a function file. Place it within the /functions/create_welcome_message.ts
file:
// /functions/create_welcome_message.ts
/**
* findUserJoinedChannelTrigger returns if the user_joined_channel trigger
* exists for the "Send Welcome Message" workflow in a channel.
*/
export async function findUserJoinedChannelTrigger(
client: SlackAPIClient,
channel: string,
): Promise<{ error?: string; exists?: boolean }> {
// Collect all existing triggers created by the app
const allTriggers = await client.workflows.triggers.list({ is_owner: true });
if (!allTriggers.ok) {
return { error: allTriggers.error };
}
// Find user_joined_channel triggers for the "Send Welcome Message"
// workflow in the specified channel
const joinedTriggers = allTriggers.triggers.filter((trigger) => (
trigger.workflow.callback_id ===
SendWelcomeMessageWorkflow.definition.callback_id &&
trigger.event_type === "slack#/events/user_joined_channel" &&
trigger.channel_ids.includes(channel)
));
// Return if any matching triggers were found
const exists = joinedTriggers.length > 0;
return { exists };
}
/**
* saveUserJoinedChannelTrigger creates a new user_joined_channel trigger
* for the "Send Welcome Message" workflow in a channel.
*/
export async function saveUserJoinedChannelTrigger(
client: SlackAPIClient,
channel: string,
): Promise<{ ok: boolean; error?: string }> {
const triggerResponse = await client.workflows.triggers.create<
typeof SendWelcomeMessageWorkflow.definition
>({
type: "event",
name: "User joined channel",
description: "Send a message when a user joins the channel",
workflow:
`#/workflows/${SendWelcomeMessageWorkflow.definition.callback_id}`,
event: {
event_type: "slack#/events/user_joined_channel",
channel_ids: [channel],
},
inputs: {
channel: { value: channel },
triggered_user: { value: "{{data.user_id}}" },
},
});
if (!triggerResponse.ok) {
return { ok: false, error: triggerResponse.error };
}
return { ok: true };
}
This trigger passes the event-related channel
and triggered_user
values on to your soon-to-be workflow. With those accessible, you can now build out your next workflow.
This second workflow will retrieve the message from the datastore and send it to the channel when a new user joins that channel.
Navigate back to your workflows
folder, and create a new file send_welcome_message.ts
.
Within that file place the workflow definition:
// /workflows/send_welcome_message.ts
import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";
import { SendWelcomeMessageFunction } from "../functions/send_welcome_message.ts";
/**
* The SendWelcomeMessageWorkFlow will retrieve the welcome message
* from the datastore and send it to the specified channel, when
* a new user joins the channel.
*/
export const SendWelcomeMessageWorkflow = DefineWorkflow({
callback_id: "send_welcome_message",
title: "Send Welcome Message",
description:
"Posts an ephemeral welcome message when a new user joins a channel.",
input_parameters: {
properties: {
channel: {
type: Schema.slack.types.channel_id,
},
triggered_user: {
type: Schema.slack.types.user_id,
},
},
required: ["channel", "triggered_user"],
},
});
This workflow will have two inputs: channel
and triggered_user
, both acquired from the trigger invocation.
Navigate to the functions
folder, and create a new file called send_welcome_message.ts
.
Within that file add the definition for a function that uses the inputs channel
and triggered_user
:
// /functions/send_welcome_message.ts
import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";
import { WelcomeMessageDatastore } from "../datastores/messages.ts";
/**
* This custom function will pull the stored message from the datastore
* and send it to the joining user as an ephemeral message in the
* specified channel.
*/
export const SendWelcomeMessageFunction = DefineFunction({
callback_id: "send_welcome_message_function",
title: "Sending the Welcome Message",
description: "Pull the welcome messages and sends it to the new user",
source_file: "functions/send_welcome_message.ts",
input_parameters: {
properties: {
channel: {
type: Schema.slack.types.channel_id,
description: "Channel where the event was triggered",
},
triggered_user: {
type: Schema.slack.types.user_id,
description: "User that triggered the event",
},
},
required: ["channel", "triggered_user"],
},
});
With the function defined, add the actual functionality right after:
// /functions/send_welcome_message.ts
export default SlackFunction(SendWelcomeMessageFunction, async (
{ inputs, client },
) => {
// Querying datastore for stored messages
const messages = await client.apps.datastore.query<
typeof WelcomeMessageDatastore.definition
>({
datastore: WelcomeMessageDatastore.name,
expression: "#channel = :mychannel",
expression_attributes: { "#channel": "channel" },
expression_values: { ":mychannel": inputs.channel },
});
if (!messages.ok) {
return { error: `Failed to gather welcome messages: ${messages.error}` };
}
// Send the stored messages ephemerally
for (const item of messages["items"]) {
const message = await client.chat.postEphemeral({
channel: item["channel"],
text: item["message"],
user: inputs.triggered_user,
});
if (!message.ok) {
return { error: `Failed to send welcome message: ${message.error}` };
}
}
return {
outputs: {},
};
});
This creates a function that:
message
item from the datastore with a matching channel
channel ID value to the user with the triggered_user
user ID.With the custom function built, add it to your send_welcome_message.ts
workflow as a step:
// /workflows/send_welcome_message.ts
SendWelcomeMessageWorkflow.addStep(SendWelcomeMessageFunction, {
channel: SendWelcomeMessageWorkflow.inputs.channel,
triggered_user: SendWelcomeMessageWorkflow.inputs.triggered_user,
});
And with that, you have created the two workflows that contain all the functionality you need to send a custom ephemeral message to a user joining a new channel.
Leaving you off with the app built but not running would be a little anticlimactic, after all.
For now, you'll want to locally install the app to the workspace. From the command line, within your app's root folder, run the following command:
slack run
Proceed through the prompts until you have a local server running in that terminal instance.
It's installed! You can't use it quite yet though.
Within a terminal located within that folder, you'll need to create that initial link trigger. You can open a new terminal tab or cancel your running server and restart later if you'd like.
You can do that with the slack trigger create
command. Make it so.
slack trigger create --trigger-def triggers/create_welcome_message_shortcut.ts
Since you haven't installed this trigger to a workspace yet, you'll be prompted to install the trigger to a new workspace. Then select an authorized workspace in which to install the app.
When you select your workspace, you will be prompted to choose an app environment for the trigger. Choose the Local option so you can interact with your app while developing locally. The CLI will then finish installing your trigger.
Once your app's trigger is finished being installed, you will see the following output:
📚 App Manifest
Created app manifest for "welcomebot (local)" in "myworkspace" workspace
⚠️ Outgoing domains
No allowed outgoing domains are configured
If your function makes network requests, you will need to allow the outgoing domains
Learn more about upcoming changes to outgoing domains: https://api.slack.com/changelog
🏠 Workspace Install
Installed "welcomebot (local)" app to "myworkspace" workspace
Finished in 1.5s
⚡ Trigger created
Trigger ID: Ft0123ABC456
Trigger Type: shortcut
Trigger Name: Setup a Welcome Message
Shortcut URL:
https://slack.com/shortcuts/Ft0123ABC456/XYZ123
...
Copy the URL, paste, and post it in a channel to kick off the first workflow and create a message.
When you're ready to make the app accessible to others, you'll want to deploy it instead of running it:
slack deploy
And then create the trigger again, but choosing the Deployed option this time:
slack trigger create --trigger-def triggers/create_welcome_message_shortcut.ts
Other than that, the steps are the same.
We hope you enjoyed your experience building a Slack automation!
Congratulations! You've successfully built your friendly neighborhood welcome bot, providing a cozy presence to all who enter your desired channel.
For your next challenge, perhaps consider creating an app that creates an issue in GitHub!