Workflows / JevModel

Slack Message Classification: Route Channel Messages with the Events API and Jev

Classify messages in a Slack help or feedback channel with a small Slack app: receive message.channels events, verify the signature, acknowledge within 3 seconds, then ask Jev a fixed Choice plus an urgency Noul and react, reply, or forward. Includes retries, deduplication, privacy, and when a no-code tool is enough.

Last updated:

Quick answer: acknowledge first, classify second

  • A Slack app subscribed to the message.channels bot event (scope channels:history) receives one HTTP POST per message in public channels the bot has joined. Verify the X-Slack-Signature header with your signing secret, then return HTTP 200 within 3 seconds; Slack treats slower responses as failures and retries up to 3 times.
  • After acknowledging, send only the message text to JevModel’s /api/v1/systemone with a Choice of 2–12 labels you define (for example bug, access, how_to, other) and a Noul for “blocked right now?”. Use the returned probabilities to react with an emoji, reply in thread, or forward the message, and send uncertain ones to a person.
  • Deduplicate on event_id, ignore bot messages and edits, and never delete a message because of a label. If you already run Zapier, Make, n8n, Power Automate, or Pipedream, their Slack triggers can call the same Jev API without hosting a server.

What does Slack message classification mean in practice?

Most teams have at least one channel where unrelated requests pile up: #help, #it-support, #customer-feedback, #sales-questions. Classification means reading each new message and attaching one label from a short, fixed list, so the message reaches the right person without someone watching the channel all day. The label is only useful if it triggers something visible: an emoji reaction that owners filter on, a thread reply that asks for missing details, a copy forwarded to a team channel, or a ticket in your own system. A second, independent question often matters as much as the category: is someone blocked right now? Keep those two decisions separate. A message can be a low-priority how-to question or an urgent access problem, and mixing urgency into the category list makes both answers worse. This guide covers public channels a bot has joined. Direct messages and private channels need different scopes and a stronger privacy case, and are out of scope here.

Route customer support with JevIntent Classification for Product & Support Text: Rules, LLMs, and Typed APIsChoice, Score, and Noul

How does a Slack app receive channel messages?

Use the Events API. In your app settings, turn on Event Subscriptions and choose a delivery method: an HTTP Request URL that Slack POSTs to, or Socket Mode, where your process opens a WebSocket and needs no public URL. Under bot events, subscribe to message.channels; it requires the channels:history scope, and you only receive messages from public channels the bot is a member of, so invite it with /invite in each channel you want classified. Each event arrives as JSON with an outer envelope (type event_callback, team_id, a globally unique event_id) and an inner event that carries type message, channel, user, text, ts, and channel_type channel. If you only want messages that mention the bot, subscribe to app_mention instead. For HTTP delivery, Slack signs every request: compute v0= plus the HMAC-SHA256 hex digest of the string v0:{X-Slack-Request-Timestamp}:{raw body} with your signing secret, compare it with X-Slack-Signature using a constant-time comparison, and reject timestamps more than five minutes from your clock to block replays. Bolt for JavaScript, Python, and Java does this for you. The first request to a new Request URL is a url_verification challenge that you answer by echoing the challenge value.

JevModel API: request and responsePrivate runs and history

Why must the app answer Slack within 3 seconds?

Slack’s Events API documentation asks for an HTTP 2xx within three seconds; otherwise the delivery counts as failed and Slack retries three times: almost immediately, after one minute, and after five minutes, with an x-slack-retry-num header (1, 2, 3) and an x-slack-retry-reason such as http_timeout. If more than 95% of deliveries fail within 60 minutes, Slack can temporarily disable your event subscriptions (apps receiving fewer than 1,000 events per hour are not disabled automatically). A classification call is a network request to another service, so do not make Slack wait for it. Acknowledge first, then classify from a queue or a background task, as Slack itself recommends. Three more rules prevent double work and loops. First, keep the event_id of every event you have handled and drop repeats; a retry carries the same event_id, and JevModel’s API has no idempotency key, so a duplicate delivery would otherwise be classified and billed twice. Second, ignore messages with a bot_id or a subtype (edits, joins, your own bot’s replies), or your bot will classify its own thread replies. Third, budget for volume: Slack caps deliveries at 30,000 events per workspace per app per 60 minutes, and every classified message is one ordinary Jev request against your token balance.

JevModel pricing and free Jev runsJevModel API: request and response

Comparison: a no-code Slack trigger vs your own Slack app vs a CSV export

All three can send the same Jev question; they differ in who runs the plumbing. The table compares workflow shape, not accuracy, and no row is a benchmark.

Zapier Text Classification: AI by Zapier Classify vs Vertex Classify Text vs HTTP → Jevn8n Text Classifier Node: Built-in vs Calling Jev from a WorkflowBatch text classification with the JevModel API
Comparison: a no-code Slack trigger vs your own Slack app vs a CSV export
DimensionNo-code tool (Zapier, Make, n8n…)Own Slack app → Events API → JevChannel export → Console → Batch
HostingThe tool’s Slack trigger; no serverYour endpoint or a Socket Mode processBrowser only
TimingDepends on the tool’s polling or webhook planNear real time after the 3-second acknowledgementAfter the fact, for analysis
Retries and dedupeHandled by the tool; check its run historyYours: store event_id, skip bot_id and subtypeNot applicable
Jev outputSame Choice / Noul answer with probabilitiesSame, plus full control of reactions and repliesRow-level answers in the export (50 rows per batch before any purchase, 1,000 after)

Code example: verify, acknowledge, classify, react (illustrative)

The sketch is a plain Node 18+ HTTP handler so every step is visible; Bolt can replace the signature check and acknowledgement. Give the bot the scopes channels:history and reactions:write, subscribe to message.channels, and invite the bot to the channel. The handler verifies the signature on the raw body, answers url_verification, returns 200 at once, and only then calls Jev. The request follows the documented JevModel contract: POST /api/v1/systemone with a Bearer key from a verified account, a state, and named questions, here a Choice for the kind of request and a Noul for urgency; a successful response has code 0 and data.answers. There is no model field because the site always calls typesafe/jev-1.13 through OpenRouter; JevModel is independent of TypeSafe AI, which develops Jev. Low-probability answers get an eyes reaction so a person looks, and a non-zero code leaves the message untouched. The in-memory Set is only for the sketch; use a shared store when you run more than one process, and a real queue if your platform stops work after the response is sent.

JevModel API: request and responseChoice, Score, and NoulJev probability, confidence and human-review thresholds
// Illustrative only. Node 18+. Env: SLACK_SIGNING_SECRET, SLACK_BOT_TOKEN, JEVMODEL_API_KEY.
// Bot scopes: channels:history (for message.channels) and reactions:write.
import crypto from "node:crypto";
import http from "node:http";

const handled = new Set(); // event_id values; use a shared store in production

function fromSlack(req, raw) {
  const ts = Number(req.headers["x-slack-request-timestamp"]);
  if (!ts || Math.abs(Date.now() / 1000 - ts) > 300) return false; // replay window
  const mine = "v0=" + crypto
    .createHmac("sha256", process.env.SLACK_SIGNING_SECRET)
    .update("v0:" + ts + ":" + raw)
    .digest("hex");
  const theirs = String(req.headers["x-slack-signature"] || "");
  return mine.length === theirs.length &&
    crypto.timingSafeEqual(Buffer.from(mine), Buffer.from(theirs));
}

http.createServer((req, res) => {
  let raw = "";
  req.on("data", (chunk) => (raw += chunk));
  req.on("end", () => {
    if (!fromSlack(req, raw)) return res.writeHead(401).end();
    const body = JSON.parse(raw);
    if (body.type === "url_verification") return res.end(body.challenge);
    res.writeHead(200).end(); // acknowledge within 3 seconds, work afterwards
    const e = body.event;
    if (!e || e.type !== "message" || e.subtype || e.bot_id) return; // skip edits, joins, bots
    if (handled.has(body.event_id)) return; // Slack retries reuse the event_id
    handled.add(body.event_id);
    classify(e).catch(console.error);
  });
}).listen(3000);

async function classify(e) {
  const res = await fetch("https://jevmodel.app/api/v1/systemone", {
    method: "POST",
    headers: {
      Authorization: "Bearer " + process.env.JEVMODEL_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      state: { channel: "help", text: e.text }, // text only, no user profile
      questions: {
        kind: {
          type: "choice",
          instructions: "What kind of request is this help-channel message?",
          criteria: {
            bug: "Something is broken, slow, or returns an error.",
            access: "Asking for an account, a permission, or access to a tool.",
            how_to: "Asking how to do something that already works.",
            other: "Chat, thanks, or anything that fits none of the labels above.",
          },
        },
        urgent: {
          type: "noul",
          instructions: "Is someone blocked right now by what this message describes?",
          criteria: {
            true: "A person or customer cannot work now, or a service is down.",
            false: "It can wait for normal working hours.",
          },
        },
      },
    }),
  });
  const out = await res.json();
  if (out.code !== 0) return; // leave the message unlabeled
  const { kind, urgent } = out.data.answers;
  const p = kind.probabilities ? kind.probabilities[kind.choice] : 0;
  const emoji = { bug: "bug", access: "key", how_to: "question" };
  const names = [p < 0.7 ? "eyes" : emoji[kind.choice]]; // 0.7 is a placeholder
  if (urgent.noul >= 0.8) names.push("rotating_light"); // placeholder threshold
  for (const name of names.filter(Boolean)) {
    await fetch("https://slack.com/api/reactions.add", {
      method: "POST",
      headers: {
        Authorization: "Bearer " + process.env.SLACK_BOT_TOKEN,
        "Content-Type": "application/json; charset=utf-8",
      },
      body: JSON.stringify({ channel: e.channel, timestamp: e.ts, name }),
    });
  }
}

What should happen after a message is labeled?

Prefer reversible, visible actions. A reaction is the lightest: owners can filter by emoji, and anyone can remove a wrong one. A thread reply that asks for a screenshot or an order number is useful for a how_to or bug label, but post it only above a threshold you have tested, because a wrong automated reply in a busy channel is noisy. Forwarding a link to a team channel or opening a ticket in your own system is fine once the label is reliable. Do not delete, hide, or move messages based on a label, and do not page someone from the Noul alone until you have checked how often it is wrong. Send only what the decision needs: the message text is usually enough, so leave out user names, emails, and profile fields. Before switching it on, export a few hundred past messages from the channel, hand-label them, run the same questions, and compare per label; pick thresholds from that sample. 0.7 and 0.8 in the example are placeholders, not recommendations. Watch the share of other and of eyes reactions: a high share means the label definitions need work.

Evaluate a Jev workflowJev probability, confidence and human-review thresholdsPrivate runs and history

Which setup should you pick?

Pick a no-code tool when it already runs your other automations and a delay of a minute or so is fine; the Zapier, Make, n8n, Power Automate, and Pipedream guides show the same Jev request in each tool. Pick your own Slack app when you want near-real-time reactions, full control of retries and replies, or several channels with different label sets. Pick a channel export plus Console → Batch when the goal is a one-off analysis of what a channel was used for, not live routing. Checklist for any of them: 1) write label definitions with an explicit other; 2) keep urgency as a separate Noul, not a label; 3) deduplicate on event_id and ignore bot messages; 4) send the text, not the profile; 5) start with reactions only, and add replies or tickets after you have measured agreement on a labeled sample. JevModel is text-only and is not open source; images, files, and voice clips in Slack are not classified by this setup. If a message can belong to several categories at once, read multi-label-text-classification before widening the Choice.

Multi-Label Text Classification: One Choice, Several Yes/No Questions, or a Second PassMake.com Categorize Text: AI Toolkit vs HTTP → Jev for Typed Labels and Confidence ThresholdsPower Automate Text Classification: AI Builder Category Models vs HTTP → JevPipedream Text Classification: Hugging Face Action vs HTTP → Jev for Typed LabelsIs Jev a Multimodal / Downloadable Model? What JevModel Actually IsAuto-Label GitHub Issues with AI: a GitHub Actions Workflow That Calls JevEmail Classification with AI: Rules, LLM Prompts, or a Typed Decision API for Inbound Mail

Frequently asked questions

Do I need a public server to classify Slack messages?

No. With Socket Mode your process opens a WebSocket to Slack and receives the same events without a public Request URL. A no-code tool with a Slack trigger is another option that needs no server of your own.

Why was the same Slack message classified twice?

Usually because the app answered Slack after more than 3 seconds, so Slack retried the delivery with the same event_id. Acknowledge immediately, store handled event_id values, and skip repeats; JevModel has no idempotency key to do this for you.

Can the bot classify private channels or direct messages?

Only with separate message events and history scopes for those conversation types, and only where the bot is a member. This guide covers public channels; for anything private, get explicit agreement from the workspace owners and send the minimum text needed.

How much does classifying Slack messages with Jev cost?

Each classified message is one ordinary API request billed in tokens from your balance; verified accounts start with 100,000 free tokens, and the Choice and Noul in one request are billed together. Filtering out bot messages, edits, and very short chatter before calling the API keeps usage down. See the pricing guide for current packs.

JevModel is independent and not affiliated with TypeSafe AI.