Slack
Category: Collaboration / Notifications
Integration type: Tenant OAuth (Bot Token — stored in integrations table)
External API: Slack Web API v1
Purpose
The Slack integration delivers platform outputs — reports, strategy documents, deliverable links, and operational alerts — directly into a designated Slack workspace. It serves two distinct audiences:
- Agency workspace — DM team receives anomaly alerts, approval-required pings, and internal report summaries
- Client workspace — Client receives monthly deliverable summaries and direct links to the DM Portal for review
The platform never posts full content to Slack. It posts structured summary messages with a link back to the DM Portal. All review and approval happens in the platform itself.
Config Structure
Tenant integration (OAuth)
Tenants connect Slack via OAuth. The bot requires the scopes: chat:write, files:write, channels:read.
interface SlackConfig {
botToken: string; // xoxb- bot token from OAuth install
workspaceId: string; // Slack workspace ID (T...)
deliverableChannelId: string; // Channel to post monthly deliverable summaries
alertChannelId?: string; // Channel for anomaly + operational alerts (default: deliverableChannelId)
teamName?: string; // Human-readable workspace name — display only
}Stored in integrations:
provider: 'slack'
api_key: encrypt(botToken)
metadata: {
workspaceId: string,
deliverableChannelId: string,
alertChannelId: string | null,
teamName: string,
}Integration Pattern
Tool layer (packages/tools/src/slack.ts)
import { WebClient } from '@slack/web-api';
class SlackTool {
private client: WebClient;
constructor(private cfg: SlackConfig) {
this.client = new WebClient(cfg.botToken);
}
/**
* Post a plain or Block Kit message to a channel.
*/
async postMessage(options: {
channelId: string;
text: string; // Fallback plain text (required by Slack)
blocks?: object[]; // Block Kit blocks for rich formatting
}): Promise<{ ts: string; channelId: string }> {
const result = await this.client.chat.postMessage({
channel: options.channelId,
text: options.text,
blocks: options.blocks,
});
return {
ts: result.ts as string,
channelId: result.channel as string,
};
}
/**
* Post a structured deliverable summary with a portal link.
*/
async postDeliverableSummary(options: {
tenantName: string;
periodLabel: string; // e.g. "April 2026"
deliverableUrl: string; // DM Portal URL to the deliverable period
blogCount: number;
socialCount: number;
pendingApproval: number;
}): Promise<{ ts: string }> {
const blocks: object[] = [
{
type: 'header',
text: { type: 'plain_text', text: `📦 ${options.periodLabel} Deliverables Ready` },
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*${options.tenantName}* — Your ${options.periodLabel} content package is ready for review.`,
},
},
{
type: 'section',
fields: [
{ type: 'mrkdwn', text: `*Blog Posts:*\n${options.blogCount}` },
{ type: 'mrkdwn', text: `*Social Posts:*\n${options.socialCount}` },
{ type: 'mrkdwn', text: `*Awaiting Approval:*\n${options.pendingApproval}` },
],
},
{
type: 'actions',
elements: [
{
type: 'button',
style: 'primary',
text: { type: 'plain_text', text: 'Review Deliverables' },
url: options.deliverableUrl,
},
],
},
];
const result = await this.postMessage({
channelId: this.cfg.deliverableChannelId,
text: `${options.periodLabel} deliverables for ${options.tenantName} are ready.`,
blocks,
});
return { ts: result.ts };
}
/**
* Post an anomaly or operational alert.
*/
async postAlert(options: {
severity: 'info' | 'warning' | 'critical';
title: string;
description: string;
portalUrl?: string;
}): Promise<void> {
const icon = { info: 'ℹ️', warning: '⚠️', critical: '🚨' }[options.severity];
const channelId = this.cfg.alertChannelId ?? this.cfg.deliverableChannelId;
const blocks: object[] = [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `${icon} *${options.title}*\n${options.description}`,
},
...(options.portalUrl && {
accessory: {
type: 'button',
text: { type: 'plain_text', text: 'View in Platform' },
url: options.portalUrl,
},
}),
},
];
await this.postMessage({
channelId,
text: `${icon} ${options.title}`,
blocks,
});
}
/**
* Upload a file (e.g. PDF report) to a channel.
*/
async uploadFile(options: {
channelId: string;
buffer: Buffer;
filename: string;
title: string;
mimeType?: string;
}): Promise<{ fileId: string; permalink: string }> {
const result = await this.client.filesUploadV2({
channel_id: options.channelId,
filename: options.filename,
title: options.title,
content: options.buffer.toString('base64'),
});
const file = (result.files as any[])[0].complete;
return {
fileId: file.id,
permalink: file.permalink,
};
}
async verify(): Promise<void> {
const result = await this.client.auth.test();
if (!result.ok) throw new Error(`Slack auth test failed: ${result.error}`);
}
}Agent Workflow
Report delivery
Report Writer agent generates monthly performance report
│
▼ (HITL approval by DM team)
Report Publisher worker
├── Fetch generated report from MongoDB
├── Resolve tenant's Slack integration
├── Call postDeliverableSummary() with portal link
└── Store Slack ts (message ID) in deliverable_periods.slack_ts
│
▼
Client receives notification in their Slack workspace
Client clicks "Review Deliverables" → lands in DM PortalAnomaly alerts
Anomaly Detector agent flags metric deviation
│
▼
Notification worker
├── Resolve agency's internal Slack integration
├── Call postAlert() with severity = 'warning' or 'critical'
└── Include link to Insights dashboardOAuth Setup Flow
- DM team clicks “Connect Slack” in Dashboard → Settings → Integrations
- Platform redirects to Slack OAuth (
/oauth/v2/authorizewith scopes) - Slack redirects back with
code→ platform exchanges forbotTokenvia/oauth.v2.access - Platform calls
auth.test()to confirm workspace access and storeworkspaceId - Team member selects target channel(s) from a channel picker (calls
conversations.list) - Config saved to
integrationstable (encrypted)
Rate Limits
Slack Tier 3 methods (chat.postMessage): 1 request/second. The tool layer uses an exponential backoff retry on ratelimited errors.
Test Cases
Unit tests (packages/tools/src/slack.test.ts)
| Test | Approach |
|---|---|
postMessage() calls chat.postMessage with correct channel and text | Mock WebClient; assert called with expected args |
postDeliverableSummary() builds Block Kit with correct counts | Assert blocks array structure matches expected shape |
postDeliverableSummary() posts to deliverableChannelId | Assert channel arg equals cfg.deliverableChannelId |
postAlert() falls back to deliverableChannelId when alertChannelId is absent | Mock cfg without alertChannelId; assert fallback used |
postAlert() uses correct icon per severity | Assert 🚨 for critical, ⚠️ for warning |
uploadFile() returns fileId and permalink | Mock filesUploadV2; assert mapped return |
verify() throws when auth.test() returns ok: false | Mock { ok: false, error: 'invalid_auth' }; assert throws |
Related
- Notion Provider — alternative document delivery to client workspaces
- Google Drive Provider — file and document storage alternative
- Report Writer Agent — generates the monthly performance report
- Anomaly Detector Agent — flags metric deviations
- Tool Integration Layer — integration management