Create a bot to welcome users

Intermediate

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.

Building this app, aptly called the Welcome Bot, will instill in you the knowledge of creating, storing and sending automated messages. Let's begin!

If you'd like to just view the completed app, visit its GitHub repository.

Step 1Create a Slack app with the CLI

You need the proper equipment before you can start building your app.

  • Install the CLI

    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 blank project

    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!

  • Alternative: create an app from the template

    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
    
Step complete!

Step 2Plan out your app with an app manifest

An app manifest is where you define the key attributes of your Slack app.

  • Create the app manifest

    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:

    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.

Step complete!

Step 3Create a workflow for setting up the welcome message

Workflows are a sequenced set of steps, interactions and functions chained together.

  • Define the workflow

    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.

  • Add a form for the user to specify the welcome message

    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:

    • "Your welcome message", where the user provides the message as a string of text
    • "Select a channel to post this message in", where the user provides the channel for the desired channel.

    The user can then submit the form.

  • Add a confirmation ephemeral message when submitting 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.

Step complete!

Step 4Use a datastore to store the welcome message

Datastores provide a place for your app to store and retrieve data.

  • Create the datastore

    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.

Step complete!

Step 5Create a custom function to send the message to the datastore

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.

  • Define the custom function

    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.

  • Add the custom function's functionality

    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 to the workflow

    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:

    • let a user fill out a form with information for a welcome message
    • store the welcome message information in a datastore
Step complete!

Step 6Create triggers

Triggers are what activate workflows.

  • Create the link trigger

    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.

  • Create the event trigger to trigger a second workflow

    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.

Step complete!

Step 7Create a workflow for sending the welcome message

  • Define the 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.

Step complete!

Step 8Create a custom function that sends the welcome message

  • Define the custom function

    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"],
      },
    });
    
  • Add the custom function's functionality

    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:

    • queries the datastore for stored messages
    • posts an ephemeral message using the message item from the datastore with a matching channel channel ID value to the user with the triggered_user user ID.
  • Add the custom function to the workflow

    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.

Step complete!

Step 9Use your Slack app

Leaving you off with the app built but not running would be a little anticlimactic, after all.

  • Run your Slack app

    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.

  • Invoke the link trigger

    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.

  • Deploy your Slack app

    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.

Step complete!

Step 10Pause and reflect

We hope you enjoyed your experience building a modular Slack app. Regardless, we want to know your thoughts and feelings.

Step complete!

Was this page helpful?