Maniac Docs
Channels

Slack bot

Wire serveChannels to Slack webhooks with the channels-slack-bot example.

Slack bot

The channels-slack-bot.ts example shows end-to-end Slack integration: inbound mentions, streaming replies, outbound ChatToolset tools, and approval cards on mutating actions.

Prerequisites

  1. Install peers:

    npm install chat @chat-adapter/slack fastify
  2. Create a Slack app and set SLACK_BOT_TOKEN and SLACK_SIGNING_SECRET per @chat-adapter/slack docs.

  3. Expose a local port through a tunnel (ngrok, cloudflared, …) and point Slack's Event Subscriptions Request URL at:

    <tunnel>/api/agents/support/channels/slack/webhook

    Point Interactivity at:

    <tunnel>/api/agents/support/channels/slack/interaction
  4. Run the example:

    tsx packages/agents/examples/channels-slack-bot.ts

Full wiring

import { Maniac, OpenAICompatibleModel, StaticPermissionPolicy } from "@maniac-ai/agents";
import { SqliteMemory } from "@maniac-ai/agents/memory/sqlite";
import { ChatToolset, buildFastifyHandler, serveChannels } from "@maniac-ai/agents/channels";
import { Chat } from "chat";
import { createSlackAdapter } from "@chat-adapter/slack";
import Fastify from "fastify";

const chat = new Chat({ adapters: { slack: createSlackAdapter() } });

const policy = new StaticPermissionPolicy(
  [{
    id: "approve-chat-writes",
    principal: "*",
    scope: { toolset: "chat", arg_constraints: [] },
    effect: "require_approval"
  }],
  { allowed: true, requires_approval: false }
);

const app = new Maniac({
  model: new OpenAICompatibleModel({ slug: "gpt-4o-mini" }),
  memory: new SqliteMemory({ path: "./.channels-bot.sqlite" }),
  policy
});

app.agent({
  id: "support",
  instructions: [
    "You are a Slack support bot. Be concise and friendly.",
    "Use chat_* tools when asked to post, react, edit, or DM.",
    "Mutating tools require human approval — explain what you intend to do."
  ].join(" "),
  toolsets: [new ChatToolset({ chat, preset: "messenger", requireApproval: true })]
});

const server = serveChannels(app, "support", {
  chat,
  threadContext: { maxMessages: 10 },
  inlineMedia: ["image/*"],
  inlineLinks: [
    { match: "youtube.com", mimeType: "video/*" },
    { match: "youtu.be", mimeType: "video/*" }
  ]
});

const fastify = Fastify({ logger: true });
const handler = buildFastifyHandler(server);
fastify.post("/api/agents/:agentId/channels/:platform/webhook", handler);
fastify.post("/api/agents/:agentId/channels/:platform/interaction", handler);

await fastify.listen({ host: "0.0.0.0", port: 3000 });

serveChannels behavior

Each inbound handle(event) call:

  1. Resolves threadId / resourceId via ThreadIdResolver
  2. Subscribes to the platform thread and optionally backfills recent history into prefixMessages
  3. Prefixes group-thread messages with the speaker's display name
  4. Streams Maniac.chatStream through toChatStream into thread.post
  5. On status="paused", posts an approval card (or plain-text fallback when cards: false)

ChatToolset presets

ChatToolset exposes outbound chat actions to the model:

new ChatToolset({
  chat,
  preset: "messenger", // chat_post, chat_react, chat_edit, chat_dm, …
  requireApproval: true // runner-level gate for mutating tools
});

requireApproval: true pauses writes even when no explicit policy matches; an explicit require_approval policy still overrides per tool.

Webhook hardening

buildWebhookHandler (and buildFastifyHandler) support:

  • Raw-byte verifyRequest with slackSigningSecret
  • Atomic IdempotencyStore.claim (in-memory default; pass null to opt out)
  • maxBodyBytes cap → HTTP 413
  • SHA-256 body-hash dedup keys
  • agentId URL enforcement
  • ThreadIdResolution.platformThreadId for native thread lookup

Multimodal inbound

Image and file parts flow through to the model as ContentParts — Maniac.chat / chatStream accept string | MessageContent, not flattened text. Configure inlineMedia and inlineLinks on serveChannels to control which attachments become model-visible parts.

On this page