INTEGRATIONS

Reactions for your Slack agent.

Give your Slack agent a way to react with emoji from your workspace. Emote picks a reaction, and your bot adds it to the original message.

Prerequisites

Start with a Slack app that already receives user messages. Add the reactions:write bot scope, reinstall the app if its scopes changed, and invite the bot to the conversations where it should react. Keep EMOTE_API_KEY and SLACK_BOT_TOKEN on your server.

This example uses the Events API. Subscribe to the message events you need: message.channels with channels:history, message.groups with groups:history, message.im with im:history, or message.mpim with mpim:history. Request only the scopes for conversations your agent serves.

Use your existing Bolt app, Socket Mode connection, or HTTP event handler. For HTTP events, verify Slack’s request signature and timestamp, handle URL verification, acknowledge within three seconds, and enqueue the message before doing model or Emote work. Bolt can handle request verification for you. For Socket Mode, acknowledge the event envelope promptly.

Copy the TypeScript or Python example into your server code. TypeScript uses Node.js 20 or later; Python uses Python 3.10 or later with pip install httpx. Each example includes the Emote request function. Keep API keys in server environment variables.

Reaction options

Slack’s reactions.add takes an emoji name, such as thumbsup, rather than 👍 or :thumbsup:. The example uses these names as custom Emote IDs and supplies descriptions so Emote knows what each one means.

Workspace custom emoji work the same way. For an installed emoji named party_parrot, add { id: "party_parrot", description: "Celebrate good news with enthusiasm." } to the palette you pass as reactions. Use names available in the target workspace; Emote does not upload or discover workspace emoji for you.

Pass your agent’s system prompt as agent and recent messages as context. Set frequency to reserved, balanced, or expressive. Every setting can return "none", which skips delivery.

Integration example

import { createHash } from "node:crypto";

export const REACTIONS = [
  "👍", "❤️", "😂", "🎉", "😮", "😢",
  "🤔", "🔥", "💀", "🙌", "👀", "💯",
];

export type ReactionOption = string | { id: string; description: string };
export type ContextMessage = { role: "user" | "assistant"; content: string; reaction?: string };
export type ReactionInput = {
  eventId: string; message: string; agent?: string;
  context?: ContextMessage[]; reactions?: ReactionOption[];
  frequency?: "reserved" | "balanced" | "expressive";
};

// Server only. eventId must identify this message across all your conversations.
export async function chooseReaction({
  eventId, message, agent, context = [], reactions = REACTIONS, frequency = "balanced",
}: ReactionInput): Promise<string> {
  const key = process.env.EMOTE_API_KEY;
  if (!key) throw new Error("Set EMOTE_API_KEY on your server.");
  if (!eventId) throw new Error("A stable eventId is required.");

  const response = await fetch("https://useemote.com/v1/react", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${key}`,
      "Content-Type": "application/json",
      "Idempotency-Key": createHash("sha256").update(eventId).digest("hex"),
    },
    body: JSON.stringify({ message, agent, context, reactions, frequency }),
    signal: AbortSignal.timeout(3000),
  });
  if (!response.ok) throw new Error(`Emote: HTTP ${response.status}`);
  const reaction = await response.json();
  const allowed = reactions.map((option) => typeof option === "string" ? option : option.id);
  if (typeof reaction !== "string" || (reaction !== "none" && !allowed.includes(reaction))) {
    throw new Error("Unexpected Emote response.");
  }
  return reaction;
}

// Slack names, without surrounding colons. Add workspace custom emoji the same way.
export const SLACK_REACTIONS = [
  { id: "thumbsup", description: "Agreement or acknowledgement." },
  { id: "heart", description: "Warmth, appreciation, or support." },
  { id: "tada", description: "Celebrate an achievement or good news." },
  { id: "eyes", description: "Interest or attention to something worth looking at." },
];

type SlackInput = Omit<ReactionInput, "eventId" | "message" | "reactions"> & {
  teamId: string; botUserId: string; channel: string; timestamp: string; text: string;
  reactions?: { id: string; description: string }[];
};

// Call from your verified, deduplicated message worker, not the webhook response.
export async function reactOnSlack({
  teamId, botUserId, channel, timestamp, text, agent, context = [],
  reactions = SLACK_REACTIONS, frequency = "balanced",
}: SlackInput) {
  const token = process.env.SLACK_BOT_TOKEN;
  if (!token) throw new Error("Set SLACK_BOT_TOKEN on your server.");
  if (!reactions.length) return;
  const reaction = await chooseReaction({
    eventId: `slack:${teamId}:${botUserId}:${channel}:${timestamp}`,
    message: text, agent, context, reactions, frequency,
  });
  if (reaction === "none") return;

  const response = await fetch("https://slack.com/api/reactions.add", {
    method: "POST",
    headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
    body: JSON.stringify({ channel, timestamp, name: reaction }),
    signal: AbortSignal.timeout(5000),
  });
  if (!response.ok) throw new Error(`Slack: HTTP ${response.status}`);
  const result = await response.json();
  // Slack can return HTTP 200 for API errors. A repeated reaction is already done.
  if (result.ok !== true && result.error !== "already_reacted") {
    throw new Error(`Slack: ${result.error ?? "Unexpected response"}`);
  }
}

Use the message’s channel and ts, including for replies in a thread. Do not replace ts with thread_ts, or the reaction will land on the parent message. Keep timestamps as strings. The environment variable above is for one workspace installation; if your app serves multiple workspaces, select the matching installation token and emoji palette for each event.

Message handler

The snippet below belongs inside your existing async worker. event is the message event from the Slack envelope, teamId is its workspace ID, and botUserId is your installed bot user’s ID. The personality, recent messages, and reply function come from your application.

// Inside your verified, deduplicated Events API worker.
// event is the message event from the envelope; teamId is envelope.team_id.
if (event.type !== "message" || event.subtype || event.bot_id ||
    !event.user || event.user === botUserId || !event.text?.trim()) return;

const reactionTask = reactOnSlack({
  teamId, botUserId, channel: event.channel,
  timestamp: event.ts, // Keep the original string, including its decimals.
  text: event.text, agent: agentPersonality, context: recentMessages,
  frequency: "balanced",
}).catch(() => console.error("Reaction failed; reply continues."));

const replyTask = Promise.resolve().then(() => replyToSlack(event));
const [, reply] = await Promise.allSettled([reactionTask, replyTask]);
if (reply.status === "rejected") throw reply.reason;

The example skips bot messages, edits, deletions, message subtypes, and empty text. Route only conversations your agent should respond to. Deduplicate event deliveries using the envelope’s event_id and track reaction delivery separately from the normal reply. Keep the Emote input unchanged on retries; its idempotency key includes the workspace, bot, channel, and message.

Delivery rules

Slack can return HTTP 200 with ok: false, so the helper checks both. It treats already_reacted as success. A missing emoji, archived channel, deleted message, or missing permission still raises an error; the worker catches reaction errors so your agent can reply normally.

These helpers surface errors without retrying. For production, use your Slack SDK’s rate-limit handling or extend the delivery function to reschedule HTTP 429 responses using Retry-After. Retry transient failures with a bounded backoff; discard deleted or stale targets. "none" leaves the message unchanged and does not remove an earlier reaction.

Verification

In a test channel, check a standard emoji, a custom workspace emoji, and a thread reply. Confirm the reaction lands on the exact message, "none" sends nothing, bot messages do not trigger a loop, and repeated events do not repeat your main reply. Test a revoked scope or unavailable emoji and confirm the agent’s reply still works.

API reference · Get an API key