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.

  1. Open Your Apps and choose Create New App.
  2. Choose From an app manifest, select your workspace, and paste this repository's manifest.json into the JSON editor.
  3. Choose Next, review the permissions, then choose Create.
  4. Open Install App and choose Install to Workspace, then Allow.
  5. Copy the Bot User OAuth Token that begins with xoxb. Keep it ready for SLACK_BOT_TOKEN in the deployment steps below.
  6. Open Basic Information. Under App-Level Tokens, choose Generate Token and Scopes.
  7. Name the token, add the connections:write scope, and choose Generate.
  8. Copy the token that begins with xapp. Keep it ready for SLACK_APP_TOKEN in 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

  1. Invite @Standup Pocket to a test channel and mention it for help.
  2. Enter /standup Shipped the export flow and reviewing production logs next.
  3. 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.

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

Guide: Host a Slack bot