Building Slack bots is fun until you try to integrate heavy AI pipelines.
If you are building an AI agent that runs sequential steps (e.g., Planning -> Web Searching -> Code Execution -> Summarization), the process is going to take 10 to 20 seconds.
Here is the problem: Slack slash commands MUST receive an HTTP response within 3,000 milliseconds. If your server takes 3.1 seconds to respond, the user gets a generic "operation_timeout" error.
In this post, I will share the exact architecture we used to bypass this timeout for our open-source agent platform, Saar Nexus, using Bun, Hono, and event-driven webhook streaming.
The Timeout Architecture
To run a long orchestration pipeline while keeping Slack happy, you cannot run the AI blocking-synchronously. Instead, the workflow must be divided into an instant acknowledgment and a decoupled background execution loop.
[Slack User] ──(Slash Command)──> [Hono Server]
│
(Verifies & ACKs <3s)
│
▼ (Spawns Async Task)
[Agent Loop]
│
(Streams updates to response_url in-place)
│
▼
[Slack Channel]
Step 1: Return the Instant ACK
When Slack hits our POST endpoint, we parse the signing headers and immediately return a 200 OK with a JSON payload telling the user that the request was received. This keeps the HTTP request under 150ms.
Here is how we handle it in Hono:
typescript
router.post('/slack/command', async (c) => {
const body = await c.req.parseBody();
const text = body.text;
const responseUrl = body.response_url;
// Acknowledge immediately to avoid Slack's 3-second timeout
const ackResponse = c.json({
response_type: 'ephemeral',
text: 🎯 Summoning Nexus agents for: "${text}"...,
});
// Trigger background task (Fire and Forget)
if (responseUrl) {
runOrchestrationInBackground(text, responseUrl);
}
return ackResponse;
});
Step 2: Stream Progress back to Slack (Bypassing the "Black Box")
Once the command is acknowledged, the server triggers the background task. But if the user has to wait 20 seconds for a response, they might think the server crashed.
To solve this, we stream progress updates back to Slack's webhook (response_url). Slack allows you to send up to 5 updates to this URL within 30 minutes.
To keep the UX clean, we do not post multiple new messages (which would spam the channel). Instead, we send a JSON payload with replace_original: true. This overwrites the existing message in-place:
typescript
async function runOrchestrationInBackground(prompt: string, responseUrl: string) {
try {
let statusText = "🤔 Triage agent is planning implementation...";
// Send first status update (replace original thinking message)
await postToSlack(responseUrl, `🎯 *Request:* "${prompt}"\n\n${statusText}`);
// Stream generator events from our multi-agent runner
for await (const event of orchestrate(prompt)) {
if (event.type === 'plan') {
statusText = `📝 *Created plan!* Running specialist agents...`;
} else if (event.type === 'merging') {
statusText = `🔀 Merging findings into final response...`;
} else if (event.type === 'merged') {
// Final formatted response (converts standard markdown to Slack mrkdwn)
const formatted = formatSlackResponse(event.mergedBrief);
await postToSlack(responseUrl, formatted);
return; // Finished!
}
// Update progress in-place
await postToSlack(responseUrl, `🎯 *Request:* "${prompt}"\n\n${statusText}`);
}
} catch (err) {
await postToSlack(responseUrl, ❌ Something went wrong: ${err.message});
}
}
async function postToSlack(url: string, text: string) {
await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
response_type: 'in_channel',
replace_original: true,
text: text
})
});
}
Step 3: Throttling for Rate Limits
Notice that we do not post on every single token or event. Because Slack limits us to 5 updates per URL, we only update on 4 key milestones:
Initial command received (ACK)
Orchestrator plan generated
Merge step started
Final completed response
This keeps us safely below the limit, ensuring your bots never throw 404 used_url errors.
Persisting configuration locally
Since the Slack integration supports multi-workspace connections, we store bot tokens dynamically. To ensure this survives VPS deployments, we configure Docker volume mapping to persist our SQLite database:
yaml
docker-compose.yml
services:
server:
build: ./server
volumes:
- ./server/nexus.db:/app/nexus.db
Try it out!
Nexus is fully open-source (MIT), built on Bun + Hono + React, and ready to self-host. Check out the repository, run a local deploy, and try the Slack integration:
⭐ GitHub: https://github.com/Poi5eN/Nexus
🎯 Live Demo: https://saarlabs.in
Top comments (0)