Skip to Content

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 Portal

Anomaly 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 dashboard

OAuth Setup Flow

  1. DM team clicks “Connect Slack” in Dashboard → Settings → Integrations
  2. Platform redirects to Slack OAuth (/oauth/v2/authorize with scopes)
  3. Slack redirects back with code → platform exchanges for botToken via /oauth.v2.access
  4. Platform calls auth.test() to confirm workspace access and store workspaceId
  5. Team member selects target channel(s) from a channel picker (calls conversations.list)
  6. Config saved to integrations table (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)

TestApproach
postMessage() calls chat.postMessage with correct channel and textMock WebClient; assert called with expected args
postDeliverableSummary() builds Block Kit with correct countsAssert blocks array structure matches expected shape
postDeliverableSummary() posts to deliverableChannelIdAssert channel arg equals cfg.deliverableChannelId
postAlert() falls back to deliverableChannelId when alertChannelId is absentMock cfg without alertChannelId; assert fallback used
postAlert() uses correct icon per severityAssert 🚨 for critical, ⚠️ for warning
uploadFile() returns fileId and permalinkMock filesUploadV2; assert mapped return
verify() throws when auth.test() returns ok: falseMock { ok: false, error: 'invalid_auth' }; assert throws

© 2026 Leadmetrics — Internal use only