Iris Docs

Channel Modes

Configure how Iris behaves per Slack channel — from mention-only to full passthrough.

How Iris behaves in a Slack channel is configured per channel in <workspace>/meta/channels.json. Keys are channel IDs; prefix wildcards like D* are supported. An exact channel ID always wins over wildcards; when several wildcards match, the longest prefix wins (e.g. DA* beats D*).

ModeBehavior
dmDefault. Responds in DMs; channels need an @iris mention
adminIdentical to dm — kept as a permanent alias. Control commands used to be what set it apart; they now work in dm channels too
threadOnly responds inside registered session threads (sessions created via the API)
interactive-threadA top-level message opens a session; replies in that thread continue it without further mentions
leadsEvery top-level message triggers Iris — no mention needed
passthroughEvery message is forwarded to an external HTTP endpoint; the reply is posted back. Iris's LLM never runs

requireMentionForTopLevel (per channel entry) gates what a top-level channel message does in interactive-thread and passthrough channels: when set, only an @iris mention opens a session / gets forwarded, and plain top-level messages are logged but otherwise ignored.

These six names are the only supported, documented surface — copy one of the recipes below. Underneath, all six are the same dispatch pipeline running with different settings; see Under the hood if you're curious how they relate, but don't configure a channel with raw settings instead of a named mode.

{
  "C0XXXXXXX": { "mode": "interactive-thread", "requireMentionForTopLevel": true },
  "C0YYYYYYY": {
    "mode": "passthrough",
    "url": "https://example.com/webhook",
    "secretName": "MY-WEBHOOK-KEY",
    "payload": { "sender_id": "{{sender_id}}", "message_text": "{{text}}" },
    "replyPrefix": "*Bot:* "
  },
  "D*": { "mode": "dm" }
}

Control commands

Three commands control a run and its context rather than talking to Iris. Each works with or without a leading slash — stop and /stop are the same command.

CommandEffect
stopAbort the run in flight (_Nothing running_ if idle)
compactSummarise the context into a compaction entry
reset (alias clear)Clear the conversation history

They are read as commands wherever the message is unmistakably meant for Iris:

  • A DM, or an explicit @iris stop — every mode. This is not a permission boundary and never was: anyone who can mention Iris can have her run arbitrary bash, which strictly dominates stop/compact/reset.
  • A reply inside a session thread (thread / interactive-thread) — every reply there is addressed to Iris by construction, so no mention is needed. The command acts on that session's run, not the channel's.
  • Bare top-level channel text (no mention, no thread) — dm and admin channels only. leads is excluded because its top-level traffic is third-party feeds, where an email bot's "STOP" is an unsubscribe keyword rather than a command.
  • A native Slack slash command/stop, /compact, /clear, /reset, /verbose on|off, in any dm / admin / leads channel. They are not registered for you: add them to your Slack app (see Setup), or Slack rejects a typed /stop client-side and Iris never sees it. Slash commands carry no thread, so in a thread / interactive-thread channel they're refused with a note to reply in the session's thread instead.

Never a command: a thread reply in a chat-container channel that is neither a mention nor a DM (a mid-conversation "stop" between two people can't wipe the channel out from under them), and anything in a passthrough channel, where every message is forwarded verbatim and Iris keeps no context to control.

admin mode is now identical to dm — control commands were the only thing that distinguished them. It remains a permanent alias, so existing channels.json files keep working unchanged.

On Telegram these are the bot commands /stop, /compact, /reset, /clear and always work — Telegram has no channel-mode concept.

Verbose tool output

By default (IRIS_VERBOSE_TOOLS unset), a run shows one status line that updates in place as Iris works (e.g. _→ running bash..._), then gets replaced by the final answer — no per-tool-call thread replies (Slack) or flat messages (Telegram), no chain-of-thought dump, no per-run cost summary.

Toggle it with verbose on / verbose off / verbose status on Slack (the /verbose ... spelling works too), or /verbose on|off|status on Telegram. It's a UX preference rather than a destructive action, so it has never been gated by channel mode at all: it works from any DM or explicit @iris mention in any mode. The setting persists per channel (in that channel's settings.json) until toggled again; IRIS_VERBOSE_TOOLS=true in /iris/.env changes the default for channels that haven't set their own override.

This toggle only controls what gets posted back into the chat transport. Tool-call invocations (name, label, and formatted arguments) are always written to the console log (journalctl -u iris) regardless of the setting — formatToolArgs() in iris-runtime/src/engine/log.ts formats the arguments and logToolStart() uniformly indents every line of the joined output.

Reply context (Telegram)

Replying to an earlier message includes that message's text (or caption, or a content-type note like [a photo] for media) prepended to what Iris receives, so a follow-up like "this one?" resolves against the message it replied to.

Passthrough configuration

Passthrough channels forward every message — top-level channel messages, thread replies, @iris mentions, and DMs — to url as a JSON POST and post the endpoint's reply back into the thread. Nothing is interpreted by Iris herself: even stop / compact / reset are forwarded verbatim (and a slash command is refused outright), and scheduled events cannot target a passthrough channel.

  • payload — a JSON template for the request body. String values may use placeholders, substituted recursively: {{text}}, {{user_id}}, {{user_name}}, {{user_handle}} (lowercased, dot-separated), {{sender_id}}, {{channel}}, {{ts}}. Default: { "text": "{{text}}", "user": "{{user_name}}", "sender_id": "{{sender_id}}" }
  • secretName — API key resolved through the get-secret skill (cached for the process lifetime); sent as an X-API-Key header. Falls back to the PASSTHROUGH_API_KEY env var.
  • replyPrefix — optional prefix prepended to replies posted back to the thread.

The endpoint's response is read as JSON; the first of response, text, or error is posted back.

Queueing and overflow

Each channel queues at most 5 pending messages for the LLM. This includes leads channels: under burst load, messages beyond the cap are logged with a warning but do not trigger a run — no notice is posted into the channel itself, which is often an external-facing feed. The full message text is always preserved in the channel's log.jsonl, so no lead is lost; only the automated response is skipped.

Sessions

thread and interactive-thread modes are built on sessions — durable conversation containers created via the internal API or by a top-level mention in an interactive-thread channel. Session state lives on disk and survives restarts; routes are rebuilt from sessions.json at startup.

Under the hood

The six named modes are three primitives plus orthogonal flags, all resolved by one dispatch pipeline in the engine (src/engine/dispatch.ts and dispatch-config.ts) instead of each mode re-implementing its own slice of routing logic. This dispatch pipeline is currently wired up for Slack only; see Writing a Transport if you're adding a new chat platform and deciding whether it needs the same wiring.

PrimitiveBehavior
chatChannel-context LLM run — replies land in the channel itself
sessionsPer-thread session LLM run — replies are scoped to a durable session (see Sessions)
relayWebhook forward — Iris's LLM never runs
FlagValuesMeaning
triggermention | all-top-level | api-onlyWhat opens/continues a container organically: an explicit @iris mention only, any top-level message, or nothing (only a pre-existing, API-created session continues)
adminCommandsbooleanBare top-level channel text (no mention, no thread) is read as a stop / compact / reset command. Only governs that ambiguous shape — a DM or an explicit @iris mention is a command in any chat container regardless of the flag
acceptBotMessagesbooleanBot/integration messages are admitted as triggers, not filtered out
replayMissedbooleanPre-startup top-level messages are replayed instead of skipped

The mapping, exactly as implemented — this is also the legacy-alias table: channels.json only ever stores one of these six names, expanded to the primitive shape on load, and the six names remain supported forever so no existing config ever needs to change.

ModecontainertriggeradminCommandsacceptBotMessagesreplayMissed
dmchatmention
adminchatmention
leadschatall-top-level
threadsessionsapi-only
interactive-threadsessionsmention if requireMentionForTopLevel, else all-top-level
passthroughrelaymention if requireMentionForTopLevel, else all-top-level

A mention or a DM always reaches its container regardless of trigger — the flag only gates plain top-level chatter with no explicit address to the bot. This mapping is an implementation detail, not a second configuration surface: channels.json only ever takes a named mode (+ requireMentionForTopLevel

  • passthrough's url/payload/secretName/replyPrefix), which the engine silently expands into this shape. There is no way to configure a channel with raw container/trigger/flag values directly, and there won't be — new behavior gets a new named mode (or a new flag on an existing one), documented here as a recipe, not a raw combination for callers to assemble themselves.

Channel workspace layout

Real Telegram and Slack channels live two levels under the data root: data/telegram/tg-<id>/ for Telegram chats and data/slack/<id>/ for Slack channels. The workspace root is resolved from the explicit working directory passed through runner creation — not inferred from the channel directory's depth — so custom model providers (models.json), memory (MEMORY.md), and skills are discovered correctly for real conversations, exactly as they are for synthetic session channels.

Edit this page on GitHub