Create an app to give kudos


Ahoy matey! Welcome aboard the Give Kudos App Tutorial!

We're setting sail on our maiden voyage – creating and deploying a new Slack app – and would love to show you the ropes. By the end of the expedition, we'll have an app to parrot personal "kudos" throughout a workspace. Nothing is more important than a crew's morale, after all!

A curiosity of the waters is all you need to sail the open seas, so climb aboard to discover what it takes to be captain of your own ship – that is, developing your own Slack app!

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

Step 1Charting the seas

Take a captain's view of the expedition that is building a Slack app.

  • Pick a path

    How much guidance would you like on this journey?

    Option 1: create an app using the template

    If you'd like to follow along without steering the ship, use the pre-built Give Kudos app as a template.

    slack create give-kudos-app -t slack-samples/deno-give-kudos
    Option 2: create a blank app

    If you're ready to take the helm yourself, create a Slack app from scratch. Don't worry, we'll be right beside you.

    slack create give-kudos-app
  • Comprehending the manifest

    Before setting sail, we'll need to take inventory of our ship's cargo. An inspection of the manifest.ts will reveal a collection of workflows, functions, and other app-related attributes.

    // manifest.ts
    import { Manifest } from "deno-slack-sdk/mod.ts";
    import { FindGIFFunction } from "./functions/find_gif.ts";
    import { GiveKudosWorkflow } from "./workflows/give_kudos.ts";
    export default Manifest({
      name: "Kudo",
      description: "Brighten someone's day with a heartfelt thank you",
      icon: "assets/default_new_app_icon.png",
      functions: [FindGIFFunction],
      workflows: [GiveKudosWorkflow],
      outgoingDomains: [],
      botScopes: ["commands", "chat:write", "chat:write.public"],

    The details of this expedition’s manifest are soon to follow, but first let's shine a light through the fog and get a bearing on the three fundamentals of a Slack app: workflows, functions, and triggers.

  • Defining the building blocks of an app

    A workflow is a collection of processes, executed in response to certain events. In nautical terms, repairing the hull is a common workflow that happens when yours truly runs the ship aground.

    The processes that make up a workflow are known as functions. Functions can be built-in, everyday actions such as sending a message or opening a form, or made custom, defined by your own logic with various inputs and outputs.

    On the sea, raising the sails and scrubbing the decks are oft run functions.

    Triggers act as inspiration for the actions of a workflow, defining when a workflow is invoked. Certain events in Slack (such as a clicked link or reaction added) or a schedule of specified time intervals can serve as triggers for a workflow.

    After taking in the view from the mast, we’re ready to set sail!

Step complete!

Step 2Weaving a workflow

Sequence the steps behind sending an individualized kudo message.

  • Planning a workflow

    The goal of our expedition is to build an app that can perform the task of parroting a personalized message. To do so, a workflow called "Give kudos" is created in workflows/give_kudos.ts. This workflow will contain all the actions our app needs to complete that task.

    // workflows/give_kudos.ts
    import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";
    const GiveKudosWorkflow = DefineWorkflow({
      callback_id: "give_kudos_workflow",
      title: "Give kudos",
      description: "Acknowledge the impact someone had on you",
      input_parameters: {
        properties: {
          interactivity: {
            type: Schema.slack.types.interactivity,
        required: ["interactivity"],
    // your steps go here
    export { GiveKudosWorkflow };

    This workflow doesn't do anything - yet. For now, just note that it'll contain steps that require user interactivity.

  • Sketching the steps

    Our task of sharing kudos can be accomplished with three actions:

    • collecting information about a message,
    • finding the right GIF,
    • then sharing the love in the form of a message (bottle not included).

    Let's look at how these actions are added as steps to a workflow. Each step is composed of a function definition as well as the input object. The input object allows the outputs of one step to become inputs to another in a chain of functions.

    The first step is composed of a built-in function, OpenForm, that will, as hinted, open a form for the user. This lets our app collect the kudos users want to give.

    // workflows/give_kudos.ts
    /* Step 1. Collect message information */
    const kudo = GiveKudosWorkflow.addStep(
        title: "Give someone kudos",
        interactivity: GiveKudosWorkflow.inputs.interactivity,
        submit_label: "Share",
        description: "Continue the positive energy through your written word",
        fields: {
          elements: [{
            name: "doer_of_good_deeds",
            title: "Whose deeds are deemed worthy of a kudo?",
            description: "Recognizing such deeds is dazzlingly desirable of you!",
            type: Schema.slack.types.user_id,
          }, {
            name: "kudo_channel",
            title: "Where should this message be shared?",
            type: Schema.slack.types.channel_id,
          }, {
            name: "kudo_message",
            title: "What would you like to say?",
            type: Schema.types.string,
            long: true,
          }, {
            name: "kudo_vibe",
            title: 'What is this kudo\'s "vibe"?',
            description: "What sorts of energy is given off?",
            type: Schema.types.string,
            enum: [
              "Appreciation for someone πŸ«‚",
              "Celebrating a victory πŸ†",
              "Thankful for great teamwork ⚽️",
              "Amazed at awesome work β˜„οΈ",
              "Excited for the future πŸŽ‰",
              "No vibes, just plants πŸͺ΄",
          required: ["doer_of_good_deeds", "kudo_channel", "kudo_message"],

    The form will look like this for the user wanting to give kudos.

    The form within slack

    The second step is composed of a custom function, FindGIFFunction. This is a function built specifically for this app. It'll find a GIF related to the "vibe" someone gives a message.

    // workflows/give_kudos.ts
    import { FindGIFFunction } from "../functions/find_gif.ts";
    /* Step 2. Find the right GIF */
    const gif = GiveKudosWorkflow.addStep(FindGIFFunction, {
      vibe: kudo.outputs.fields.kudo_vibe,

    Notice how the OpenForm step contains the interactivity context of the workflow and FindGIFFunction uses kudo_vibe, an output parameter of the OpenForm step.

    The third step is composed of a built-in function, SendMessage, that will send a message to a specific channel. It'll combine the user's words and the chosen GIF into one spectacular message fit for even the most seasoned sailors. That message will be sent to the channel the user specified within the form from step one.

    // workflows/give_kudos.ts
    /* Step 3. Share the love */
    GiveKudosWorkflow.addStep(Schema.slack.functions.SendMessage, {
      channel_id: kudo.outputs.fields.kudo_channel,
        `*Hey <@${kudo.outputs.fields.doer_of_good_deeds}>!* Someone wanted to share some kind words with you :otter:\n` +
        `> ${kudo.outputs.fields.kudo_message}\n` +

    And that's all you need to do for built-in functions! FindGIFFunction is a custom function, however. We'll have to roll up our sleeves and build that out ourselves.

Step complete!

Step 3Customizing a function

Add custom code as a step within a workflow, expanding the horizons of our expedition.

  • Defining the custom function

    The FindGIFFunction function, unique to our app, is defined in functions/find_gif.ts, where inputs, outputs, and other attributes are described. This definition allows FindGIFFunction to be used within workflows, as when sharing a kudo!

    // functions/find_gif.ts
    import { DefineFunction, Schema } from "deno-slack-sdk/mod.ts";
    export const FindGIFFunction = DefineFunction({
      callback_id: "find_gif",
      title: "Find a GIF",
      description: "Search for a GIF that matches the vibe",
      source_file: "functions/find_gif.ts",
      input_parameters: {
        properties: {
          vibe: {
            type: Schema.types.string,
            description: "The energy for the GIF to match",
        required: ["vibe"],
      output_parameters: {
        properties: {
          URL: {
            type: Schema.types.string,
            description: "GIF URL",
          alt_text: {
            type: Schema.types.string,
            description: "description of the GIF",
        required: ["URL"],
  • Finding a GIF

    With the FindGIFFunction function defined, it can then be implemented, where input from the form is parsed and converted into a random GIF that's obtained from the GIF catalog.

    // functions/find_gif.ts
    import { SlackFunction } from "deno-slack-sdk/mod.ts";
    import gifs from "../assets/gifs.json" assert { type: "json" };
    const getEnergy = (vibe: string): string => {
      if (vibe === "Appreciation for someone πŸ«‚") return "appreciation";
      if (vibe === "Celebrating a victory πŸ†") return "celebration";
      if (vibe === "Thankful for great teamwork ⚽️") return "thankful";
      if (vibe === "Amazed at awesome work β˜„οΈ") return "amazed";
      if (vibe === "Excited for the future πŸŽ‰") return "excited";
      if (vibe === "No vibes, just plants πŸͺ΄") return "plants";
      return "otter"; // 🦦
    interface GIF {
      URL: string;
      alt_text?: string;
      tags: string[];
    const matchVibe = (vibe: string): GIF => {
      const energy = getEnergy(vibe);
      const matches = gifs.filter((g: GIF) => g.tags.includes(energy));
      const randomGIF = Math.floor(Math.random() * matches.length);
      return matches[randomGIF];
    export default SlackFunction(FindGIFFunction, ({ inputs }) => {
      const { vibe } = inputs;
      const gif = matchVibe(vibe);
      return { outputs: gif };
  • A collection of GIFs

    The GIF catalog – the treasure of this expedition – is listed in assets/gifs.json, but could safely be stored in a datastore. Think of datastores as your very own treasure chests.

    For readability, we've only included a portion of the GIFs in the snippet below. If you want the full treasure trove, grab it from the GitHub repo.

    // assets/gifs.json
        "URL": "",
        "alt_text": "A person wearing a banana hat says thanks a bunch",
        "tags": ["thankful"]
        "URL": "",
        "alt_text": "An impressed sponge-like creature expresses amazement",
        "tags": ["amazed"]
        "URL": "",
        "alt_text": "A cheerful high-five from the newsroom",
        "tags": ["celebration", "excited"]
        "URL": "",
        "alt_text": "Wow! A feeling of wild disbelief overwhelms the senses",
        "tags": ["amazed"]
        "URL": "",
        "alt_text": "A kingly racoon nodding over many subjects",
        "tags": ["excited", "amazed"]
        "URL": "",
        "alt_text": "Elmo dances in celebration",
        "tags": ["celebration"]
      "URL": "",
      "alt_text": "You're noticed and appreciated <3",
      "tags": ["appreciation"]
      "URL": "",
      "alt_text": "Fern having a messy hair day",
      "tags": ["plants"]
      "URL": "",
      "alt_text": "Sleepy otter rubs checks and yawns",
      "tags": ["otter"]
Step complete!

Step 4Invoking with a trigger

Create an entry point into the workflow with triggers for a convenient way to share kudos.

  • Create a link trigger

    The workflows and functions discussed above cannot be invoked until a trigger is created, similar to how a sailor needs an order from their captain to carry out tasks.

    To create an entry point into our app's "Give kudos" workflow, a link trigger is defined in triggers/give_kudos.ts.

    // triggers/give_kudos.ts
    import { Trigger } from "deno-slack-api/types.ts";
    const trigger: Trigger = {
      type: "shortcut",
      name: "Give some kudos",
      description: "Broadcast your appreciation with kind words and a GIF",
      workflow: "#/workflows/give_kudos_workflow",
      inputs: {
        interactivity: {
          value: "{{data.interactivity}}",
    export default trigger;

    The trigger is then created using the Slack CLI with the slack trigger create command, generating a Shortcut URL that can be clicked from a channel:

    slack trigger create --trigger-def triggers/give_kudos.ts

    You'll be prompted to install your app to a workspace. Select your desired workspace, and then select a Local app environment. When you want to deploy your app later on, you would repeat this step and select a Deployed app environment.

  • Run your app

    With the Shortcut URL in hand, our destination is near. Run the app in development mode to test it out on your local machine.

    slack run

    Use the Shortcut URL within Slack to kick off the workflow. There's no X, but rest assured you've found the ultimate treasure - the experience of building your very own Give Kudos app.

    The kudos app congratulating you!

Step complete!

Step 5Ending our expedition

After sailing the seas of a Slack app, our journey now comes to an end.

  • What an adventure!

    Returning back to harbor, we can reminisce about the journey of sharing a kudo; recalling that functions compose workflows and workflows are invoked by triggers.

    With these ropes, you're ready to take the seas yourself! There are many more knots to learn while on these waters, but you'll stay afloat just fine with what you now know!

    Bon voyage, captain!

Step complete!

Was this page helpful?