ai-agenttutorialapislackintegrationbotwebhooksreal-time

Integrating AI Agents with External APIs - Part 2: Slack Integration

By AgentForge Hub8/14/20258 min read
Intermediate
Integrating AI Agents with External APIs - Part 2: Slack Integration

Ad Space

featuredImage: /assets/integrate-apis-part-2.png

Integrating AI Agents with External APIs - Part 2: Slack Integration

Slack is usually the first surface where stakeholders experience your agent. That means low latency messaging, predictable installs, and workflows that feel native. We will reuse the API scorecard and authentication service from Part 1, then go deep on Slack-specific routing, interactions, and observability.

Scenario: Incident Response Bridge

Our Escalation Concierge now needs to coordinate responders inside Slack:

  • Detect priority incidents and post to a channel with rich context cards.
  • Collect responder confirmations via buttons and modals.
  • Stream updates from the CRM while respecting rate limits and Slack's 3-second response budget.

We will build this in five layers so the resulting Slack app can ship to production without rewrites later.


Section 1: Slack Architecture Decisions

Before coding, decide how your agent will live inside Slack. Use this checklist:

Decision Options Guidance
App scope Workspace vs Org-wide Use workspace installs unless you control every workspace in the org.
Token strategy Bot token only vs user + bot Default to bot tokens; add user tokens only when an action must reflect user identity.
Event delivery Socket Mode vs HTTPS endpoint Socket Mode is simpler for local dev; HTTPS with a CDN is better for production scale.
Channel targeting Public, private, DMs, threads Request the minimum scopes (channels:history, im:write, etc.) needed for the workflow.
Message style Plain text, Block Kit, modals Combine Block Kit for summaries plus modals for data capture.

Document these choices with links to the Slack app config so the infra team can audit scopes quickly.


Section 2: Install and Authenticate

We will implement the OAuth handshake described in Part 1 but tailor it to Slack's bot scopes. Provide both Node and Python examples for parity.

Node.js Bolt install server

// slack/install-server.js
import express from "express";
import { InstallProvider } from "@slack/oauth";
import dotenv from "dotenv";
import crypto from "crypto";

dotenv.config();
const app = express();
const installer = new InstallProvider({
  clientId: process.env.SLACK_CLIENT_ID,
  clientSecret: process.env.SLACK_CLIENT_SECRET,
  stateSecret: process.env.STATE_SECRET,
  installationStore: {
    storeInstallation: async (install) => saveInstall(install),
    fetchInstallation: async (query) => loadInstall(query.teamId)
  }
});

app.get("/slack/install", async (req, res) => {
  const state = crypto.randomBytes(16).toString("hex");
  const url = await installer.generateInstallUrl({
    state,
    scopes: ["channels:history", "chat:write", "commands"],
    userScopes: ["users:read"],
    redirectUri: `${process.env.APP_URL}/slack/oauth_redirect`
  });
  res.redirect(url);
});

app.get("/slack/oauth_redirect", async (req, res) => {
  await installer.handleCallback(req, res, {
    redirectUri: `${process.env.APP_URL}/slack/oauth_redirect`
  });
  res.send("Slack workspace connected");
});

async function saveInstall(install) {
  // Persist bot token, team id, installed user, and scopes to your vault/db
  console.log("Saved install", install.team.id);
}

async function loadInstall(teamId) {
  // Retrieve installation metadata
  return queryInstallFromStore(teamId);
}

app.listen(3001, () => console.log("Slack OAuth listening on 3001"));

Python FastAPI install server

# slack/install_server.py
import os
import secrets
from fastapi import FastAPI, Request
from fastapi.responses import RedirectResponse, PlainTextResponse
import httpx

app = FastAPI()
STATE_CACHE = {}

@app.get("/slack/install")
async def start_install():
    state = secrets.token_hex(16)
    STATE_CACHE[state] = True
    scopes = "channels:history chat:write commands"
    user_scopes = "users:read"
    params = (
        f"client_id={os.environ['SLACK_CLIENT_ID']}"
        f"&scope={scopes}&user_scope={user_scopes}"
        f"&redirect_uri={os.environ['APP_URL']}/slack/oauth_redirect"
        f"&state={state}"
    )
    return RedirectResponse(f"https://slack.com/oauth/v2/authorize?{params}")

@app.get("/slack/oauth_redirect")
async def oauth_redirect(request: Request):
    code = request.query_params.get("code")
    state = request.query_params.get("state")
    if not STATE_CACHE.pop(state, None):
        return PlainTextResponse("invalid state", status_code=400)
    async with httpx.AsyncClient() as client:
        resp = await client.post(
            "https://slack.com/api/oauth.v2.access",
            data={
                "client_id": os.environ["SLACK_CLIENT_ID"],
                "client_secret": os.environ["SLACK_CLIENT_SECRET"],
                "code": code,
                "redirect_uri": f"{os.environ['APP_URL']}/slack/oauth_redirect"
            },
            headers={"Content-Type": "application/x-www-form-urlencoded"}
        )
    payload = resp.json()
    save_install(payload)
    return PlainTextResponse("Slack workspace connected")

def save_install(data: dict):
    # Persist bot token, refresh token, team, enterprise, and scopes
    print(f"Stored install for team {data.get('team', {}).get('id')}")

Reuse the token rotation strategy from Part 1; Slack refresh tokens expire in 12 hours, so schedule renewals well before that cutoff.


Section 3: Real-Time Routing and Messaging

With installs handled, wire up Bolt so your agent can post, reply, and stream context into threads.

Node.js Slack Bolt service

// slack/bridge-bot.js
import { App } from "@slack/bolt";
import { trackAuthEvent } from "../observability/instrumentation.js";
import { fetchIncidentSummary } from "../services/crm.js";
import { callWithBudget } from "../transport/rate-limiter.js";

const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  socketMode: true,
  appToken: process.env.SLACK_APP_TOKEN
});

app.event("app_mention", async ({ event, say }) => {
  await say({
    text: `Hi <@${event.user}>. Fetching the incident summary...`
  });
  const summary = await fetchIncidentSummary(event.text);
  await say({
    thread_ts: event.ts,
    blocks: summary.toBlockKit()
  });
});

app.message(/incident #(\d+)/i, async ({ context, say }) => {
  const incidentId = context.matches[1];
  await callWithBudget("slack-post", () => say({ text: `Tracking incident #${incidentId}` }));
});

app.start().then(() => trackAuthEvent("slack_bot_started", { service: "slack" }));

Python Slack Bolt service

# slack/bridge_bot.py
import os
from slack_bolt.async_app import AsyncApp
from slack_sdk.errors import SlackApiError
from transport.rate_limiter import AdaptiveLimiter
from services.crm import fetch_incident_summary

limiter = AdaptiveLimiter(rate_per_minute=60)
app = AsyncApp(token=os.environ["SLACK_BOT_TOKEN"], signing_secret=os.environ["SLACK_SIGNING_SECRET"])

@app.event("app_mention")
async def handle_mention(body, say):
    user = body["event"]["user"]
    await say(text=f"Hi <@{user}>. Pulling diagnostics...")
    summary = await fetch_incident_summary(body["event"]["text"])
    await say(blocks=summary.to_block_kit(), thread_ts=body["event"]["ts"])

@app.message(r"incident #(\d+)")
async def handle_incident(message, say, context):
    incident_id = context["matches"][0]
    async def post():
        await say(text=f"Tracking incident #{incident_id}")
    await limiter.run(post())

if __name__ == "__main__":
    app.start(port=3002)

Key practices:

  1. Respond within 3 seconds; if the workflow takes longer, ack() immediately and defer work to a queue.
  2. Use thread replies to keep channel noise low.
  3. Wrap Slack API calls in the shared rate limiter to avoid 429_request_limit_exceeded errors.

Section 4: Interactive Workflows and Automations

Slash commands, buttons, and modals let responders update incidents without leaving Slack.

Node.js slash command + modal

// slack/workflows.js
import { WebClient } from "@slack/web-api";
const client = new WebClient(process.env.SLACK_BOT_TOKEN);

export function registerWorkflows(app) {
  app.command("/bridge", async ({ ack, body, respond }) => {
    await ack();
    await client.views.open({
      trigger_id: body.trigger_id,
      view: buildEscalationModal(body.text)
    });
  });

  app.view("bridge_escalation", async ({ ack, body }) => {
    await ack();
    const payload = body.view.state.values;
    await notifyPagerDuty(payload);
  });
}

function buildEscalationModal(initialText = "") {
  return {
    type: "modal",
    callback_id: "bridge_escalation",
    title: { type: "plain_text", text: "Bridge Incident" },
    submit: { type: "plain_text", text: "Send" },
    blocks: [
      {
        type: "input",
        block_id: "incident",
        element: {
          type: "plain_text_input",
          action_id: "id",
          initial_value: initialText
        },
        label: { type: "plain_text", text: "Incident ID" }
      },
      {
        type: "input",
        block_id: "status",
        element: {
          type: "static_select",
          action_id: "state",
          options: ["investigating", "mitigated", "resolved"].map((label) => ({
            text: { type: "plain_text", text: label },
            value: label
          }))
        },
        label: { type: "plain_text", text: "Status" }
      }
    ]
  };
}

Python button handler for confirmations

# slack/interactions.py
from slack_bolt.async_app import AsyncApp
from services.crm import confirm_responder

app = AsyncApp()

@app.action("confirm_incident")
async def handle_confirm(ack, body, client):
    await ack()
    responder = body["user"]["id"]
    incident = body["actions"][0]["value"]
    await confirm_responder(incident, responder)
    await client.chat_postMessage(
        channel=body["channel"]["id"],
        thread_ts=body["message"]["ts"],
        text=f"<@{responder}> confirmed incident {incident}"
    )

Workflow guidance:

  1. Keep slash command payloads short; use modals for structured input.
  2. Store interaction payloads for audit trails.
  3. When calling external APIs from actions, use queues to avoid blocking Slack retries.

Section 5: Observability, Governance, and Runbooks

Slack apps live at the intersection of security and productivity. Instrument every request and publish runbooks so on-call engineers know what to do when tokens expire or Slack throttles calls.

Node.js logging middleware

// observability/slack-logger.js
export function attachLogging(app) {
  app.use(async ({ logger, context, next }) => {
    logger.info({
      event: context.event?.type,
      team: context.teamId,
      channel: context.channelId
    });
    try {
      await next();
    } catch (err) {
      logger.error({
        event: context.event?.type,
        error: err.message
      });
      throw err;
    }
  });
}

Python audit sink

# observability/audit.py
import json
from datetime import datetime

LOG_PATH = "logs/slack_audit.log"

def record(event_type: str, payload: dict):
    entry = {
        "timestamp": datetime.utcnow().isoformat(),
        "event_type": event_type,
        "team": payload.get("team_id"),
        "channel": payload.get("channel"),
        "user": payload.get("user"),
        "scopes": payload.get("authorizations", [{}])[0].get("scopes")
    }
    with open(LOG_PATH, "a", encoding="utf-8") as fh:
        fh.write(json.dumps(entry) + "\n")

Runbook must-haves:

  1. Steps to re-authorize the Slack app if tokens are revoked.
  2. Command to replay missed events when the queue backlog grows.
  3. Contact list for Slack admins to approve new scopes.
  4. Dashboards that watch error types (not_in_channel, missing_scope, rate_limited).

Implementation Checklist

  • Confirm architecture choices (scopes, delivery mode, channels) and document in the repo.
  • Deploy the Slack install service (Node or Python) behind HTTPS with vault-backed storage.
  • Run the Bolt app with rate limiting wrappers and ensure responses finish within 3 seconds.
  • Build interactive workflows (commands, buttons, modals) and persist their payloads.
  • Wire observability sinks plus runbooks for token refresh, scope requests, and throttling events.

Part 3 will extend these patterns to Discord and compare gateway intents, sharding, and moderation tooling. Keep your shared libraries ready; most abstractions carry over with minimal changes.

Ad Space

Recommended Tools & Resources

* This section contains affiliate links. We may earn a commission when you purchase through these links at no additional cost to you.

OpenAI API

AI Platform

Access GPT-4 and other powerful AI models for your agent development.

Pay-per-use

LangChain Plus

Framework

Advanced framework for building applications with large language models.

Free + Paid

Pinecone Vector Database

Database

High-performance vector database for AI applications and semantic search.

Free tier available

AI Agent Development Course

Education

Complete course on building production-ready AI agents from scratch.

$199

💡 Pro Tip

Start with the free tiers of these tools to experiment, then upgrade as your AI agent projects grow. Most successful developers use a combination of 2-3 core tools rather than trying everything at once.

🚀 Join the AgentForge Community

Get weekly insights, tutorials, and the latest AI agent developments delivered to your inbox.

No spam, ever. Unsubscribe at any time.

Loading conversations...