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 workspace, on your computer. The quickest way to try it.
import { monitor, recipes } from "jev-events";
import { slack } from "@jev-events/slack";
const team = monitor({
// Also judges the 5 latest messages, so there's something to see right away.
source: slack.messages({ backfill: 5 }),
questions: {
urgent: recipes.team.urgent,
},
// Native actions only log what they would do until you set dryRun: false.
dryRun: true,
})
.on("urgent", { min: 0.8 }, slack.post("#incidents"));
await team.start();
console.log("Watching for new messages. Stop with Ctrl-C.");
Run it
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-monitor npm init -y && npm pkg set type=module npm i jev-events @jev-events/slack tsx
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
Put your TypeSafe API key in .env.
echo "TYPESAFE_API_KEY=<your key>" >> .env
Save the code as monitor.ts, and run it.
npx tsx --env-file=.env monitor.ts
Native actions log a [dry-run] would … line instead of running. When those look right, set dryRun: 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: after from next/server. Keep the route's maxDuration long 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 from SLACK_BOT_TOKEN and SLACK_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, and manifest() gives it as an object.