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.
Building this app, aptly called the Welcome Bot, will instill in you the knowledge of creating, storing and sending automated messages. Let's begin!
You need the proper equipment before you can start building your app.
The app you'll be creating is a modular Slack app, built using the Slack CLI. Make sure to install the latest version of the Slack CLI before proceeding.
Create a new app with the Slack CLI using the following command:
slack create welcomebot
A new app folder will be created. Shift into that folder in whichever editor you prefer. You'll be bouncing between a few folders so we recommend using one 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, you can scaffold your new app using the GitHub repository as a template:
slack create --template https://github.com/slack-samples/deno-welcome-bot
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:
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 and easy 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:
import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";
import { WelcomeMessageSetupFunction } from "../functions/create_welcome_message.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 built-in function. Built-in functions give you the ability to add common Slack functionality without the need to do so from scratch.
The built-in function to use here is the OpenForm
function. Add it to your create_welcome_message.ts
workflow like so:
export 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 built-in SendEphemeralMessage
function. Add the following step to your create_welcome_message.ts
workflow:
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:`,
});
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:
import { DefineDatastore, Schema } from "deno-slack-sdk/mod.ts";
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 built-in 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:
import { SlackAPIClient } from "deno-slack-api/types.ts";
import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";
import { SendWelcomeMessageWorkflow } from "../workflows/send_welcome_message.ts";
import { WelcomeMessageDatastore } from "../datastores/messages.ts";
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: ["welcome_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
:
export default SlackFunction(
WelcomeMessageSetupFunction,
async ({ inputs, client }) => {
const uuid = crypto.randomUUID();
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:
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:
import { Trigger } from "deno-slack-api/types.ts";
import MessageSetupWorkflow from "../workflows/create_welcome_message.ts";
const welcomeMessageTrigger: Trigger<typeof MessageSetupWorkflow.definition> = {
type: "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: "{{data.interactivity}}",
},
channel: {
value: "{{data.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 triggered 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. Within the /functions/create_welcome_message.ts
file, locate the TODO
that we created earlier and place the trigger definition within the triggers.create
method:
const triggerResponse = await client.workflows.triggers.create({
type: "event",
name: "Member joined response",
description: "Triggers when member joined",
workflow: "#/workflows/send_welcome_message",
event: {
event_type: "slack#/events/user_joined_channel",
channel_ids: [inputs.channel],
},
inputs: {
channel: { value: "{{data.channel_id}}" },
triggered_user: { value: "{{data.user_id}}" },
},
});
if (!triggerResponse.ok) {
const error = `Failed to create a trigger - ${triggerResponse.error}`;
return { error };
}
return { outputs: {} };
/**
* 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:
import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";
import { SendWelcomeMessageFunction } from "../functions/send_welcome_message.ts";
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
:
import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";
import { WelcomeMessageDatastore } from "../datastores/messages.ts";
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:
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:
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/future/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.
Congratulations! You've successfully built your friendly neighborhood welcome bot, providing a cozy presence to all who enter your desired channel.