I built a Slack bot with a coworker for our internal company hackathon. It wasā¦.an experience! Here are some notes that might be useful to other folks.
Our team runs a company-public āhelpā channel, which is meant for folks outside of our team to ask questions. Unsurprisingly, many of these questions are repetitive, and answering them is a drain on the on-call engineerās time. I looked back at the last month of questions and found that ~35% of them were good candidates for an automated response. So, I wanted to build a bot that would automatically reply to certain keywords in a specified channel with a canned response.
The first hiccup was: how do we interact with Slack? Slack has a lot of different ways to build automations: there are SDKs for standalone apps that interact with Slack via webhooks, thereās the āWorkflow Builderā GUI, there are āworkflowsā, there are āSlack appsā, thereās Slackbotā¦.
In order to respond to the message_posted event trigger, you need to build a Workflow (not to be confused with the Workflow Builder). Sometimes the docs refer to this as a āWorkflow App.ā
A Workflow is basically a special app that can be deployed to Slack. You canāt put it in the Slack apps directory. It must be written in Typescript, and you donāt run it as a standalone program.
The entire Workflows developer experience has the vibe of āsome folks put a lot of work into this, a while ago.ā There are a lot of really cool use-cases for Slack automations, but there are also a bunch of gotchas and rough edges to the API. Hereās some of my notes from working with the Workflows API:
The sample apps are your friend
Thereās a robust repo of example projects that are the best point of reference for using the API. While the docs are also useful, I found that the Slack sample apps were the best reference to use.
Nested arrays and objects donāt work.
We ran into all sorts of bugs when trying to store arrays inside of objects or objects inside of arrays, both when interacting with the datastores but also when just trying to pass parameters between functions.
Thereās an open bug from 2023 where storing an object containing an array doesnāt work. Hopefully that will get fixed eventually?
No conditionals or early termination
Workflows consist of a series of steps. Thereās no way to build in conditional branching logic into a Workflow despite customer demand. Conditionals in Workflow Builder were announced as an upcoming feature back in 2022, but thereās been no news about them since.
Thereās also no way to end a workflow early. This becomes a problem if a necessary precondition is not met, because of how errors are handled.
Error handling
If your workflow encounters an error, Slackbot will DM the workflowās admin. There is no way to mute these messages.
Because of this, itās really important that you wrap each step in a try/catch if thereās a chance that it will error.
Call the Slack client from a custom step, NOT from a builtin step.
I found that the Slack builtin methods for doing things will throw errors on normal user interactions (e.g. closing a modal window) which will then trigger a DM to the workflow admin. Yikes!
The docs generally advise you to use one of the builtin Slack functions from Schema.slack.functions
for interacting with the Slack API, but you can instead use the client
param which is available in custom functions to call the Slack API.
For example, the docs might suggest doing this:
MyWorklflow.addStep(
Schema.slack.functions.SendMessage,
{
channel_id: inputs.channel_id,
message: `Setting topic to ${generateMessageStep.outputs.message}`,
},
);
But instead, we did this from a custom Workflow step:
let post_args: {
"channel": string;
"text": string;
} = {
channel: inputs.channel_id,
text: inputs.message,
};
await client.chat.postMessage(post_args);
Similarly, I really donāt recommend using the Schema.slack.functions.OpenForm
Slack function in your workflows, because it will error if the user closes a modal prematurely.
Instead you can do something like:
export default SlackFunction(
OpenModal,
async ({ inputs, client }) => {
// Open a new modal with the end-user who interacted with the link trigger
const view: ModalView = {
"type": "modal",
// Note that this ID can be used for dispatching view submissions and view closed events.
"callback_id": "first-page",
// This option is required to be notified when this modal is closed by the user
"notify_on_close": true,
"title": {
"type": "plain_text",
"text": MODAL_TITLE,
},
"close": { "type": "plain_text", "text": "Close" },
"blocks": modalBlocksWithRows(inputs.entries),
};
const response = await client.views.open({
interactivity_pointer: inputs.interactivity.interactivity_pointer,
view: view,
});
}
);
Not only does calling a custom function let you do bespoke error handling, we also encountered problems where the Slack built-in functions would inexplicably error but our custom functions worked fine. Unclear what was going on there.
Mismatches between CLI help messages and behavior
There are a few places where the CLI help messages provide commands that donāt actually work.
For example, when trying to count the number of entries in a datastore:
$ slack datastore count --help
...
# Count all items in a datastore
$ slack datastore count --datastore tasks
If you try to actually do this, it will error.
$ slack datastore count --datastore response_store
Check .slack/logs/slack-debug-20240514.log for full error logs
š« No expression was provided (invalid_datastore_expression)
Suggestion:
Provide a JSON expression in the command arguments
Use slack datastore count --help for an example
Build a new expression using slack datastore count --show --unstable
You canāt delete a Slack workflowās datastore.
Slack can create a datastore for you, and the CLI provides a nice interface for creating datastores, and adding or removing data. However there is no support for deleting an entire datastore.
To remove a datastore, you must remove the datastore from your manifest and force-deploy, then add the datastore back in and re-deploy.
JSONL bulk put doesnāt work?
Maybe thereās a way to do this that I havenāt been able to figure out. The docs claim that bulk-put is supported for datastores but I wasnāt able to make this work.
The docs suggest running this command like so:
slack datastore bulk-put '{"datastore": "running_datastore"}' ā-from-file /path/to/file.jsonl
The --from-file
flag is also documented via the help command in CLI:
$ slack datastore bulk-put --help
Create or replace a list of items in a datastore
USAGE
$ slack datastore bulk-put <expression> [flags]
FLAGS
--datastore string the datastore used to store items
--from-file string store multiple items from a file of JSON Lines
But, it doesnāt work.
$ slack datastore bulk-put --datastore '{"datastore": "response_store"}' --from-file assets/test.jsonl
Check .slack/logs/slack-debug-20240514.log for full error logs
š« No expression was provided (invalid_datastore_expression)
Suggestion:
Provide a JSON expression in the command arguments
Use slack datastore bulk-put --help for an example
Build a new expression using slack datastore bulk-put --show --unstable
Well, that sure was a hackathon.
My coworker and I did eventually get our Slack workflow up and running, and it was even kind of fun! But oof I had trouble figuring out how to get the API working. For being ājustā a chat app, Slack is pretty complicated, and unsurprisingly the API surface area is complicated too. Maybe my notes will help you (or, at least, help my future self when Iām debugging this six months from nowā¦).