Install
$ agentstack add skill-mugwork-mug-slack Open-source listing — not yet scanned by AgentStack. Follow the source repository for install instructions.
Security review
⚠ Flagged1 finding(s); flagged for manual review. · v0.1.0 How review works →
- • Prompt-injection patterns
- • Secret / credential exfiltration
- • Dangerous shell & filesystem operations
- • Untrusted network calls
- • Known-malicious package signatures
- high Dangerous shell/eval execution.
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ● Dynamic code execution Used
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
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
{
"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// 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::
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 ` } },
],
});
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:
``bash mug slack setup --config-token "xoxe.xoxp-..." --refresh-token "xoxe-..." ``
- Create the app:
``bash mug slack setup --create-app ``
- Open the install URL for the user:
``bash mug slack setup --install-url --open ``
- After install, check state again with
--jsonto 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
- Source: mugwork/mug
- License: MIT
- Homepage: https://mug.work
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.