Back to blog
Engineering
June 17, 20269 min read

Driving an agent from a Slack thread: how the bridge actually works

Mention the bot and a task is born; the agent streams its thinking back into the thread and asks you questions with real buttons. A look under the hood of the Agentis Slack bridge — events, guards, streaming and an interactive MCP.

Ondřej Novák

Founder

Slack is where problems get reported and where decisions get made — and historically it is also where the trail goes cold. Someone describes a bug, someone else copies it into a ticket, and the actual work happens somewhere you cannot see. The Agentis Slack bridge closes that gap: you mention the bot in a thread, an agent run starts against the right project, and the entire run — its progress, its questions, its result — comes back to the same thread. Nobody leaves the conversation. This is how that round trip is built.

A mention becomes a task

The bridge is a small Slack Bolt app running over Socket Mode, so it needs no public URL or inbound webhooks. It listens for two events: an app_mention when you @ the bot, and plain messages in threads it is part of. When one arrives, it strips the bot mention out of the text, then pulls the whole thread with conversations.replies and resolves every participant’s real name. That history is folded into the task description as context, so the agent reads the conversation the way a new teammate would — not just the one line that mentioned it.

From there the handler builds a task and immediately starts a run. The defaults — which project, agent, model, effort and adapter — come from environment configuration, so a mention carries no ceremony. The original Slack coordinates (team, channel, thread, message timestamp, user) ride along in the task headers, which is the thread the run will later stream back into and ask questions in.

The only thing you see in Slack at this point is an acknowledgement you can trust: a 👀 reaction lands on your message when the task was created, and a ❌ reaction if something went wrong on the way. No chatty “working on it” reply — a reaction is enough, and it never clutters the thread.

Two guards before anything runs

A chat bridge that creates tasks needs to be conservative, because chat is noisy and Slack redelivers events. Three cheap checks sit in front of every run. First, the bridge ignores anything from a bot — including itself — and any message subtype, so it can never answer its own messages or loop. Second, an event deduper keyed on Slack’s event id drops redeliveries for ten minutes, so a retried delivery never spawns a second task. Third, a global rate limiter caps how many events turn into runs in a window — thirty per minute by default — so a busy channel can’t accidentally launch a fleet of agents.

A reaction means the task exists. Dedupe means it exists exactly once. The rate limiter means a noisy channel never turns into a runaway bill.

Watching the agent think, in the thread

Once the run starts, you don’t want a wall of silence followed by a wall of text. The bridge tees the agent’s structured JSON event stream and turns it into a live, single message in the thread using Slack’s AI-agent streaming API: chat.startStream opens it, chat.appendStream adds lines as they happen, chat.stopStream finalizes it. Each agent event becomes one readable line with an icon — 🔧 for a shell command, 📖 for a file read, ✏️ for an edit, 🎓 for a skill, 💭 for a line of reasoning — so the thread reads like a terminal someone is narrating.

Two details make it pleasant instead of spammy. Appends are throttled — a few seconds between them by default — so short steps are batched rather than fired one character at a time. And the bridge also sets the thread’s native status via assistant.threads.setStatus, so Slack shows “App is working…” in the header even outside the message body. The streamer is deliberately fragile-proof: every Slack call is best-effort, and any failure is logged to stderr and swallowed. If streaming breaks, the agent run does not — the pipe just becomes a plain tee.

One subtlety worth calling out: the agent’s final answer is held back from the stream on purpose. The streamed message is the progress log; the real answer is posted as its own message by the next workflow step, so the result is never buried at the bottom of a scrolling status feed.

When the agent needs you, it asks in the thread

The most useful part of controlling an agent from chat is the reverse direction: the agent asking you. The bridge ships a stdio MCP server exposing two tools — question and approve — with the same surface the agent already knows from the web UI, so it is a drop-in. The difference is purely where the question is answered: instead of the web, it lands in the Slack thread the task came from.

  • The agent calls question (one or more questions, optional predefined options, optional free text) or approve (a yes/no decision).
  • The MCP registers the question on the backend, which mints an external id that ties the eventual answer back to this exact run.
  • It posts a prompt into the thread with a real “Odpovědět” button; the button carries the external id and task id in its payload.
  • A tap opens a Block Kit modal — radio buttons, checkboxes or a free-text field — built from the live question batch fetched from the backend.
  • Submitting writes the answers back, the prompt message rewrites itself into a tidy “question → answer” summary, and the agent receives the result and continues.

While you decide, the MCP simply polls the backend for the result, with a generous timeout, so the agent blocks on a real human decision instead of guessing. And because the question is registered on the backend regardless of Slack, the same prompt can be answered in the web UI too — if the Slack context is missing or a Slack call fails, the run keeps waiting and never crashes. Approval is modelled as a two-option question (approve / reject), so the whole interactive layer reuses one code path.

Wiring it into an adapter is one block of config — point the adapter’s ask-question MCP at this server and the Slack channel and thread arrive automatically through the task headers:

"mcp": {
  "ask-question": {
    "command": [".venv/bin/python", "-m", "agentis_slack.question_mcp"],
    "cwd": "/var/www/agentis-slack",
    "type": "local"
  }
}

Formatting that survives the trip

Agents write Markdown; Slack speaks its own dialect called mrkdwn. The bridge carries a converter that translates the gap rather than leaking raw asterisks into the thread: bold and italics are remapped, headings become bold lines, links are rewritten to Slack’s angle-bracket form, code fences and inline code are preserved, and — the fiddly one — Markdown tables are rendered as aligned monospace blocks, since Slack has no native table. Small thing, but it is the difference between a reply that reads like a document and one that reads like escaping gone wrong.

The shape of it

None of these pieces are large. The bridge is a few hundred lines: an event handler that turns mentions into tasks, three guards that keep it honest, a streamer that narrates the run, an MCP that lets the agent ask for a decision, and a formatter that makes the output legible. Put together, they turn a Slack thread into a real control surface for an agent — you start work by talking, you watch it happen where you already are, and you stay in the loop exactly when the agent needs you, with a button instead of a context switch.

See Agentis in action on your own task

Sign in, describe a task and watch an agent deliver reviewed, ready-to-ship work in minutes.

Try Agentis now

Keep reading