The problem
What problem does Slack AI bot solve?
Teams already live in Slack. The questions they ask about a system, how many bookings came in today, which invoices are overdue, what this customer's history looks like, should be answerable there, from the system's own data. And some actions, like sending an email or issuing a refund, should be possible from Slack too, but only after a person has approved them.
The chat loop is the easy part. Slack is a stateless, retrying client with a three-second deadline, and the model is an untrusted caller. Every id the model passes into a tool came from whoever was talking in the thread. Every webhook may arrive twice, on a cold instance. A bot that gets this wrong answers twice, forgets the thread it was following, or runs a tool for the wrong user.
This skill gives a coding agent the bot the way we build it: identity resolved from the Slack profile to an app account, tools scoped to that account, a shared state store so follow-ups survive cold starts, raw-body signature verification, an immediate acknowledgement with the work finished afterwards, and Approve and Cancel buttons whose click runs the side effect exactly once.
The module
What does the skill build?
An agent with this skill builds one module for a Next.js App Router app, on your stack. For Slack AI bot that module consists of:
- 1. Mentions, DMs and thread follow-ups answered by a model with tools scoped to the asking user
- 2. App-initiated reports
- 3. Approve and Cancel buttons whose click runs the side effect exactly once
- 4. Raw-body signature verification
- 5. 3-second ack
What it needs from you
- Next.js App Router on a Node runtime, with a way to run work after the response:
after()fromnext/server,waitUntilfrom@vercel/functions, or a queue. chatand@chat-adapter/slack, a chat state adapter,aiandzod. In production the state adapter must be shared, Redis or Postgres: with in-memory state every cold start forgets which threads the bot follows.- A Slack app you control, distributed by OAuth, with the scope list in
references/setup.mdrequested by the authorize URL rather than only listed on the config page. - A users table that can be queried by lower-cased email in one indexed lookup. The Slack profile email is the identity bridge, and
references/adaptation.mdspells out the trust decision that implies and what to do instead when it is not acceptable.
Provenance
Where do the rules come from?
This skill was written by the engineer who has shipped this module. The earlier implementation it was audited against was the Slack assistant of a product on Next.js 16, Vercel and Supabase. The templates hold the properties a two-way bot has to hold: every answer and every tool call scoped to the person who asked, one side effect per approved click however often the button is clicked, both webhooks acknowledged inside Slack's three seconds, a bot token chosen by the workspace the event came from, and a failure the person in the thread can see. The suite in `references/testing.md` states each one; `references/provenance.md` has the record.
The engineering ledger behind the templates, for the person editing this skill. It keeps three things apart: what the audit of the earlier implementation changed and how the templates verify it, what was kept deliberately and why it is safe, and what was designed here and has never run in production.
The ledger separates what the audit changed, what was kept on purpose, and what has not run in production yet.
Fixed in the templates
- Two read tools returned any tenant's data by id Both queried with the service-role client and never checked that the asking user could see the record. The tool that ran the action did check. Shipped:
BotHosthas no id-only method; every lookup takesuserId, see conversation.md, adaptation.md. - The install flow did not request the scopes identity depends on The authorize URL omitted
users:read.email,app_mentions:read,im:readandim:write, though the setup docs listed them. Tokens from the app's own Connect button cannot read emails, so no user can be matched. Shipped: one exported scope list, with a test, see setup.md. - In-memory chat state in a serverless deployment Subscriptions, dedupe and locks were per-instance. Thread follow-ups are dropped whenever they land on an instance that did not see the first mention. Shipped: state is a seam with production adapters named, see setup.md.
- Approve could send twice Status was checked in application code, the email sent, then status written. The send also ran before Slack's 3-second ack, which invites the second click. Shipped: conditional-update claim; ack before work, see approvals.md.
- Initialization race on cold start A boolean was set before the awaits. A second webhook during init skipped it and reached a bot with no handlers registered. Shipped: cached promise, cleared on failure, see setup.md.
Show 9 moreShow fewer
- Identity lookup capped at 1000 users, cache never expired
listUsers({ perPage: 1000 })filtered in memory; positive matches cached for the life of the instance, keyed without the workspace. Shipped: single-query contract,team:userkey, TTL, see conversation.md. - Bot token chosen by unordered limit(1) Tokens were stored per
(user, provider, team); several lookups took "the first" row, one of them with no team filter at all. Shipped: one row per workspace, see data-model.md. - Failures were silent Handler errors were logged and swallowed; refused or failed button clicks returned 200 with no feedback. Shipped: failure message in thread; ephemeral reply via
response_url. - Bots and non-users triggered identity-failure replies Only the bot's own messages were ignored. Any other bot, and every colleague without an account in a followed thread, got the "couldn't match" reply. Shipped: bot authors ignored; silent in followed threads.
- Markdown converter corrupted bold headings, bold-italic and arithmetic Verified by running it:
# **Title**came out as**Title**,***x***as__x__, and2 * 3 * 4as2 _ 3 _ 4. Shipped: placeholder-parking converter with regression tests, see outbound.md. - OAuth return path was an open redirect and broke on query strings
${appUrl}${returnTo}?flag=1withreturnToread out ofstate. Shipped:safeReturnPath,withParam, return path in the cookie. - Signature check could throw instead of rejecting String-length comparison before a byte-length-sensitive
timingSafeEqual;parseIntaccepted junk timestamps. Shipped: byte-length guard, digit check, tests, see approvals.md. - Progress reaction used a custom emoji
:loading:exists only where someone uploaded it; elsewhere the call failed silently. Shipped:hourglass_flowing_sand. - Smaller items Emails and message text in logs;
logger: "debug"in production; empty-string defaults for missing team and channel ids; a dual write of installations into chat state; two exported helpers (isChannelMonitored,resolveTokenForTeam) that nothing called.
- Two read tools returned any tenant's data by id Both queried with the service-role client and never checked that the asking user could see the record. The tool that ran the action did check. Shipped:
Non-negotiables
What are the 5 rules the module never breaks?
Every module built from this skill holds these, whoever builds it. The same list is in the skill's README and SKILL.md, so the agent reads it before it writes a line.
Never put a payload in a button
value.Everyone in the channel can read what a button carries, and a client can send any value back. The button carries an opaque approval id and the rest is loaded server-side. A test asserts the posted blocks contain the id and not the payload.
Never authorize a click by channel membership.
Anyone who can see a message can click it, so the clicker is resolved to an app user and checked against the resource, every time. A test clicks as an outsider and asserts the request is refused and still pending.
Never pick a bot token with
limit(1).A token belongs to a workspace, not to whoever pressed Install, so
slack_installationsis keyed byteam_idand the team comes off the event. One row per workspace is the shape the schema enforces, and the OAuth callback writes it: a token is never an environment variable, even for a single workspace.Never parse the body before verifying the signature.
The HMAC is over the raw bytes, so a re-serialized body never matches.
request.text()comes first, and the comparison is over byte lengths, so a forged multi-byte signature returns false instead of throwing. Both are in the suite.Never let a failure be silent.
After the eyes reaction, silence reads as "still working". A thrown handler posts into the thread, and a refused or failed click answers the clicker through
response_url.
Fit
When should you use it, and when not?
Use it for
- A product needs a Slack assistant that answers from the user's own data.
- The app must start conversations: reports, alerts, drafts for review.
- An action is too risky to let a model fire: send email, publish, pay, delete.
- An existing bot loses thread follow-ups, double-sends, or fails identity.
Not for
- Learning the Chat SDK: cards, modals, other platformsInsteadThe
chat-sdkskill. This skill uses the SDK, it does not document it - Model choice, streaming internals, tool-calling mechanicsInsteadThe
ai-sdkskill - Reading channel history as a data sourceInsteadA history module of its own: history scopes, pagination and user-name resolution are not here
- One-way notifications with no reply pathInsteadA Slack incoming webhook URL. No bot user, no tokens table, no webhooks to verify
- Slash commands or modals as the main interfaceInsteadThe
chat-sdkskill; this module is mentions, DMs, threads and buttons
Build it yourself
How do I install it?
One command. The skills.sh CLI installs the skill into every skills-compatible agent it finds.
$ npx skills add timerise-ai/slack-ai-botClaude Code
Invoke with /slack-ai-bot
Codex CLI
Invoke with $slack-ai-bot
Gemini CLI
Invoke with /skills
Name the agents instead with -a, for example npx skills add timerise-ai/slack-ai-bot -a claude-code -a codex. Or clone the repository into your agent's skills folder. Nothing in it is agent-specific.
What is inside the repository (16 entries)
SKILL.mdEntry point: architecture diagram, critical facts, hard rules, quick start, and the reference directoryREADME.mdThis fileCHANGELOG.mdOne section per release, newest firstCLAUDE.mdThe editing conventions, for an agent editing this repositoryLICENSEMITreferences/adaptation.mdThe seam contract: theBotHostinterface, the rename table, the identity lookup and its trust decision, the host probereferences/data-model.mdslack_installationsandslack_approvals, the store interfaces, the Postgres schema, and the conditional-updateclaimin four data layersreferences/setup.mdSlack app configuration, environment, the requested scope list, OAuth install routes, chat state, the cached bot factory, the events routereferences/conversation.mdIdentity resolution and its cache, origin parsing, the handlers, the user-scoped tools, the system promptreferences/outbound.mdThe Web API client and Slack's limits, report posting, the Markdown to mrkdwn converter, the strings blockreferences/approvals.mdThe approval service and its state machine, signature verification, the interactivity routereferences/testing.mdThe suite, 31 tests, what each group proves, and the manual end-to-end scriptreferences/operations.mdSymptom table, stuck approvals, uninstalls, what to make visible, logging, the go-live checklistreferences/provenance.mdThe engineering ledger: what the audit changed and how the templates verify it, what was kept on purpose, and what is new in the skillevals/The prompts an operator types after installing (prompts.md) and one file per agent eval: the skill installed into an empty Next.js app, one prompt, no help, then type-checked, built and tested.github/workflows/agent-eval.ymlThe caller of the index's reusable eval workflow, run on every published release and on a maintainer's dispatch
Recent releases
- v0.1.5September 28, 2026
Wording release, from scoring the prompt-1 agent eval runs against 0.1.4. The templates and the suite are unchanged.
- v0.1.4September 28, 2026
Security fix release, from scoring the prompt-1 agent eval runs against 0.1.3.
- v0.1.3September 28, 2026
Fix release, from scoring the prompt-1 agent eval runs against 0.1.2.
After installing
What do I tell my agent?
Say what you need in your own words; the skill supplies the how. These are starting points, and the ones we tested say how it went.
Add a Slack bot to this Next.js app that answers mentions and DMs from the asking user's own data and keeps the thread for follow-up questions.
Have the bot post a weekly report to a channel, and draft emails that a person approves in Slack before they are sent.
PostgresOur Slack bot sometimes answers twice and loses the thread. Find out why.
Tested
How does it do in each agent?
We install the skill into an empty Next.js app, give the agent one of the prompts above and no further help, then type-check, build and run the tests it left behind. Nothing is fixed by hand before the checks, and a failing run is published like a passing one. The procedure and every result are public, and the first prompt runs again before each release.
- Built, checks pass
Gemini CLI0.61.0
gemini-3.8-flash
Add a Slack bot to this Next.js app that answers mentions and DMs from the asking user's own data and keeps the thread for follow-up questions.
Typecheck: passBuild: passTests: pass- Time
- 8 min
- Changed
- 26 files, +5,249 lines
- Stack
- Postgres
- Skill
- v0.1.5
- Run
- Sep 28, 2026
What we saw
Rubric 8/8. Scored from the summary only; the Gemini CLI was not available for a local rerun, so items 2 and 6 rest on its account. It lists the whole file map,
createPostgresState()unconditionally inhost.ts, failing loudly insideinit()without the variable, tokens stored per workspace by the OAuth install and never read from the environment, the six variables setup.md names, empty and tracked, and the suite under vitest reporting 31. The handover names every demo body and the 401 install, the state adapter and its variable, and the email trust decision with theInstallationStorerestriction. - Built, checks pass
Codex CLIcodex-cli 0.158.0
gpt-6-astra
Add a Slack bot to this Next.js app that answers mentions and DMs from the asking user's own data and keeps the thread for follow-up questions.
Typecheck: passBuild: passTests: pass- Time
- 7 min
- Changed
- 40 files, +5,867 lines
- Stack
- Postgres
- Skill
- v0.1.5
- Run
- Sep 28, 2026
What we saw
Rubric 8/8. Scored from every turn's diff. It extracted every block, wrote only
host.tsand its own modules beside the templates (resources, stores, Supabase clients), and ended with its own assertion that every shipped module and the suite match the skill byte for byte, the vitest import aside; no turn's diff touches a template. Chat state is Postgres, unconditionally; tokens come from the OAuth install;.env.examplelists setup.md's variables and the host's Supabase ones, empty and tracked. The suite reports 31 beside ten tests of its own, and the handover names state, data and the email trust decision limited to approved workspaces. - Built, checks pass
Claude Code2.1.283
claude-opus-5-5
Add a Slack bot to this Next.js app that answers mentions and DMs from the asking user's own data and keeps the thread for follow-up questions.
Typecheck: passBuild: passTests: pass- Time
- 3 min
- Changed
- 29 files, +5,485 lines
- Stack
- Postgres
- Skill
- v0.1.5
- Run
- Sep 28, 2026
What we saw
Rubric 8/8. Scored from the summary. It reports every piece of the file map in place with
host.tsas the one app-specific file, Postgres chat state with no memory fallback, tokens per workspace from the install flow, the Supabase variables beside setup.md's in an un-ignored.env.example, the model as a string inhost.ts, and the suite under vitest with only its import changed, reporting 31. The one change it names as its own, escaping resource names in approval summaries so<!channel>cannot ping, is in the summary text the host builds. The handover names the 401 install, the placeholder data, the state adapter and its variable, and the email trust decision.
Build it with Timerise
How long does it take, and what does it cost?
We quote this module per project. The price depends on what it has to connect to. The path to a number is short and free:
Step 1
Brief
Tell us what the module must connect to. Takes minutes, in a chat.
Step 2
Prototype in 48 hours
A clickable prototype of your system and a quote, at no cost.
Step 3
Build and handoff
One project price. Source code, documentation and IP are yours.
Two ways to get Slack AI bot
Build it yourself
Install the skill. Your own agent builds the module.
- MIT licensed, no strings
- Runs in Claude Code, Codex CLI and Gemini CLI
- The same rules our engineers build by
$ npx skills add timerise-ai/slack-ai-botBuild it with Timerise
Send a brief. We build Slack AI bot into a system you own.
- Clickable prototype and a quote within 48 hours, free
- One project price, no subscription, no commission
- Source code, documentation and IP handed over
Generated from the skill's own files at commit 5edc6f6. Every rule above links to where the repository says it. All skills