AI & integrations

Slack AI bot

A two-way Slack AI bot inside a Next.js app: mentions, DMs and thread follow-ups answered by a model with tools scoped to the asking user, scheduled reports, and Approve and Cancel buttons that run a side effect exactly once.

Released
September 28, 2026
npx skills add timerise-ai/slack-ai-bot
v0.1.5
Current release
9
Reference docs
5
Non-negotiables
MIT
License

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() from next/server, waitUntil from @vercel/functions, or a queue.
  • chat and @chat-adapter/slack, a chat state adapter, ai and zod. 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.md requested 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.md spells 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.

  1. 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: BotHost has no id-only method; every lookup takes userId, 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:read and im: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 more
    • 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:user key, 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__, and 2 * 3 * 4 as 2 _ 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=1 with returnTo read out of state. Shipped: safeReturnPath, withParam, return path in the cookie.
    • Signature check could throw instead of rejecting String-length comparison before a byte-length-sensitive timingSafeEqual; parseInt accepted 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.
Read the full record in provenance.md

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.

  1. 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.

  2. 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.

  3. Never pick a bot token with limit(1).

    A token belongs to a workspace, not to whoever pressed Install, so slack_installations is keyed by team_id and 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.

  4. 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.

  5. 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-sdk skill. This skill uses the SDK, it does not document it
  • Model choice, streaming internals, tool-calling mechanicsInsteadThe ai-sdk skill
  • 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-sdk skill; 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-bot

Claude 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 directory
  • README.mdThis file
  • CHANGELOG.mdOne section per release, newest first
  • CLAUDE.mdThe editing conventions, for an agent editing this repository
  • LICENSEMIT
  • references/adaptation.mdThe seam contract: the BotHost interface, the rename table, the identity lookup and its trust decision, the host probe
  • references/data-model.mdslack_installations and slack_approvals, the store interfaces, the Postgres schema, and the conditional-update claim in four data layers
  • references/setup.mdSlack app configuration, environment, the requested scope list, OAuth install routes, chat state, the cached bot factory, the events route
  • references/conversation.mdIdentity resolution and its cache, origin parsing, the handlers, the user-scoped tools, the system prompt
  • references/outbound.mdThe Web API client and Slack's limits, report posting, the Markdown to mrkdwn converter, the strings block
  • references/approvals.mdThe approval service and its state machine, signature verification, the interactivity route
  • references/testing.mdThe suite, 31 tests, what each group proves, and the manual end-to-end script
  • references/operations.mdSymptom table, stuck approvals, uninstalls, what to make visible, logging, the go-live checklist
  • references/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 skill
  • evals/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

  1. 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.

  2. v0.1.4September 28, 2026

    Security fix release, from scoring the prompt-1 agent eval runs against 0.1.3.

  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.

  1. 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.

  2. Have the bot post a weekly report to a channel, and draft emails that a person approves in Slack before they are sent.

    Postgres
  3. Our 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.

  • Gemini CLI0.61.0

    gemini-3.8-flash

    Built, checks pass
    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

    Result fileAgent log

    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 in host.ts, failing loudly inside init() 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 the InstallationStore restriction.

  • Codex CLIcodex-cli 0.158.0

    gpt-6-astra

    Built, checks pass
    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

    Result fileAgent log

    What we saw

    Rubric 8/8. Scored from every turn's diff. It extracted every block, wrote only host.ts and 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.example lists 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.

  • Claude Code2.1.283

    claude-opus-5-5

    Built, checks pass
    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

    Result fileAgent log

    What we saw

    Rubric 8/8. Scored from the summary. It reports every piece of the file map in place with host.ts as 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 in host.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:

  1. Step 1

    Brief

    Tell us what the module must connect to. Takes minutes, in a chat.

  2. Step 2

    Prototype in 48 hours

    A clickable prototype of your system and a quote, at no cost.

  3. 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-bot

Build 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
3 years of support included.

Generated from the skill's own files at commit 5edc6f6. Every rule above links to where the repository says it. All skills