agentsclimarketplace

Slack

Skill mugwork/mug/.agents/skills/slack

Mug is an AI automation platform for building deployed agents, real code workflows, & headless web surfaces for everyday business, integrated with any API, and accessed via email, SMS, & Slack — all built locally using Claude Code, Codex, & Cursor.

Install
npx -y skills add mugwork/mug --skill slack

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Build Slack integrations — configure slack.json, send Block Kit messages, slash commands, interactive buttons, Home Tab, shortcuts, and two-way workflows.

SKILL.md

20.8 KB, as published. Nobody here has run it

Slack Integration

Build Slack-powered surfaces for workspaces. Mug creates per-client Slack apps via manifest API — each client gets their own branded app, one-click install, full Block Kit control.

For full API reference, see .mug/docs/slack.md, .mug/docs/api.md (Slack section), and .mug/docs/notifications.md.

slack.json — App Configuration

Every workspace has a slack.json at the root. Created by mug init with {"enabled": false}.

Minimal — enable Slack

{
  "enabled": true,
  "name": "Acme Dispatch",
  "description": "Job dispatch and approval"
}

Full schema

{
  "enabled": true,
  "name": "Acme Dispatch",
  "description": "Job dispatch and approval",
  "color": "#1a1a2e",
  "botName": "acme-ops",
  "homeTab": {
    "enabled": true,
    "sections": [
      {
        "type": "text",
        "title": "Operations Dashboard",
        "text": "Real-time overview of active jobs and crew status."
      },
      {
        "type": "query",
        "title": "Active Jobs",
        "database": "jobs",
        "query": "SELECT title, status, assignee FROM jobs WHERE status = 'active' ORDER BY created_at DESC",
        "columns": ["title", "status", "assignee"]
      },
      {
        "type": "actions",
        "buttons": [
          { "text": "Run Daily Report", "workflow": "daily-report", "style": "primary" },
          { "text": "Sync All Sources", "workflow": "run-sync" }
        ]
      },
      { "type": "divider" },
      {
        "type": "query",
        "title": "Recent Alerts",
        "database": "ops",
        "query": "SELECT message, created_at FROM alerts ORDER BY created_at DESC LIMIT 5",
        "columns": ["message", "created_at"],
        "emptyMessage": "No recent alerts."
      }
    ]
  },
  "messagesTab": true,
  "shortcuts": [
    {
      "name": "Create Dispatch",
      "callbackId": "create_dispatch",
      "description": "Create a new dispatch job",
      "type": "global",
      "workflow": "create-dispatch"
    }
  ],
  "unfurlDomains": ["acme.mug.work"],
  "scopes": []
}

Field reference

FieldTypeDefaultDescription
enabledbooleanfalseEnable Slack app for this workspace
namestringworkspace nameApp display name in Slack (max 35 chars)
descriptionstringApp description (max 140 chars)
colorstringApp background color (hex, e.g. "#1a1a2e")
botNamestringsame as nameBot display name (max 80 chars)
homeTabobjectHome Tab configuration (see below)
messagesTabbooleanfalseEnable DM tab — users can message the bot directly. Auto-enabled when any agent has chat: true in agent.json
defaultAgentstringAgent name for DMs when no agent is specified (must match an agents/<name>/ folder with chat: true)
suggestedPromptsarrayauto-generatedPrompts shown when user opens a DM. Each: { title, agent } or { title, message }. Max 4.
appIconstringPath to app icon image (512-2000px square) for reference. Must be uploaded manually via Slack UI — the manifest API doesn't support icons.
shortcutsarrayGlobal and message shortcuts (see below)
unfurlDomainsstring[]Domains to unfurl with rich previews (max 5)
scopesstring[]Additional OAuth scopes (most are auto-inferred)
commandsobjectExplicit slash commands not tied to workflow triggers
eventsstring[]Additional event subscriptions

Scope auto-inference

Most scopes are inferred automatically:

  • Always added: chat:write, channels:join, channels:read, groups:read, users:read, users:read.email, im:write, mpim:read, mpim:write, reactions:write, files:write
  • commands — when slash commands or shortcuts exist
  • im:history — when Messages Tab is enabled
  • links:read, links:write — when unfurl domains are configured
  • assistant:write, app_mentions:read — when chat agents detected

Add explicit scopes only for capabilities not covered by auto-inference.

Scope change warnings

When a deploy adds new OAuth scopes, mug deploy warns:

⚠ New OAuth scopes: users:read, users:read.email — workspace admin may need to re-authorize

Home Tab

The Home Tab is a per-user dashboard inside the Slack app. Configured in slack.json as sections, rendered automatically when a user opens the app.

Section types

query — run SQL, render as a table:

{
  "type": "query",
  "title": "Active Projects",
  "database": "projects",
  "query": "SELECT name, status, manager FROM projects WHERE status = 'active'",
  "columns": ["name", "status", "manager"],
  "emptyMessage": "No active projects."
}

actions — buttons that trigger workflows:

{
  "type": "actions",
  "buttons": [
    { "text": "Run Daily Report", "workflow": "daily-report", "style": "primary" },
    { "text": "Sync Data", "workflow": "run-sync" }
  ]
}

Button styles: "primary" (green), "danger" (red), or omit for default gray.

text — headers and markdown:

{
  "type": "text",
  "title": "Welcome",
  "text": "Operations dashboard for Acme. Updated in real-time."
}

divider — visual separator:

{ "type": "divider" }

Limits

  • Max 100 blocks per Home Tab (Slack limit). Sections truncate with "showing X of Y" if over.
  • Query results capped at 15 rows per section.
  • Home Tab refreshes each time a user opens the app.

Shortcuts

Shortcuts appear in Slack's lightning bolt menu (global) or message context menu (message).

{
  "shortcuts": [
    {
      "name": "Run Workflow",
      "callbackId": "run_workflow",
      "description": "Trigger a Mug workflow",
      "type": "global",
      "workflow": "run-workflow"
    },
    {
      "name": "Summarize Thread",
      "callbackId": "summarize",
      "description": "AI summary of this thread",
      "type": "message",
      "workflow": "summarize-thread"
    }
  ]
}

The workflow field maps the shortcut directly to a workflow. The workflow receives:

  • ctx.params.callbackId — the shortcut's callback ID
  • ctx.params.triggerId — for opening modals
  • ctx.params.userId, ctx.params.userName
  • For message shortcuts: ctx.params.messageTs, ctx.params.channelId, ctx.params.messageText

Workflow Triggers

Slack triggers are defined in workflow .ts files, not in slack.json:

import { workflow } from "@mugwork/mug";

workflow("handle-dispatch", async (ctx) => {
  // ctx.params.command, ctx.params.text, ctx.params.triggerId
  await ctx.slack.openModal({ ... });
}, {
  trigger: { type: "slack_command", command: "/dispatch", description: "Create a dispatch" },
});

workflow("classify-message", async (ctx) => {
  // ctx.params.text, ctx.params.userId, ctx.params.channelId
}, {
  trigger: { type: "slack_event", event: "message" },
});

Triggers merge into the manifest automatically at deploy time.

Sending Messages

to accepts a channel name (#ops-alerts or ops-alerts) or a channel ID (C01234ABCDE). Channel names are resolved automatically. The bot auto-joins public channels on first message — private channels need the bot added manually via the channel's Integrations tab.

// Plain text
await ctx.notify.slack({
  to: "#ops-alerts",
  message: "New job assigned",
});

// Block Kit — raw blocks, no Mug abstraction
await ctx.notify.slack({
  to: "C01234ABCDE",
  message: "Approval needed",
  blocks: [
    {
      type: "section",
      text: { type: "mrkdwn", text: `*New job:* ${job.title}\n*Customer:* ${job.customer}` },
    },
    {
      type: "actions",
      elements: [
        {
          type: "button",
          text: { type: "plain_text", text: "Approve" },
          action_id: "mug:handle-approval:approve",
          value: job.id.toString(),
          style: "primary",
        },
        {
          type: "button",
          text: { type: "plain_text", text: "Reject" },
          action_id: "mug:handle-approval:reject",
          value: job.id.toString(),
          style: "danger",
        },
      ],
    },
  ],
});

// Threading
await ctx.notify.slack({
  to: channelId,
  message: "Update on the job",
  thread_ts: originalMessageTs,
});

Action ID Convention

Button action_id format: mug:<workflow>:<custom>

  • mug:handle-approval:approve → routes to handle-approval workflow
  • Without mug: prefix → routes to the default inbound Slack handler

The workflow receives ctx.params.actionId (the custom part) and ctx.params.actionValue.

Message Updates

await ctx.slack.updateMessage({
  channel: ctx.params.channelId,
  ts: ctx.params.messageTs,
  text: "Approved",
  blocks: [
    { type: "section", text: { type: "mrkdwn", text: `*Approved* by <@${ctx.params.userId}>` } },
  ],
});

Slash Commands

workflow("handle-dispatch", async (ctx) => {
  await ctx.slack.openModal({
    triggerId: ctx.params.triggerId,
    view: {
      type: "modal",
      title: { type: "plain_text", text: "Create Dispatch" },
      submit: { type: "plain_text", text: "Create" },
      blocks: [
        {
          type: "input",
          element: { type: "plain_text_input", action_id: "title" },
          label: { type: "plain_text", text: "Job Title" },
        },
      ],
    },
  });
}, {
  trigger: { type: "slack_command", command: "/dispatch", description: "Create a dispatch" },
});

Modal Forms

Slash commands and shortcuts can open modals for structured data collection. The same workflow handles opening, submission, and dynamic selects.

Open a modal

workflow("create-job", async (ctx) => {
  if (ctx.params.type === "slash_command") {
    await ctx.slack.openModal({
      triggerId: ctx.params.triggerId,
      view: {
        type: "modal",
        callback_id: "mug:create-job:submit",
        private_metadata: JSON.stringify({ channelId: ctx.params.channelId }),
        title: { type: "plain_text", text: "New Job" },
        submit: { type: "plain_text", text: "Create" },
        blocks: [
          {
            type: "input",
            block_id: "title_block",
            label: { type: "plain_text", text: "Title" },
            element: { type: "plain_text_input", action_id: "title" },
          },
          {
            type: "input",
            block_id: "priority_block",
            label: { type: "plain_text", text: "Priority" },
            element: {
              type: "static_select",
              action_id: "priority",
              options: [
                { text: { type: "plain_text", text: "Low" }, value: "low" },
                { text: { type: "plain_text", text: "Medium" }, value: "medium" },
                { text: { type: "plain_text", text: "High" }, value: "high" },
              ],
            },
          },
          {
            type: "input",
            block_id: "customer_block",
            label: { type: "plain_text", text: "Customer" },
            element: {
              type: "external_select",
              action_id: "customer-picker",
              placeholder: { type: "plain_text", text: "Search customers..." },
              min_query_length: 1,
            },
          },
        ],
      },
    });
    return { opened: true };
  }

  // Handle submission
  if (ctx.params.type === "view_submission") {
    const values = ctx.params.formValues;
    const title = values?.title_block?.title?.value;
    const priority = values?.priority_block?.priority?.selected_option?.value;
    const customer = values?.customer_block?.["customer-picker"]?.selected_option?.text?.text;
    const meta = JSON.parse(ctx.params.metadata || "{}");

    await ctx.exec("INSERT INTO jobs (title, priority, customer) VALUES (?, ?, ?)", [title, priority, customer]);
    await ctx.notify.slack({ to: meta.channelId, message: `Job created: ${title} (${priority}) for ${customer}` });
    return { created: true };
  }
}, {
  trigger: { type: "slack_command", command: "/newjob", description: "Create a new job" },
});

Modal submission params

When a user submits a modal, the workflow receives:

  • ctx.params.type"view_submission"
  • ctx.params.actionId — the custom part from callback_id (e.g. "submit" from "mug:create-job:submit")
  • ctx.params.formValues — nested object: { block_id: { action_id: { value, selected_option, ... } } }
  • ctx.params.metadata — the private_metadata string from the modal (stash context like channelId here)
  • ctx.params.viewId — for updating the modal via ctx.slack.updateModal()
  • ctx.params.userId, ctx.params.userName, ctx.params.triggerId

Update a modal (multi-step flows)

await ctx.slack.updateModal({
  viewId: ctx.params.viewId,
  view: { type: "modal", title: { ... }, blocks: [ /* step 2 blocks */ ] },
});

Dynamic select menus

For dropdowns that search large datasets (100+ options), use external_select in the modal block and configure a suggestions mapping in slack.json:

{
  "suggestions": {
    "customer-picker": {
      "database": "crm",
      "query": "SELECT name, id FROM customers WHERE name LIKE ? AND _mug_deleted_at IS NULL LIMIT 20"
    }
  }
}

The action_id on the external_select block must match the key in suggestions. The query receives the user's typed text as a %value% LIKE parameter. First column = display text, second column = value.

For small option sets (under 100), use static_select instead — no config needed.

Human-in-the-Loop

const callbackUrl = await ctx.waitForUrl("approval");

await ctx.notify.slack({
  to: "#approvals",
  message: "Approve this job?",
  blocks: [
    { type: "section", text: { type: "mrkdwn", text: `*${job.title}* — $${job.amount}` } },
    {
      type: "actions",
      elements: [
        {
          type: "button",
          text: { type: "plain_text", text: "Approve" },
          url: `${callbackUrl}?action=approved`,
          style: "primary",
        },
        {
          type: "button",
          text: { type: "plain_text", text: "Reject" },
          url: `${callbackUrl}?action=rejected`,
          style: "danger",
        },
      ],
    },
  ],
});

const result = await ctx.waitFor("approval", { timeout: "24h" });
if (result.timedOut) { /* escalate */ }
// result.payload.action === "approved" or "rejected"

Slack Data

After Slack app install, slack_users and slack_channels tables auto-sync every 6 hours.

SELECT t.name, t.specialty, s.display_name, s.email
FROM technicians t
JOIN slack_users s ON s.email = t.email;

App Setup

mug slack setup supports both interactive (terminal) and flag-driven (agent) modes. All flags can be used by AI agents — no readline prompts needed.

Check current state

mug slack setup --json

Returns machine-readable JSON with all state fields: appId, hasCredentials, hasConfigToken, configTokenValid, hasBotToken, hasAgents, installUrl, configTokenUrl, plus app-specific URLs (eventSubscriptionsUrl, agentToggleUrl, appSettingsUrl) when an app exists.

Always start here to determine which flow to run.

Flow 1 — New app setup (developer is admin)

  1. Check state: mug slack setup --json
  2. Open the config token page for the user: run open https://api.slack.com/apps (or use the configTokenUrl from --json)
  3. Tell the user: "Scroll to 'App Configuration Tokens' at the bottom, click 'Generate Token', select your workspace, copy the Access Token and Refresh Token"
  4. User pastes both tokens back — save them:
    mug slack setup --config-token "xoxe.xoxp-..." --refresh-token "xoxe-..."
    
  5. Create the app:
    mug slack setup --create-app
    
  6. Open the install URL for the user:
    mug slack setup --install-url --open
    
  7. After install, check state again with --json to confirm hasBotToken: true
  8. Open the app settings page so the user can upload a custom app icon (512px+ square PNG):
    open <appSettingsUrl from --json>
    
    The appSettingsUrl field from --json resolves to https://api.slack.com/apps/<APP_ID>/general.

Flow 2 — New app setup (developer is NOT admin)

  1. Generate instructions for the Slack admin:
    mug slack setup --admin-instructions
    
    This outputs copy-pasteable plain text with numbered steps and the manifest JSON included. The user sends this to their Slack admin.
  2. Admin follows the instructions, sends back 6 values: App ID, Client ID, Client Secret, Signing Secret, Access Token, Refresh Token
  3. Save credentials:
    mug slack setup --app-id "A0..." --client-id "..." --client-secret "..." --signing-secret "..."
    
  4. Save config tokens:
    mug slack setup --config-token "xoxe.xoxp-..." --refresh-token "xoxe-..."
    
  5. Open install URL:
    mug slack setup --install-url --open
    
  6. After install, open the app settings page for custom icon upload:
    open <appSettingsUrl from --json>
    

Flow 3 — Refresh expired config token

  1. Check state: mug slack setup --json — look for configTokenValid: false
  2. Open the config token page: run open https://api.slack.com/apps
  3. Tell the user: "Scroll to 'App Configuration Tokens', click 'Generate Token' for your workspace, copy both tokens"
  4. Save fresh tokens:
    mug slack setup --config-token "xoxe.xoxp-..." --refresh-token "xoxe-..."
    

Flow 4 — Reinstall app (after scope changes)

  1. Open the install URL:
    mug slack setup --install-url --open
    
  2. User clicks "Allow" in the browser to re-authorize with new scopes

Other flags

mug slack setup --manifest          # output generated manifest JSON
mug slack setup --install-url       # print the OAuth install URL
mug slack setup --install-url --open # print and open in browser

After setup

  1. Deploy: mug deploy pushes the manifest and credentials
  2. Install: someone with workspace admin access visits the install URL — OAuth stores the bot token automatically
  3. Future deploys: manifest auto-updates via config token, no manual steps

Secrets (managed by mug slack setup)

SecretSource
SLACK_CONFIG_TOKENGenerated by workspace admin at api.slack.com
SLACK_CONFIG_REFRESH_TOKENGenerated alongside config token
SLACK_APP_IDAuto-stored on app creation (or from Basic Information page)
SLACK_CLIENT_IDAuto-stored on app creation (or from Basic Information page)
SLACK_CLIENT_SECRETAuto-stored on app creation (or from Basic Information page)
SLACK_SIGNING_SECRETAuto-stored on app creation (or from Basic Information page)
SLACK_BOT_TOKENAuto-stored after OAuth install callback

AI Agent DMs (Agents & AI Apps)

Mug uses Slack's "Agents & AI Apps" feature for agent conversations. When any agent has chat: true in agent.json, deploy auto-enables the Agents & AI mode with proper scopes and events.

Setup

  1. Add "chat": true to agents/<name>/agent.json
  2. Set "defaultAgent" in slack.json to the agent name
  3. mug deploy — auto-adds assistant:write, im:history, app_mentions:read scopes
  4. Enable "Agents & AI Apps" in Slack UI: https://api.slack.com/apps/<app_id>/app-assistant
  5. Set suggested prompts to "Dynamic" in the same UI page

Multi-agent routing

Multiple agents can share one Slack app. Users are routed by:

  • Suggested prompts: clicking a prompt pins the thread to that agent
  • @mentions: @BotName agent-name query routes to the named agent
  • Thread pinning: once a thread is assigned to an agent, all messages in that thread go to the same agent

Configure per-agent names visible in Slack with "slackName" in agent.json:

{
  "model": "openai/gpt-4.1-nano",
  "tools": ["query"],
  "chat": true,
  "slackName": "ops-bot"
}

Suggested prompts

Auto-generated from chat agents, or configure explicitly in slack.json:

{
  "suggestedPrompts": [
    { "title": "Ask Ops Bot", "agent": "ops-bot" },
    { "title": "Check Schedule", "message": "What's on the schedule today?" }
  ]
}
  • agent prompts include an agent:<name> prefix to pin the thread
  • message prompts send the text directly to the default agent
  • Max 4 prompts (Slack limit)

App icon

Slack's manifest API doesn't support setting app icons programmatically. Upload manually at https://api.slack.com/apps/<app_id>/general (512px+ square PNG). mug deploy prints this link on first app creation.

Block Kit Reference

Agents already know Block Kit. Use Slack's Block Kit Builder for visual design: https://app.slack.com/block-kit-builder

For Slack API docs: https://docs.slack.dev/apis/web-api.md

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.