Slack Bots with Bolt skill for Codex
Ack in three seconds, do the work afterwards. Codex builds Slack apps from a manifest kept in the repo, acknowledges every command, action and event within Slack's three-second window, and pushes slow work to the background. It picks Socket Mode or HTTP based on how you deploy, keeps scopes minimal, respects rate-limit tiers and Retry-After, and only posts to real channels after you confirm.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- slack-bots Installs to ~/.agents/skills/slack-bots/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install slack-bots The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project slack-bots Installs to .agents/skills/slack-bots/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/slack-bots.zipunzip -o slack-bots.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) slack-bots PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $slack-bots, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Builds Slack apps with Bolt for JavaScript or Python - app manifests, scopes, Socket Mode or HTTP events, slash commands, Block Kit messages, modals, interactive buttons, request signing and rate-limit handling. Use when creating a Slack bot or slash command, posting alerts or reports to channels, opening a modal from a shortcut, debugging dispatch_failed or operation_timeout errors, or moving a Slack app from Socket Mode to a public HTTP endpoint.
Use it when
- Creating a Slack bot, slash command, shortcut or Home tab
- Slack shows dispatch_failed or timeout errors on interactions
- Designing Block Kit messages or modals
- Moving a Slack app from one workspace to multi-workspace OAuth
Use something else when
- You need recurring reports sent to Slack on a schedule: use scheduled-reports
- The bot lives on Discord instead: use discord-bots
How to check the result
Every command, button and modal works in a test workspace, unit tests confirm ack runs before slow work, and the logs show no X-Slack-Retry-Num headers.
The mistake it prevents
Doing database or API work before calling ack(), so Slack times out after three seconds and retries or shows the user an error.
Done well means: every listener acknowledges within 3 seconds and does slow work afterwards,
the app requests the smallest set of scopes, every HTTP request is signature-verified (Bolt does
this when given the signing secret), messages include fallback text, and nothing posts to a
real workspace channel without the user’s confirmation.
Before writing code
- Read
AGENTS.mdand the package manifest; check for an existing Bolt app,manifest.ymlormanifest.json, and how the project is deployed (long-running server, container, Lambda). - Choose the transport:
| Situation | Transport |
|---|---|
| Internal tool, behind a firewall, or local development | Socket Mode (needs an app-level token xapp-... with connections:write) |
| Distributed to many workspaces, or Marketplace | HTTP Request URLs plus OAuth installation |
| Serverless (Lambda, Cloud Functions) | HTTP with processBeforeResponse: true (Bolt JS) |
- Secrets:
SLACK_BOT_TOKEN(xoxb-...),SLACK_SIGNING_SECRET,SLACK_APP_TOKEN(Socket Mode). Never commit them; never print tokens in logs. - Posting to real channels, DMing users, or installing to a production workspace is an outward-facing action: confirm with the user first. Use a test workspace for development.
Package installs and any Slack API call need network; in a Codex sandbox, ask for approval or
unit-test listeners with mocked client objects.
Manifest first
Keep the app definition in the repo so scopes and commands are reviewable:
display_information:
name: Deploy Bot
features:
bot_user:
display_name: deploybot
slash_commands:
- command: /deploy
description: Deploy a service
usage_hint: "[service] [env]"
settings:
socket_mode_enabled: true
interactivity:
is_enabled: true
event_subscriptions:
bot_events: [app_mention]
oauth_config:
scopes:
bot: [commands, chat:write, app_mentions:read]
Add scopes only when a method needs them (the API error names the missing scope). Every scope change requires reinstalling the app.
Listener pattern (Bolt JS)
import { App } from "@slack/bolt";
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.command("/deploy", async ({ ack, command, client, respond }) => {
await ack(); // within 3 s, before any slow work
const [service, env = "staging"] = command.text.trim().split(/\s+/);
if (!service) return respond({ response_type: "ephemeral", text: "Usage: /deploy <service> [env]" });
await client.views.open({ // trigger_id expires 3 s after the command
trigger_id: command.trigger_id,
view: confirmModal(service, env),
});
});
app.view("deploy_confirm", async ({ ack, view, body, client }) => {
const reason = view.state.values.reason.input.value ?? "";
if (reason.length < 5) {
return ack({ response_action: "errors", errors: { reason: "Give a short reason" } });
}
await ack();
const { service, env } = JSON.parse(view.private_metadata);
queueDeploy({ service, env, user: body.user.id, reason }); // slow work off the request path
});
app.event("app_mention", async ({ event, say }) => {
await say({ text: `Hi <@${event.user}>`, thread_ts: event.ts });
});
await app.start(Number(process.env.PORT) || 3000);
Python Bolt is equivalent: App(token=..., signing_secret=...), @app.command("/deploy")
with ack() first, and SocketModeHandler(app, app_token).start(). For async frameworks use
AsyncApp.
Block Kit essentials
await client.chat.postMessage({
channel,
text: `Deploy of ${service} to ${env} succeeded`, // fallback for notifications and screen readers
blocks: [
{ type: "section", text: { type: "mrkdwn", text: `*${service}* deployed to *${env}*` } },
{ type: "context", elements: [{ type: "mrkdwn", text: `by <@${userId}> · ${sha.slice(0, 7)}` }] },
{ type: "actions", elements: [
{ type: "button", action_id: "rollback", text: { type: "plain_text", text: "Roll back" },
style: "danger", value: JSON.stringify({ service, env, sha }) },
] },
],
});
- Limits: 50 blocks per message, 100 per modal or Home tab; button
valueup to 2,000 chars;private_metadataup to 3,000 chars. Store larger state server-side and pass an ID. - Escape user text placed in
mrkdwn: replace&,<,>with&,<,>. - Prototype layouts in Block Kit Builder, then paste the JSON into code.
- Update instead of re-posting:
chat.updatewith the messagetsfrom the original post.
Ack rules and timeouts
| Payload | Must ack within | If slow |
|---|---|---|
| Slash command, action, shortcut, view submission | 3 s | ack() first, then respond() (response_url valid 30 min) or chat.postMessage |
| Events API event | 3 s (HTTP 200) | Slack retries up to 3 times with X-Slack-Retry-Num; dedupe on event_id |
| Modal open | trigger_id valid 3 s | Open a loading view, then views.update |
On serverless, processBeforeResponse: true keeps the function alive until the handler
finishes, so keep handlers short or hand off to a queue.
Rate limits
- Web API methods have tiers;
chat.postMessageallows roughly one message per second per channel with short bursts. On HTTP 429, wait forRetry-Afterseconds. The Node and Python SDK clients retry 429s by default; do not add a second retry layer that ignores the header. - Since mid-2025,
conversations.historyandconversations.repliesare heavily limited for commercially distributed apps that are not Marketplace-approved. Check Slack’s current rate-limit page before designing features that read channel history. - Batch alerts: aggregate into one message per interval instead of one per event.
Gotchas
dispatch_failedor “operation_timeout”: the listener did not ack in time, threw before acking, or the Request URL is wrong. Check the server logs for the payload type.not_in_channel: the bot must be invited to a channel beforechat.postMessage(or usechat:write.publicfor public channels only).- Custom HTTP servers that parse JSON before Bolt break signature checks; let Bolt’s receiver
read the raw body, or verify
v0:+ timestamp +:+ raw body with HMAC-SHA256 yourself and reject timestamps older than five minutes. - User IDs, not names: mention with
<@U123>; display names change. - Enterprise Grid: IDs can be org-wide; store
team_idandenterprise_idwith installations. - Multi-workspace apps need an
installationStore; a singletokenonly works for one workspace.
Verification
- Run locally in Socket Mode against a test workspace; invoke each command, button and modal.
- Unit-test listeners by calling handlers with fake
ack,clientandrespondand assertingackwas called first. - Watch logs for
X-Slack-Retry-Numheaders; their presence means acks are too slow. - Before production install, review the manifest scopes with the user.
Report template
**App:** Deploy Bot, Bolt JS 4.x, Socket Mode (internal)
**Scopes:** commands, chat:write, app_mentions:read (manifest.yml in repo)
**Features:** /deploy -> confirm modal -> queued deploy; rollback button; mention reply in thread
**Ack strategy:** ack first, slow work queued; no listener exceeds 3 s in tests
**Tested in:** test workspace only; production install awaiting user approval What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Slack Bots with Bolt"
short_description: "Ack in three seconds, do the work afterwards"
default_prompt: "$slack-bots Build a Slack /deploy command that opens a confirmation modal before deploying" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.