Skip to Content
IntegrationsSlack Bot

Slack Bot

Build a Slack bot that lets your team submit AI prompts and receive results directly in Slack channels or threads.

Testing tip — Build and verify this integration end-to-end with convoy-mock first. It’s a free, synthetic model that returns a callback in ~60 seconds and is never billed. Set DEFAULT_MODEL=convoy-mock while wiring the bot up, then switch to claude-3-haiku once the Slack flow works.

Use Case: Team AI Assistant

Send a message in Slack, get an AI-generated response back in the same thread. Perfect for:

  • Content teams — “Draft a blog intro about our new feature” → AI response in-thread
  • Support teams — “Summarize this customer issue and suggest a response” → AI drafts a reply
  • Engineering teams — “Write a PR description for these changes” → AI generates the description
  • Anyone — Quick AI-powered writing, summarization, or analysis without leaving Slack
┌──────────────┐ ┌──────────────┐ ┌─────────┐ │ Slack User │──msg──▶ │ Your Bot │──POST──▶│ Convoy │ │ @convoy │ │ (Server) │ │ API │ └──────────────┘ └──────┬───────┘ └────┬────┘ │ │ │ (minutes–hours later)│ │ │ ┌──────▼───────┐ ┌────▼────┐ │ Webhook │◀──POST──│ Convoy │ │ Endpoint │ │ Worker │ └──────┬───────┘ └─────────┘ ┌──────▼───────┐ │ Slack API │ │ (Reply) │ └──────────────┘

Since Convoy processes in batches (minutes to hours, up to 24-hour SLA), this bot is best for background tasks like content generation, not real-time chat. The bot acknowledges immediately and replies when the result is ready.


Prerequisites

  • A Convoy account with a project API key (convoy_sk_...)
  • A Slack workspace where you can install apps
  • Node.js 18+ or Python 3.10+
  • A publicly accessible server (or ngrok for development)

Step 1: Create a Slack App

  1. Go to api.slack.com/apps  and click Create New App
  2. Choose From scratch
  3. Name it “Convoy AI” and select your workspace
  4. Click Create App

Configure Bot Permissions

  1. Go to OAuth & Permissions in the sidebar

  2. Under Bot Token Scopes, add:

    • app_mentions:read — Detect when users mention the bot
    • chat:write — Send messages
    • channels:history — Read channel messages (for thread context)
    • groups:history — Read private channel messages
  3. Click Install to Workspace and authorize

  4. Copy the Bot User OAuth Token (starts with xoxb-)

Enable Event Subscriptions

  1. Go to Event Subscriptions in the sidebar
  2. Toggle Enable Events to ON
  3. Set the Request URL to your server’s endpoint:
    https://your-server.com/slack/events
  4. Under Subscribe to bot events, add:
    • app_mention — Triggers when someone @mentions your bot
  5. Click Save Changes

Step 2: Build the Bot Server

Node.js Implementation

mkdir convoy-slack-bot && cd convoy-slack-bot npm init -y npm install @slack/bolt dotenv
// app.js require('dotenv').config(); const { App } = require('@slack/bolt'); const app = new App({ token: process.env.SLACK_BOT_TOKEN, signingSecret: process.env.SLACK_SIGNING_SECRET, port: process.env.PORT || 3000, }); // Store pending requests to match callbacks to Slack threads const pendingRequests = new Map(); // Handle @convoy mentions app.event('app_mention', async ({ event, say }) => { // Extract the prompt (remove the @mention) const prompt = event.text.replace(/<@[A-Z0-9]+>/g, '').trim(); if (!prompt) { await say({ text: '👋 Hi! Mention me with a prompt and I\'ll generate a response. Example:\n`@Convoy AI Write a blog intro about serverless computing`', thread_ts: event.ts, }); return; } // Acknowledge immediately await say({ text: `⏳ Working on it! I'll reply here when the result is ready (usually minutes to hours).`, thread_ts: event.ts, }); // Submit to Convoy try { const response = await fetch(`${process.env.CONVOY_BASE_URL}/cargo/load`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': process.env.CONVOY_API_KEY, }, body: JSON.stringify({ params: { model: process.env.DEFAULT_MODEL || 'claude-3-haiku', max_tokens: 2048, messages: [{ role: 'user', content: prompt }], }, callback_url: `${process.env.PUBLIC_URL}/convoy/callback`, }), }); const data = await response.json(); if (!response.ok) { await say({ text: `❌ Failed to submit: ${data.detail || 'Unknown error'}`, thread_ts: event.ts, }); return; } // Store the mapping: cargo_id → Slack thread info pendingRequests.set(data.cargo_id, { channel: event.channel, thread_ts: event.ts, user: event.user, prompt: prompt.slice(0, 100), }); console.log(`Submitted ${data.cargo_id} for user ${event.user}`); } catch (error) { console.error('Error submitting to Convoy:', error); await say({ text: `❌ Error connecting to Convoy: ${error.message}`, thread_ts: event.ts, }); } }); // Handle Convoy callbacks app.receiver.app.post('/convoy/callback', async (req, res) => { // Collect the body let body = ''; req.on('data', chunk => { body += chunk; }); req.on('end', async () => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ received: true })); try { const payload = JSON.parse(body); const { cargo_id, success, response, error } = payload; const request = pendingRequests.get(cargo_id); if (!request) { console.warn(`No pending request found for ${cargo_id}`); return; } pendingRequests.delete(cargo_id); if (success) { const text = response.content[0].text; const model = response.model; const tokens = response.usage.output_tokens; await app.client.chat.postMessage({ channel: request.channel, thread_ts: request.thread_ts, text: text, blocks: [ { type: 'section', text: { type: 'mrkdwn', text: text }, }, { type: 'context', elements: [ { type: 'mrkdwn', text: `_${model} · ${tokens} tokens · <@${request.user}>_`, }, ], }, ], }); } else { await app.client.chat.postMessage({ channel: request.channel, thread_ts: request.thread_ts, text: `❌ Processing failed: ${error}`, }); } } catch (err) { console.error('Error processing callback:', err); } }); }); (async () => { await app.start(); console.log('⚡️ Convoy Slack bot is running!'); })();

.env file:

SLACK_BOT_TOKEN=xoxb-your-bot-token SLACK_SIGNING_SECRET=your-signing-secret CONVOY_API_KEY=convoy_sk_your_key_here CONVOY_BASE_URL=https://api.cnvy.ai PUBLIC_URL=https://your-server.com DEFAULT_MODEL=claude-3-haiku PORT=3000

Python Implementation

pip install slack-bolt flask httpx python-dotenv
# app.py import os import json from dotenv import load_dotenv from slack_bolt import App from slack_bolt.adapter.flask import SlackRequestHandler from flask import Flask, request, jsonify import httpx load_dotenv() app = App( token=os.environ["SLACK_BOT_TOKEN"], signing_secret=os.environ["SLACK_SIGNING_SECRET"], ) flask_app = Flask(__name__) handler = SlackRequestHandler(app) # Store pending requests pending_requests: dict[str, dict] = {} CONVOY_API_KEY = os.environ["CONVOY_API_KEY"] CONVOY_BASE_URL = os.environ.get("CONVOY_BASE_URL", "https://api.cnvy.ai") PUBLIC_URL = os.environ["PUBLIC_URL"] DEFAULT_MODEL = os.environ.get("DEFAULT_MODEL", "claude-3-haiku") @app.event("app_mention") def handle_mention(event, say): """Handle @convoy mentions.""" # Extract prompt (remove @mention) import re prompt = re.sub(r"<@[A-Z0-9]+>", "", event["text"]).strip() if not prompt: say( text="👋 Mention me with a prompt! Example:\n`@Convoy AI Write a blog intro about AI`", thread_ts=event["ts"], ) return # Acknowledge immediately say( text="⏳ Working on it! I'll reply here when ready (minutes to hours).", thread_ts=event["ts"], ) # Submit to Convoy try: response = httpx.post( f"{CONVOY_BASE_URL}/cargo/load", headers={ "Content-Type": "application/json", "X-API-Key": CONVOY_API_KEY, }, json={ "params": { "model": DEFAULT_MODEL, "max_tokens": 2048, "messages": [{"role": "user", "content": prompt}], }, "callback_url": f"{PUBLIC_URL}/convoy/callback", }, ) response.raise_for_status() data = response.json() pending_requests[data["cargo_id"]] = { "channel": event["channel"], "thread_ts": event["ts"], "user": event["user"], } print(f"Submitted {data['cargo_id']} for user {event['user']}") except Exception as e: say(text=f"❌ Error: {e}", thread_ts=event["ts"]) @flask_app.route("/convoy/callback", methods=["POST"]) def convoy_callback(): """Receive results from Convoy.""" payload = request.get_json() cargo_id = payload["cargo_id"] req_info = pending_requests.pop(cargo_id, None) if not req_info: return jsonify({"received": True, "warning": "unknown cargo_id"}) if payload["success"]: text = payload["response"]["content"][0]["text"] model = payload["response"]["model"] tokens = payload["response"]["usage"]["output_tokens"] app.client.chat_postMessage( channel=req_info["channel"], thread_ts=req_info["thread_ts"], text=text, blocks=[ {"type": "section", "text": {"type": "mrkdwn", "text": text}}, { "type": "context", "elements": [ { "type": "mrkdwn", "text": f"_{model} · {tokens} tokens · <@{req_info['user']}>_", } ], }, ], ) else: app.client.chat_postMessage( channel=req_info["channel"], thread_ts=req_info["thread_ts"], text=f"❌ Processing failed: {payload['error']}", ) return jsonify({"received": True}) @flask_app.route("/slack/events", methods=["POST"]) def slack_events(): return handler.handle(request) if __name__ == "__main__": flask_app.run(port=int(os.environ.get("PORT", 3000)))

Step 3: Deploy and Configure

Local Development (ngrok)

# Start your bot node app.js # or: python app.py # In another terminal, expose it ngrok http 3000

Copy the ngrok URL (e.g., https://abc123.ngrok.io) and:

  1. Set PUBLIC_URL=https://abc123.ngrok.io in your .env
  2. Update the Slack Event Subscriptions Request URL to https://abc123.ngrok.io/slack/events

Production Deployment

Deploy to any platform that supports long-running Node.js/Python processes:

  • Railway / Render — Simple PaaS deployment
  • AWS ECS — If you’re already running Convoy on AWS
  • Fly.io — Global edge deployment

For production, use a database (Redis, PostgreSQL) instead of an in-memory Map to store pending requests. This ensures callbacks are handled correctly even if the bot restarts.


Step 4: Using the Bot

Once deployed, invite the bot to a channel:

/invite @Convoy AI

Then mention it with any prompt:

@Convoy AI Write a professional email declining a meeting invitation politely

The bot will:

  1. Acknowledge immediately with ⏳
  2. Submit the prompt to Convoy
  3. Reply in the same thread when the result arrives (minutes to hours)

Advanced Features

Model Selection with Slash Commands

Add a slash command for model control:

  1. In your Slack app settings, go to Slash Commands
  2. Create /convoy with the request URL https://your-server.com/slack/commands
app.command('/convoy', async ({ command, ack, say }) => { await ack(); // Parse: /convoy [model] prompt const parts = command.text.split(' '); let model = process.env.DEFAULT_MODEL; let prompt = command.text; const validModels = ['claude-3-haiku', 'claude-3-sonnet', 'claude-3-opus']; if (validModels.includes(parts[0])) { model = parts[0]; prompt = parts.slice(1).join(' '); } // Submit to Convoy with the selected model // ... (same as app_mention handler but with model parameter) });

Usage:

/convoy claude-3-sonnet Write a detailed product roadmap for Q3 /convoy Write a quick tweet about our launch

System Prompts per Channel

Configure different system prompts for different channels:

const channelPrompts = { 'C01MARKETING': 'You are a marketing copywriter. Be engaging and concise.', 'C02SUPPORT': 'You are a customer support specialist. Be empathetic and helpful.', 'C03ENGINEERING': 'You are a senior software engineer. Be technical and precise.', }; app.event('app_mention', async ({ event, say }) => { const systemPrompt = channelPrompts[event.channel] || 'You are a helpful assistant.'; // Include system prompt in the Convoy request... });

Thread Context

Include previous messages in the thread for context-aware responses:

app.event('app_mention', async ({ event, say, client }) => { // Fetch thread history for context let messages = []; if (event.thread_ts) { const result = await client.conversations.replies({ channel: event.channel, ts: event.thread_ts, limit: 10, }); messages = result.messages .filter(m => !m.bot_id) // Exclude bot messages .map(m => ({ role: 'user', content: m.text.replace(/<@[A-Z0-9]+>/g, '').trim(), })); } else { messages = [{ role: 'user', content: prompt }]; } // Submit with full conversation context // ... });

Usage Tracking

Track usage per user or channel:

// After receiving a callback const usage = response.usage; console.log(JSON.stringify({ event: 'convoy_result', user: request.user, channel: request.channel, model: response.model, input_tokens: usage.input_tokens, output_tokens: usage.output_tokens, cargo_id: cargo_id, }));

Troubleshooting

Bot doesn’t respond to mentions

  • Verify the bot is invited to the channel (/invite @Convoy AI)
  • Check Event Subscriptions are enabled and the URL is verified
  • Ensure app_mentions:read scope is granted
  • Check your server logs for incoming events

”not_authed” or “invalid_auth” errors

  • Verify SLACK_BOT_TOKEN starts with xoxb-
  • Re-install the app to your workspace if the token was regenerated

Callback never arrives in Slack

  • Check that PUBLIC_URL is correct and publicly accessible
  • Verify the Convoy request was accepted (check for cargo_id in logs)
  • Use the tracking endpoint to check if the cargo is still processing
  • Ensure your callback endpoint returns 200 within 30 seconds

Messages are too long for Slack

  • Slack has a 4,000 character limit per message block
  • Split long responses into multiple messages:
function splitMessage(text, maxLength = 3900) { const chunks = []; while (text.length > 0) { chunks.push(text.slice(0, maxLength)); text = text.slice(maxLength); } return chunks; }

Rate limiting

  • Slack API: 1 message per second per channel
  • Convoy API: 60 requests per minute per project
  • Add queuing if your team generates many simultaneous requests
Last updated on