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.
npx -y skills add mugwork/mug --skill slackAssembled 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
| 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/<name>/ 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 existim:history— when Messages Tab is enabledlinks:read,links:write— when unfurl domains are configuredassistant: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 IDctx.params.triggerId— for opening modalsctx.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 tohandle-approvalworkflow- 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 fromcallback_id(e.g."submit"from"mug:create-job:submit")ctx.params.formValues— nested object:{ block_id: { action_id: { value, selected_option, ... } } }ctx.params.metadata— theprivate_metadatastring from the modal (stash context like channelId here)ctx.params.viewId— for updating the modal viactx.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)
- Check state:
mug slack setup --json - Open the config token page for the user: run
open https://api.slack.com/apps(or use theconfigTokenUrlfrom--json) - Tell the user: "Scroll to 'App Configuration Tokens' at the bottom, click 'Generate Token', select your workspace, copy the Access Token and Refresh Token"
- User pastes both tokens back — save them:
mug slack setup --config-token "xoxe.xoxp-..." --refresh-token "xoxe-..." - Create the app:
mug slack setup --create-app - Open the install URL for the user:
mug slack setup --install-url --open - After install, check state again with
--jsonto confirmhasBotToken: true - Open the app settings page so the user can upload a custom app icon (512px+ square PNG):
Theopen <appSettingsUrl from --json>appSettingsUrlfield from--jsonresolves tohttps://api.slack.com/apps/<APP_ID>/general.
Flow 2 — New app setup (developer is NOT admin)
- Generate instructions for the Slack admin:
This outputs copy-pasteable plain text with numbered steps and the manifest JSON included. The user sends this to their Slack admin.mug slack setup --admin-instructions - Admin follows the instructions, sends back 6 values: App ID, Client ID, Client Secret, Signing Secret, Access Token, Refresh Token
- Save credentials:
mug slack setup --app-id "A0..." --client-id "..." --client-secret "..." --signing-secret "..." - Save config tokens:
mug slack setup --config-token "xoxe.xoxp-..." --refresh-token "xoxe-..." - Open install URL:
mug slack setup --install-url --open - After install, open the app settings page for custom icon upload:
open <appSettingsUrl from --json>
Flow 3 — Refresh expired config token
- Check state:
mug slack setup --json— look forconfigTokenValid: false - Open the config token page: run
open https://api.slack.com/apps - Tell the user: "Scroll to 'App Configuration Tokens', click 'Generate Token' for your workspace, copy both tokens"
- Save fresh tokens:
mug slack setup --config-token "xoxe.xoxp-..." --refresh-token "xoxe-..."
Flow 4 — Reinstall app (after scope changes)
- Open the install URL:
mug slack setup --install-url --open - 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
- Deploy:
mug deploypushes the manifest and credentials - Install: someone with workspace admin access visits the install URL — OAuth stores the bot token automatically
- Future deploys: manifest auto-updates via config token, no manual steps
Secrets (managed by mug slack setup)
| Secret | Source |
|---|---|
SLACK_CONFIG_TOKEN | Generated by workspace admin at api.slack.com |
SLACK_CONFIG_REFRESH_TOKEN | Generated alongside config token |
SLACK_APP_ID | Auto-stored on app creation (or from Basic Information page) |
SLACK_CLIENT_ID | Auto-stored on app creation (or from Basic Information page) |
SLACK_CLIENT_SECRET | Auto-stored on app creation (or from Basic Information page) |
SLACK_SIGNING_SECRET | Auto-stored on app creation (or from Basic Information page) |
SLACK_BOT_TOKEN | Auto-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
- Add
"chat": truetoagents/<name>/agent.json - Set
"defaultAgent"inslack.jsonto the agent name mug deploy— auto-addsassistant:write,im:history,app_mentions:readscopes- Enable "Agents & AI Apps" in Slack UI:
https://api.slack.com/apps/<app_id>/app-assistant - 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 queryroutes 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?" }
]
}
agentprompts include anagent:<name>prefix to pin the threadmessageprompts 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