Shortcut:
docs/slack/manifest.jsonis a ready app
manifest with every scope and event this agent needs. Paste it into
api.slack.com/apps → Create New App → From a manifest, replaceYOUR-DOMAINin three places, and skip most of this page.
What each scope is for.
Or skip Slack app config entirely withnpx eve add channel/slackand Vercel
Connect, which manages the app, the token, and verification for you.
This guide walks you through creating a Slack app and connecting it to mOperator. Even if you're not a developer, you can complete these steps by following along carefully.
You should now see your app's settings page.
mOperator needs specific permissions to read messages and send responses:
app_mentions:read — Read when the bot is mentionedchat:write — Send messagesfiles:read — Read attached filesfiles:write — Upload filesim:history — Read direct messagesusers:read — Get user informationusers:read.email — Get user email addresses (needed for approval workflow)channels:history — Read channel messagesgroups:history — Read private channel messagescommands — Enable slash commandsYour scopes list should look like this:
app_mentions:read
chat:write
commands
files:read
files:write
groups:history
channels:history
im:history
users:read
users:read.email
https://your-app.vercel.app/eve/v1/slackyour-app with your actual Vercel domainapp_mention — Bot was mentionedmessage.im — Direct message to bot/moperatorhttps://your-app.vercel.app/eve/v1/slack (or your local dev URL)bug|feature|help If you want to use the approval workflow for Salesforce write operations, you need to enable interactivity so Slack can send button clicks to your app:
https://your-app.vercel.app/eve/v1/slackyour-app with your actual Vercel domainThis enables the Approve/Deny buttons that appear when a non-authorized user triggers a Salesforce write operation. See the approval workflow section below for details on configuring AUTHORIZED_USER_EMAILS.
This lets users DM the bot directly.
xoxb- — copy the entire token.env.local:SLACK_BOT_TOKEN=xoxb-your-copied-token-here
This is only needed if you want the bot to access thread history:
U12345678ABC.env.local:SLACK_BOT_USER_ID=U12345678ABC
@mOperator say hellomOperator includes an approval workflow for Salesforce write operations (create, update, delete, bulk update, add to campaign). When a non-authorized user asks the bot to perform a write operation, the bot posts an approval request with Approve and Deny buttons in the Slack thread. An authorized user must click Approve before the operation executes.
Add these to your .env.local:
# Comma-separated email addresses of authorized users (must match Slack profile emails)
AUTHORIZED_USER_EMAILS=admin@example.com,ops-lead@example.com
Optional: Slack user group ID for @mentioning approvers in approval messages
Find this in Slack Admin > User Groups > copy the group ID (e.g., S0123456789)
SLACK_APPROVER_GROUP_ID=S0123456789
You also need Redis configured for the approval store:
UPSTASH_REDIS_REST_URL=https://your-redis.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-token-here
003xxx" with Approve/Deny buttonsAUTHORIZED_USER_EMAILS) clicks Approve or DenyAUTHORIZED_USER_EMAILS is empty or not set, all write operations require approval (no one can self-approve)With channels:history granted, the agent can answer a follow-up in its own
thread without another @mention — "rebuild it with the CTA" just works. The
trap is that a thread is also where people talk to each other, and early
versions answered all of it: screenshots posted to show a colleague, a message
tagging a teammate, the word "testing".
So an un-mentioned message only starts a turn when the agent is mid-exchange:
its own message was the one immediately before, and that message asked for
something. "Want me to run a build?" earns the next message; "Standing by."
does not. Everything else waits for an @mention.
The rule is deliberately conversational rather than clever. Letting the model
decide whether to stay quiet does not work — it is not consulted, because
whether to reply is settled in the channel layer before any model runs.
| How | Effect |
|---|---|
/quiet in the thread | agent stays silent until /unquiet |
:mute: reaction on the thread's first message | same thing, and visible to everyone |
| @mention | always reaches the agent, muted or not |
/quiet needs reactions:write to set the reaction itself. Without that scope
it says so and asks you to add the reaction by hand — it reads the reaction
either way, so the mute still works, it just cannot set it for you.
The flag lives on the Slack message rather than in a datastore because Redis is
optional here: getRedis() returns null when Upstash is unconfigured, so a
Redis-backed mute would have silently done nothing in exactly the deployments
that need it most.
Two small things that make the agent much more discoverable. Both are optional —
skip them and everything else still works.
App Home gives the agent a Home tab listing what is actually connected on
your install, example prompts drawn from those integrations, and the rules that
govern writes. It is the only place someone can find out what the agent does
without guessing a prompt.
app_home_opened.The :bug: reaction files an issue from any message, using the thread as
context — the shortest path from "this is broken" to a written ticket, with
nobody having to re-describe the problem.
reaction_added.reactions:read scope.It needs a project tracker configured, otherwise the agent has nowhere to file.
| Scope | Why |
|---|---|
app_mentions:read, chat:write | the basics |
users:read, users:read.email | resolve who is talking |
im:history | direct messages |
channels:history | unmentioned follow-ups in a thread |
files:read | read attachments people send |
files:write | attach CSVs and exports to replies |
reactions:read | the :bug: reaction, and the :mute: thread mute |
reactions:write | lets /quiet set the mute itself |
im:write | deliver private sign-in prompts as a DM |
users:read.email is the one to double-check. Without it the agent cannot
resolve anyone's email, which means every caller is treated as a non-approver
and every Salesforce write is refused for lack of attribution — with nothing
looking misconfigured. npm run agent:doctor checks for exactly this.