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!
Take a captain's view of the expedition that is building a Slack app.
How much guidance would you like on this journey?
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
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
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.
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!
Sequence the steps behind sending an individualized kudo message.
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.
Our task of sharing kudos can be accomplished with three actions:
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(
Schema.slack.functions.OpenForm,
{
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 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,
message:
`*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` +
`${gif.outputs.URL}`,
});
...
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.
Add custom code as a step within a workflow, expanding the horizons of our expedition.
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"],
},
});
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 };
});
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": "https://media2.giphy.com/media/3oEjHWXddcCOGZNmFO/giphy.gif",
"alt_text": "A person wearing a banana hat says thanks a bunch",
"tags": ["thankful"]
},
{
"URL": "https://media.giphy.com/media/3o6Zt2jsgmiAUl2ydi/giphy.gif",
"alt_text": "An impressed sponge-like creature expresses amazement",
"tags": ["amazed"]
},
{
"URL": "https://media.giphy.com/media/WKdPOVCG5LPaM/giphy.gif",
"alt_text": "A cheerful high-five from the newsroom",
"tags": ["celebration", "excited"]
},
{
"URL": "https://media1.giphy.com/media/Lcn0yF1RcLANG/giphy-downsized.gif",
"alt_text": "Wow! A feeling of wild disbelief overwhelms the senses",
"tags": ["amazed"]
},
{
"URL": "https://media0.giphy.com/media/rgIdiNjWC933y/giphy.gif",
"alt_text": "A kingly racoon nodding over many subjects",
"tags": ["excited", "amazed"]
},
{
"URL": "https://media0.giphy.com/media/kyLYXonQYYfwYDIeZl/giphy.gif",
"alt_text": "Elmo dances in celebration",
"tags": ["celebration"]
},
{
"URL": "https://media2.giphy.com/media/3ohs7NuHL3gjbe2uGI/giphy-downsized.gif",
"alt_text": "You're noticed and appreciated <3",
"tags": ["appreciation"]
},
{
"URL": "https://media1.giphy.com/media/xUA7aOIFDR4ZgqLy8w/giphy.gif",
"alt_text": "Fern having a messy hair day",
"tags": ["plants"]
},
"URL": "https://media1.giphy.com/media/MbAlP79yMRysHKUyHV/giphy-downsized.gif",
"alt_text": "Sleepy otter rubs checks and yawns",
"tags": ["otter"]
}
]
Create an entry point into the workflow with triggers for a convenient way to share kudos.
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.
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.
After sailing the seas of a Slack app, our journey now comes to an end.
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!