oimiragieo/agent-studio

slack-expert

Expert-level Slack platform development using Bolt SDK — build Slack apps with slash commands, interactive components (buttons, modals, shortcuts), home tabs, event subscriptions, workflows, Socket Mode, and Block Kit.

First seen Mar 22, 2026

Installation

$ npx skills add oimiragieo/agent-studio --skill slack-expert

Summary

  • Expert-level Slack platform development using Bolt SDK — build Slack apps with slash commands, interactive components (buttons, modals, shortcuts), home tabs, event subscriptions, workflows, Socket Mode, and Block Kit.
  • Use for building Slack bots, workflow automation, and rich interactive Slack experiences.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from oimiragieo/agent-studio · top by installs.

npx skills add oimiragieo/agent-studio

Browse all from oimiragieo/agent-studio

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 40
Default branch main
Open issues 501
Status Active

Skill metadata

Parsed from SKILL.md frontmatter.

Version1.0.0

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 11,962 B
  • docs SUMMARY.md 330 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 2 installs

SKILL.md

Slack Expert Skill

Overview

Full-platform Slack development using the Bolt SDK. Covers interactive app development beyond basic messaging — slash commands, modals, shortcuts, home tabs, event subscriptions, workflow steps, and multi-workspace OAuth.

Related skill: slack-notifications — use that skill for simple one-way messaging/alerts. Use slack-expert when building interactive Slack apps or bots.

Requirements

Variable Purpose Required
SLACKBOTTOKEN Bot OAuth token (xoxb-...) Yes (single workspace)
SLACKSIGNINGSECRET Request signature verification Yes
SLACKAPPTOKEN Socket Mode app-level token (xapp-...) Socket Mode only
SLACKCLIENTID OAuth client ID Multi-workspace only
SLACKCLIENTSECRET OAuth client secret Multi-workspace only
SLACKSTATESECRET OAuth state token Multi-workspace only

Quick Start — Bolt App (Node.js)

const { App } = require('@slack/bolt');

const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  // For Socket Mode, add:
  socketMode: true,
  appToken: process.env.SLACK_APP_TOKEN,
});

// Slash command
app.command('/hello', async ({ command, ack, respond }) => {
  await ack();
  await respond(`Hello <@${command.user_id}>!`);
});

// Button click
app.action('approve_button', async ({ body, ack, client }) => {
  await ack();
  await client.views.open({
    trigger_id: body.trigger_id,
    view: confirmationModal,
  });
});

// Event subscription
app.event('app_mention', async ({ event, say }) => {
  await say({ text: `Hello <@${event.user}>!`, thread_ts: event.ts });
});

(async () => {
  await app.start(process.env.PORT || 3000);
  console.log('Bolt app running');
})();

Core Concepts

Listeners

// Message listener with regex
app.message(/deploy/, async ({ message, say }) => {
  await say('Triggering deployment...');
});

// Slash command — MUST ack() within 3 seconds
app.command('/status', async ({ ack, respond, command }) => {
  await ack();
  // Long-running: defer actual work
  respond({ text: 'Checking status...', response_type: 'ephemeral' });
  const status = await fetchStatus(); // async work after ack
  await respond({ text: `Status: ${status}` });
});

// Shortcut (global or message)
app.shortcut('create_ticket', async ({ shortcut, ack, client }) => {
  await ack();
  await client.views.open({
    trigger_id: shortcut.trigger_id,
    view: ticketModal,
  });
});

Modals (Views)

const modal = {
  type: 'modal',
  callback_id: 'ticket_submission',
  title: { type: 'plain_text', text: 'Create Ticket' },
  submit: { type: 'plain_text', text: 'Submit' },
  close: { type: 'plain_text', text: 'Cancel' },
  blocks: [
    {
      type: 'input',
      block_id: 'title_block',
      element: {
        type: 'plain_text_input',
        action_id: 'title_input',
        placeholder: { type: 'plain_text', text: 'Ticket title' },
      },
      label: { type: 'plain_text', text: 'Title' },
    },
    {
      type: 'input',
      block_id: 'priority_block',
      element: {
        type: 'static_select',
        action_id: 'priority_select',
        options: [
          { text: { type: 'plain_text', text: 'High' }, value: 'high' },
          { text: { type: 'plain_text', text: 'Medium' }, value: 'medium' },
          { text: { type: 'plain_text', text: 'Low' }, value: 'low' },
        ],
      },
      label: { type: 'plain_text', text: 'Priority' },
    },
  ],
};

// Handle modal submission
app.view('ticket_submission', async ({ ack, view, client, body }) => {
  await ack();
  const title = view.state.values.title_block.title_input.value;
  const priority = view.state.values.priority_block.priority_select.selected_option.value;
  await createTicket({ title, priority, userId: body.user.id });
});

Block Kit — Rich Messages

const richMessage = {
  blocks: [
    {
      type: 'header',
      text: { type: 'plain_text', text: 'Deployment Ready' },
    },
    {
      type: 'section',
      fields: [
        { type: 'mrkdwn', text: '*Service:*\napi-gateway' },
        { type: 'mrkdwn', text: '*Version:*\nv1.2.3' },
        { type: 'mrkdwn', text: '*Environment:*\nProduction' },
        { type: 'mrkdwn', text: '*Requested by:*\n<@U1234567>' },
      ],
    },
    { type: 'divider' },
    {
      type: 'actions',
      block_id: 'deploy_actions',
      elements: [
        {
          type: 'button',
          text: { type: 'plain_text', text: 'Approve' },
          style: 'primary',
          action_id: 'approve_deploy',
          value: 'v1.2.3',
          confirm: {
            title: { type: 'plain_text', text: 'Confirm Deploy' },
            text: { type: 'mrkdwn', text: 'Deploy *v1.2.3* to production?' },
            confirm: { type: 'plain_text', text: 'Deploy' },
            deny: { type: 'plain_text', text: 'Cancel' },
          },
        },
        {
          type: 'button',
          text: { type: 'plain_text', text: 'Reject' },
          style: 'danger',
          action_id: 'reject_deploy',
          value: 'v1.2.3',
        },
      ],
    },
  ],
};

App Home Tab

// Publish home tab when user opens it
app.event('app_home_opened', async ({ event, client }) => {
  if (event.tab !== 'home') return;

  await client.views.publish({
    user_id: event.user,
    view: {
      type: 'home',
      blocks: [
        {
          type: 'header',
          text: { type: 'plain_text', text: 'My App Dashboard' },
        },
        {
          type: 'section',
          text: { type: 'mrkdwn', text: 'Welcome to your app home!' },
          accessory: {
            type: 'button',
            text: { type: 'plain_text', text: 'Refresh' },
            action_id: 'refresh_home',
          },
        },
      ],
    },
  });
});

Lazy Listeners (Long-Running Operations)

// Use lazy: for operations that take >3s
app.command(
  '/report',
  async ({ command, ack, respond, say }) => {
    await ack(); // Must ack in <3s
  },
  async ({ command, respond }) => {
    // lazy: runs asynchronously, can take minutes
    const report = await generateReport(command.text);
    await respond({ text: report, response_type: 'in_channel' });
  }
);

Socket Mode (Internal Tools)

// Socket Mode — no public URL required, ideal for internal tools
const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  socketMode: true,
  appToken: process.env.SLACK_APP_TOKEN, // xapp- token
});

OAuth (Multi-Workspace Apps)

const { App, ExpressReceiver } = require('@slack/bolt');
const { FileInstallationStore } = require('@slack/oauth');

const receiver = new ExpressReceiver({
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  clientId: process.env.SLACK_CLIENT_ID,
  clientSecret: process.env.SLACK_CLIENT_SECRET,
  stateSecret: process.env.SLACK_STATE_SECRET,
  scopes: ['chat:write', 'channels:read', 'commands'],
  installationStore: new FileInstallationStore(),
});

const app = new App({ receiver });

Workflow Steps (Deprecated → Automations)

// Legacy workflow steps (still used in some workspaces)
const ws = new WorkflowStep('copy_review', {
  edit: async ({ ack, step, configure }) => {
    await ack();
    await configure({ blocks: editBlocks });
  },
  save: async ({ ack, step, view, update }) => {
    await ack();
    const { values } = view.state;
    await update({ inputs: {}, outputs: [] });
  },
  execute: async ({ step, complete, fail }) => {
    try {
      // do work
      await complete({ outputs: {} });
    } catch (err) {
      await fail({ error: { message: err.message } });
    }
  },
});
app.step(ws);

Scopes Reference

Capability Required Scopes
Post messages chat:write
Read channels channels:read, groups:read
Read messages channels:history, groups:history
Slash commands commands
React to messages reactions:write
Upload files files:write
Read user profiles users:read, users:read.email
Home tab im:history
Bot mentions app_mentions:read
DMs im:write, mpim:write

Common Patterns

Approval Workflow

app.command('/approve-request', async ({ command, ack, client }) => {
  await ack();
  // Post approval request to manager channel
  await client.chat.postMessage({
    channel: '#approvals',
    text: `<@${command.user_id}> requests approval`,
    blocks: buildApprovalBlocks(command),
  });
});

app.action('approve_action', async ({ action, body, ack, client }) => {
  await ack();
  const requestId = action.value;
  await markApproved(requestId, body.user.id);
  // Update original message
  await client.chat.update({
    channel: body.channel.id,
    ts: body.message.ts,
    text: `Approved by <@${body.user.id}>`,
    blocks: buildApprovedBlocks(requestId),
  });
});

User Lookup

async function lookupUser(client, email) {
  const result = await client.users.lookupByEmail({ email });
  return result.user.id;
}

Error Handling

app.error(async error => {
  console.error('Global error handler', error);
  // Notify admin channel
  await app.client.chat.postMessage({
    channel: process.env.ADMIN_CHANNEL,
    text: `App error: ${error.message}`,
  });
});

Security

  • Always verify SLACKSIGNINGSECRET to authenticate requests from Slack
  • Never log raw payloads (may contain user PII)
  • Use response_type: 'ephemeral' for sensitive command responses
  • Store tokens in encrypted secrets manager, not .env in production
  • Scope bot tokens to minimum required permissions
  • Rotate signing secrets if compromised — requires reinstalling the app

Debugging

# Enable Bolt debug logging
DEBUG=bolt:* node app.js

# Test with Slack CLI (requires Slack CLI installed)
slack run
slack triggers create --trigger-def "triggers/trigger.json"

# Replay events locally
slack events replay

Related