Slack
Judge messages in the channels and DMs your Slack app is in, then reply, react or post.
Jev Events reads each new message in the channels your Slack app is in, asks Jev your questions about it, and runs what you pick on the answers. Choose below, and the code and the steps to run it fill in.
With your own Slack, on your computer. The quickest way to try it.
What to watch
New messages. Messages in the channels and DMs the app is in. Invite it with /invite @your-app.
What to ask
Jev answers every question about each message, in one request.
What to do
Each rule runs when an answer is likely enough. Leave the rules out to print every answer.
Posts what fired, who wrote the message and where, with a link to it.
Reacts to the message with an emoji, by its name without colons.
The code
import { monitor, recipes } from "jev-events";import { slack } from "@jev-events/slack";const team = monitor({ // Also judges the 5 latest messages each time it starts, so there's something to see right away. source: slack.messages({ backfill: 5 }), questions: { urgent: recipes.team.urgent, needsAnswer: recipes.team.needsAnswer, }, // Native actions only log what they would do. Set dryRun: false once the answers look right. dryRun: true,}) .on("urgent", { min: 0.9 }, slack.post("#incidents")) .on("needsAnswer", { min: 0.8 }, slack.react("eyes"));await team.start();console.log("Watching for new messages. Stop with Ctrl-C.");Run it
- 1
Make a folder for it, and install Jev Events and
tsx, which runs TypeScript. Jev Events needs Node.js 22 or newer.mkdir my-monitor && cd my-monitornpm init -y && npm pkg set type=modulenpm i jev-events @jev-events/slack tsx - 2
Create your own Slack app and add it to your workspace: the command walks you through it. Then invite the app to a channel with
/invite @jev_events.npx jev-events auth slack - 3
Put your TypeSafe API key in
.env.echo "TYPESAFE_API_KEY=<your key>" >> .env - 4
Save the code as
monitor.ts, and run it.npx tsx --env-file=.env monitor.ts - 5
Native actions log a
[dry-run] would …line instead of running. When those look right, setdryRun: false.
Try it without code
To see what Jev says about your own workspace before writing anything, install the integration, set up the app once and watch. It prints an answer for the 5 latest messages, then for each new one:
npm i jev-events @jev-events/slack
npx jev-events auth slack
npx jev-events watch slackwatch slack:general reads one channel. By default it asks whether each message needs an answer, whether it's urgent, and what kind of message it is. Ask your own with --ask "bug=Is this reporting a bug?". See the CLI.
The first time, auth slack prints a link that creates the Slack app with everything filled in. Install it to your workspace, paste the two tokens it asks for, and invite the app to a channel with /invite @jev_events.
The source
slack.messages() emits each new message in the channels, private channels and DMs the app is in.
| Option | Default | What it does |
|---|---|---|
channels | Every conversation the app is in | Only these, by name ("general" or "#general") or ID |
backfill | 0 | Also emits this many of the latest messages when starting |
includeBots | false | Also judges messages from bots and integrations |
Messages arrive in one of two ways, and the builder sets up the right one:
| Socket Mode | Events API | |
|---|---|---|
| Used | On your machine, with auth slack | In your web app, for your users |
| Needs | The app-level token (xapp-…) | The signing secret, and your site at a public URL |
| Messages arrive | Over a WebSocket the process keeps open | As a POST to /api/jev/webhook/slack |
A web app can also use Socket Mode for every workspace that added your app, over one connection: pass slack.app({ appToken }) or set SLACK_APP_TOKEN, and run jev.start() in a process that stays up.
Actions
| Action | What it does |
|---|---|
slack.reply(text, { broadcast? }) | Replies in the message's thread, or straight into a DM. broadcast also posts the reply in the channel. |
slack.react(emoji) | Adds a reaction, such as slack.react("eyes"). |
slack.post(channel, text?) | Posts in another channel. By default it says what fired and who wrote what where, with a link to the message. |
text can be a string or a function of the event, such as (e) => `<@${e.item.author.id}> on it` , and Slack formatting such as *bold* works. When the app isn't in a channel or lacks a scope, the error says what to type or click to fix it. Actions are dry-run until you pass dryRun: false to monitor().
Messages
Handlers receive a SlackMessageItem:
| Field | Type | Notes |
|---|---|---|
text | string | Mentions shown as names, files as [file: name] |
author | { id, name, guest, external, bot } | external means another company, through Slack Connect |
channel | { id, name, kind } | kind is channel, private, dm or group-dm |
inThread | boolean | A reply in a thread |
mentionsYou | boolean | It @-mentions the app |
mentionsEveryone | boolean | It uses @here, @channel or @everyone |
permalink | string | A link to the message in Slack |
files | { name, type? }[] |
Jev sees the text and the author's name, plus facts: the channel, and whether it's in a thread, mentions the app or everyone, or comes from a guest, a bot or another company.
Good to know
- Skipped before judging. Joins, edits, the app's own posts and other bots are never judged, so they cost nothing.
- Slack wants an answer within 3 seconds. In a web app, the runtime answers Slack first and judges the message after, through
waitUntil: afterfromnext/server. Keep the route'smaxDurationlong enough to judge. - Retries. A message that fails for a reason that may pass, such as Slack being down, answers an error so Slack sends it again: 3 times over about 5 minutes. A workspace that removed the app answers OK and is marked
needs-sign-in. - Keep token rotation off in your app's settings. The saved bot token has to keep working.
- A server with one workspace of your own.
slack.fromEnv()builds the connection fromSLACK_BOT_TOKENandSLACK_APP_TOKEN:await team.start({ connections: [slack.fromEnv()] }). - Your own manifest.
manifestUrl({ name, requestUrl, redirectUrls })makes the link that creates the app with everything filled in, andmanifest()gives it as an object.