Node Slack bot
A Slack standup bot posts team updates, explains its commands when mentioned, and reports how long its current connection has been open. Tokay keeps it connected as one Background Worker.
Last updated
This project fits a workplace bot that listens over Slack Socket Mode instead of exposing a public endpoint. Tokay runs the long lived process and keeps the connection logs close to its Secrets.
- Runtime
- Node.js
- Framework
- Slack Bolt
- Service types
- Background Worker
- Resources
- None
- Required secrets
SLACK_BOT_TOKEN,SLACK_APP_TOKEN
What it does
Mention the bot in a channel and it replies in a thread with a short guide to its two commands.
Enter /standup followed by an update and the bot posts it to the channel under your Slack identity. An empty update stays private and asks you to add one useful line.
Use /botuptime for a private response showing the age of the current Socket Mode connection.
Deploy it
Bring this folder to Tokay any way you like. Paste or upload it in the dashboard, push it with git, or hand it to your AI agent along with our agent instructions. Tokay figures out the rest.
Working in Claude, ChatGPT, VS Code, or another MCP client? Connect the Tokay MCP server and ask your agent to deploy this example. It signs in through your browser, so there is no token to paste.
New to Tokay? The getting started guide walks you through your first deploy.
Create the Slack app
Create and install the Slack app before deploying the bot. This usually takes about two minutes.
- Open Your Apps and choose Create New App.
- Choose From an app manifest, select your workspace, and paste this repository's
manifest.jsoninto the JSON editor. - Choose Next, review the permissions, then choose Create.
- Open Install App and choose Install to Workspace, then Allow.
- Copy the Bot User OAuth Token that begins with
xoxb. Keep it ready forSLACK_BOT_TOKENin the deployment steps below. - Open Basic Information. Under App-Level Tokens, choose Generate Token and Scopes.
- Name the token, add the
connections:writescope, and choose Generate. - Copy the token that begins with
xapp. Keep it ready forSLACK_APP_TOKENin the deployment steps below.
Keep both tokens out of this folder and out of chat.
Add the Slack tokens
Open Environment and find SLACK_BOT_TOKEN and SLACK_APP_TOKEN under From your code. Choose Set shared value for each one, paste the matching token, and save it.
When both rows say Good to go, follow the deployment tracker and choose Go Live if that action appears. The Background Worker has no public page. Logs will show when the bot is connected.
Try it
- Invite
@Standup Pocketto a test channel and mention it for help. - Enter
/standup Shipped the export flow and reviewing production logs next. - Run
/botuptime, restart the Worker, then run it again. The shorter age proves the new process reconnected.
What failure looks like
A missing token keeps the bot from going live. If a token is present but wrong, Logs shows a direct Slack connection error.
Tokay restarts the Background Worker and makes a repeated failure visible as an incident. Replace the invalid Project Secret and the bot will reconnect.
How it works
You can use the bot without a public URL because Bolt keeps one outbound WebSocket open to Slack. Mentions and slash commands arrive on that connection, and replies go back through Slack's API.
During a deployment, Tokay gives the old process time to close its connection before the replacement takes over. A failed login remains in Logs instead of looking like a silent bot.
Secrets
| Name | Required | Purpose |
|---|---|---|
SLACK_BOT_TOKEN |
Yes | Gives the installed bot its workspace permissions. |
SLACK_APP_TOKEN |
Yes | Opens Socket Mode with connections:write. |
Grow from here
Try the Discord bot when the conversation happens in a community server instead.
Source code
app.js Raw
const { App } = require("@slack/bolt");
const botToken = process.env.SLACK_BOT_TOKEN?.trim();
const appToken = process.env.SLACK_APP_TOKEN?.trim();
if (!botToken || !appToken) {
console.error("SLACK_BOT_TOKEN and SLACK_APP_TOKEN are required. Add both Project Secrets before Go Live.");
process.exit(1);
}
const startedAt = Date.now();
const app = new App({
token: botToken,
appToken,
socketMode: true,
});
function uptime() {
const seconds = Math.floor((Date.now() - startedAt) / 1000);
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return [days && `${days}d`, hours && `${hours}h`, `${minutes}m`].filter(Boolean).join(" ");
}
app.event("app_mention", async ({ event, say }) => {
await say({
thread_ts: event.ts,
text: "I can post a concise team update with `/standup your update` and show my connection age with `/botuptime`.",
});
});
app.command("/standup", async ({ command, ack, respond }) => {
await ack();
const update = command.text.trim();
if (!update) {
await respond({ response_type: "ephemeral", text: "Add one useful line after `/standup` and I will post it here." });
return;
}
await respond({
response_type: "in_channel",
text: `*Standup from <@${command.user_id}>*\n${update}`,
});
console.log(JSON.stringify({ event: "standup.posted", channel: command.channel_id, user: command.user_id }));
});
app.command("/botuptime", async ({ ack, respond }) => {
await ack();
await respond({ response_type: "ephemeral", text: `Connected for ${uptime()}.` });
});
async function stop(signal) {
console.log(`Received ${signal}. Closing the Slack connection.`);
await app.stop();
process.exit(0);
}
process.on("SIGTERM", () => void stop("SIGTERM"));
process.on("SIGINT", () => void stop("SIGINT"));
(async () => {
try {
await app.start();
console.log("Slack Socket Mode connected. Standup bot is ready.");
} catch (error) {
console.error(`Slack connection failed. Check both tokens and reinstall the app if its scopes changed. ${error.message}`);
process.exit(1);
}
})();
manifest.json Raw
{
"_metadata": {
"major_version": 2
},
"display_information": {
"name": "Standup Pocket",
"description": "A small standup helper that stays connected through Socket Mode",
"background_color": "#31594d"
},
"features": {
"bot_user": {
"display_name": "Standup Pocket",
"always_online": true
},
"slash_commands": [
{
"command": "/standup",
"description": "Post one concise standup update",
"usage_hint": "Shipped the export flow and reviewing logs next",
"should_escape": false
},
{
"command": "/botuptime",
"description": "Show how long the bot has stayed connected",
"should_escape": false
}
]
},
"oauth_config": {
"scopes": {
"bot": [
"app_mentions:read",
"chat:write",
"commands"
]
}
},
"settings": {
"event_subscriptions": {
"bot_events": [
"app_mention"
]
},
"interactivity": {
"is_enabled": true
},
"org_deploy_enabled": false,
"socket_mode_enabled": true,
"token_rotation_enabled": false
}
}
package.json Raw
{
"name": "node-slack-bot",
"version": "1.0.0",
"private": true,
"engines": {
"node": ">=24"
},
"scripts": {
"start": "node app.js"
},
"dependencies": {
"@slack/bolt": "5.0.0"
}
}
Built from revision 52c7b7f