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

📚 Integrating AI Agents with External APIs
View All Parts in This Series
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.
Related Tools
Useful tools for this topic
If you want to turn this article into a concrete next step, start with one of these.
Solution Type Quiz
PlanningDecide whether your use case is better served by automation, a chatbot, RAG, a copilot, or a more capable agent.
Open toolArchitecture Recommender
ArchitectureGet a recommended starting architecture based on autonomy, data shape, action model, and team profile.
Open toolEvaluation Plan Builder
OperationsBuild a first evaluation plan for answer quality, action safety, human review, monitoring, and rollback.
Open tool📚 Integrating AI Agents with External APIs
View All Parts in This Series
Subscribe to AgentForge Hub
Get weekly insights, tutorials, and the latest AI agent developments delivered to your inbox.
No spam, ever. Unsubscribe at any time.
