Jev Events

Actions and handlers

Run a built-in platform action or your own code when an outcome fires.

Every .on() takes an event, an optional policy and a handler. A handler is either your own function or a native action such as twitch.timeout(). You can attach as many handlers to one event as you like, and .on() returns the monitor so calls chain.

handlers.ts
import { defineAction, monitor, recipes } from "jev-events";
import { twitch } from "@jev-events/twitch";

const mods = monitor({
  source: twitch.chat(),
  questions: { kind: recipes.chat.kind, hateful: recipes.chat.hateful },
});

// A built-in action: dry-run until you pass dryRun: false,
// and never aimed at moderators or VIPs.
mods.on("hateful", { min: 0.9 }, twitch.timeout({ seconds: 600, reason: "Please keep chat kind" }));

// Your own function runs every time the outcome fires.
mods.on("kind:question", (e) => {
  overlay.push(`${e.item.author.name} asks: ${e.item.text}`);
});

// Your own action gets the same dry-run gate and protections as built-in ones.
const flagInDatabase = defineAction({
  platform: "*",
  name: "db.flag",
  describe: (e) => `flag ${e.item.author?.name ?? e.item.id} in the database`,
  run: (e) => db.flag(e.item.author?.id ?? e.item.id, e.trigger.event),
});
mods.on("kind:spam", { min: 0.85 }, flagInDatabase);

await mods.start();

Your functions

Your function receives the triggered event: the typed item, every answers, the trigger that fired (event name, label, probability or score), the connection the item came from, latencyMs, usage, and whether the answer was cached.

Your functions run whenever the outcome fires. Dry-run and protected users only gate native actions, because only you know what your function does. If it has side effects that should respect them, check the flags on the event:

chat.on("kind:spam", (e) => {
  if (e.dryRun || e.protected) return;
  strikes.add(e.item.author.id);
});

An exception in a handler becomes an error event with phase: "handler". It doesn't stop the monitor or the other handlers.

Native actions

Native actions call the platform's API: timeouts, bans, deletes, replies. Each one:

  • only logs what it would do while the monitor is in dry-run, which is the default,
  • never runs on protected users, such as a Twitch channel's broadcaster and moderators, or your colleagues in Gmail,
  • runs at most once per item, even if a restart judges the item again,
  • belongs to a platform, so attaching a Twitch action to a monitor for another platform is a type error,
  • reports every outcome as an action event with a status of done, dry-run, skipped or failed.

The actions for each platform are listed on its page: Gmail, Google Calendar, Slack and Twitch.

Your own actions

defineAction() wraps your code in the same guarantees, as flagInDatabase does above. Use platform "*" for an action that works with any source, or a platform name to restrict it. describe is the line printed in dry-run and written to the audit log.

run(e, ctx) gets the triggered event and ctx.session, what the source built for this connection. For the built-in integrations, that includes an API client signed in as the connection's account, such as ctx.session.api for Gmail and Slack or ctx.session.helix for Twitch, so an action of your own can call an endpoint the integration doesn't wrap yet.

Counting with burst()

Jev judges one item at a time; counting across items is a job for code. burst() wraps a handler so it only runs when an outcome fires a number of times within a window:

chat.on(
  "streamIssue",
  burst(
    { count: 3, within: "45s", distinctBy: (e) => e.item.author.id },
    (b) => alertStreamer(`${b.events.length} viewers say the stream is broken`),
  ),
);

distinctBy counts each person once, so one angry viewer spamming "no sound" doesn't page anyone. After a burst fires, it stays quiet for cooldown, which defaults to the window.

On this page