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

📚 Integrating AI Agents with External APIs
View All Parts in This Series
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:
- Respond within 3 seconds; if the workflow takes longer,
ack()immediately and defer work to a queue. - Use thread replies to keep channel noise low.
- Wrap Slack API calls in the shared rate limiter to avoid
429_request_limit_exceedederrors.
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:
- Keep slash command payloads short; use modals for structured input.
- Store interaction payloads for audit trails.
- 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:
- Steps to re-authorize the Slack app if tokens are revoked.
- Command to replay missed events when the queue backlog grows.
- Contact list for Slack admins to approve new scopes.
- 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.
📚 Featured AI Books
OpenAI API
AI PlatformAccess GPT-4 and other powerful AI models for your agent development.
LangChain Plus
FrameworkAdvanced framework for building applications with large language models.
Pinecone Vector Database
DatabaseHigh-performance vector database for AI applications and semantic search.
AI Agent Development Course
EducationComplete course on building production-ready AI agents from scratch.
💡 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.
📚 Integrating AI Agents with External APIs
View All Parts in This Series
🚀 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.



