# Slack

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

- **Type:** Skill
- **Install:** `agentstack add skill-mugwork-mug-slack`
- **Verified:** Pending review
- **Seller:** [mugwork](https://agentstack.voostack.com/s/mugwork)
- **Installs:** 0
- **Category:** [Communication](https://agentstack.voostack.com/c/communication)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [mugwork](https://github.com/mugwork)
- **Source:** https://github.com/mugwork/mug/tree/main/.claude/skills/slack
- **Website:** https://mug.work

## Install

```sh
agentstack add skill-mugwork-mug-slack
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# 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

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

### Full schema

```json
{
  "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

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | boolean | `false` | Enable Slack app for this workspace |
| `name` | string | workspace name | App display name in Slack (max 35 chars) |
| `description` | string | — | App description (max 140 chars) |
| `color` | string | — | App background color (hex, e.g. `"#1a1a2e"`) |
| `botName` | string | same as name | Bot display name (max 80 chars) |
| `homeTab` | object | — | Home Tab configuration (see below) |
| `messagesTab` | boolean | `false` | Enable DM tab — users can message the bot directly. Auto-enabled when any agent has `chat: true` in agent.json |
| `defaultAgent` | string | — | Agent name for DMs when no agent is specified (must match an `agents//` folder with `chat: true`) |
| `suggestedPrompts` | array | auto-generated | Prompts shown when user opens a DM. Each: `{ title, agent }` or `{ title, message }`. Max 4. |
| `appIcon` | string | — | Path to app icon image (512-2000px square) for reference. Must be uploaded manually via Slack UI — the manifest API doesn't support icons. |
| `shortcuts` | array | — | Global and message shortcuts (see below) |
| `unfurlDomains` | string[] | — | Domains to unfurl with rich previews (max 5) |
| `scopes` | string[] | — | Additional OAuth scopes (most are auto-inferred) |
| `commands` | object | — | Explicit slash commands not tied to workflow triggers |
| `events` | string[] | — | 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:
```json
{
  "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:
```json
{
  "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:
```json
{
  "type": "text",
  "title": "Welcome",
  "text": "Operations dashboard for Acme. Updated in real-time."
}
```

**divider** — visual separator:
```json
{ "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).

```json
{
  "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`:

```typescript
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.

```typescript
// 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::`

- `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

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

## Slash Commands

```typescript
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

```typescript
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)

```typescript
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`:

```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

```typescript
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.

```sql
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

```bash
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:
   ```bash
   mug slack setup --config-token "xoxe.xoxp-..." --refresh-token "xoxe-..."
   ```
5. Create the app:
   ```bash
   mug slack setup --create-app
   ```
6. Open the install URL for the user:
   ```bash
   mug slack setup --install-url --open
   ```
7. After install, check state again with `--json` to co

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [mugwork](https://github.com/mugwork)
- **Source:** [mugwork/mug](https://github.com/mugwork/mug)
- **License:** MIT
- **Homepage:** https://mug.work

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** yes

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: flagged — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-mugwork-mug-slack
- Seller: https://agentstack.voostack.com/s/mugwork
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
