agentsclimarketplace

Slack

Skill mugwork/mug/.agents/skills/slack

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

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.

SKILL.md

20.8 KB, ~5.4k tokens by cl100k_base, 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

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. 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.