Apus applies the same group rules across group-capable channels, including Discord, iMessage, Matrix, Microsoft Teams, QQBot, Signal, Slack, Telegram, WhatsApp, and Zalo.

For always-on rooms that should provide quiet context unless the agent explicitly sends a visible message, see Ambient room events.

Beginner intro (2 minutes)

Apus "lives" on your own messaging accounts. There is no separate WhatsApp bot user: if you are in a group, Apus can see that group and respond there.

Default behavior:

Translation: allowlisted senders can trigger Apus by mentioning it.

TL;DR

  • DM access is controlled by *.allowFrom.
  • Group access is controlled by *.groupPolicy + allowlists (*.groups, *.groupAllowFrom).
  • Reply triggering is controlled by mention gating (requireMention, /activation).

Quick flow (what happens to a group message):

groupPolicy? disabled -> drop
groupPolicy? allowlist -> group allowed? no -> drop
requireMention? yes -> mentioned? no -> store for context only
mention/reply/command/DM -> user request
always-on group chatter -> user request, or room event when configured

Bot-created threads

Use requireMentionInBotThreads to override mention gating only in a thread or topic created by the receiving bot:

For example, keep Discord parent channels mention-gated while opening the bot's own threads, and apply the same policy to Slack. Merge these fields into your existing channel configuration, preserving its credentials and allowlists:

{
  channels: {
    discord: {
      intents: { messageContent: true },
      guilds: {
        "123456789012345678": {
          requireMention: true,
          requireMentionInBotThreads: false,
        },
      },
    },
    slack: {
      requireMention: true,
      requireMentionInBotThreads: false,
    },
  },
}

The setting uses each channel's existing configuration scopes; account-scoped configuration uses the corresponding paths under accounts.<id> where supported. For a narrower change, set the option on an existing allowed channel entry. See the Discord example and Slack example.

Channel Configuration scopes Thread ownership evidence
Buzz Group Verified root event author and room
ClickClack Account, group Authenticated root message author, workspace, and channel
Discord Guild, channel Discord thread owner ID
Feishu Account, group Native topic root and current app identity
iMessage Group Native thread-root GUID in the account/conversation-scoped sent-message cache
Matrix Account, room Native m.thread root sender
Mattermost Account, group Native root post author and channel
Microsoft Teams Global, team, channel Channel root in the current app/conversation's sent-message cache
Slack Account, channel Native root author or authenticated thread-starter lookup
Telegram Group, topic Recorded topic creator from native creation events or successful bot topic creation
Tlon Account, channel authorization rule Authenticated root post author

Unknown or unavailable ownership keeps ordinary mention rules. Existing bounded caches can lose old ownership evidence; see the channel documentation for its limits. A bot replying in someone else's thread does not make it the creator. Sender, channel, and bot-access restrictions still apply. Explicit session bindings retain their own route activation rules.

These rules govern new message admissions. An already admitted turn keeps its captured policy; changing mention rules or sender allowlists does not suppress its reply.

The transport must deliver unmentioned messages before Apus can apply the policy:

The current Google Chat interaction webhook does not provide ordinary unmentioned space messages or thread-owner metadata, so it does not expose this option. Channels that provide only quotes, direct conversations, or no native threads also keep their existing mention behavior. External plugins need to implement the shared thread mention policy.

Visible replies

For normal group/channel requests, Apus defaults to messages.groupChat.visibleReplies: "automatic": the final assistant text posts to the room as the visible reply.

Use messages.groupChat.visibleReplies: "message_tool" when visible answers must go through message(action=send). This selects the delivery method, not whether a reply is required. It works best with models that reliably follow tool-only delivery. If the model misses the tool and returns substantive final text, Apus keeps that text private and attempts a bounded delivery recovery rather than posting it directly. Claude CLI runs call the registered tool as mcp__apus__message; the backend explains this mapping and how to discover deferred tools in the system prompt.

Use "automatic" for models or runtimes that do not reliably follow tool-only delivery: normal text finals post directly to the room, and the agent may still call message(action=send) for files, images, or other attachments that cannot ride along with the final text.

If the message tool is unavailable under the active tool policy, Apus falls back to automatic visible replies instead of silently suppressing the response. apus doctor warns about this mismatch.

For direct chats and any other source event, messages.visibleReplies: "message_tool" applies the same tool-only behavior globally; messages.groupChat.visibleReplies remains the more specific override for group/channel rooms. Internal WebChat direct turns default to automatic final-reply delivery so Pi and Codex receive the same visible-reply contract.

Accepted group/channel requests require a reply by default. To permit selective silence for unaddressed requests, explicitly set agents.defaults.silentReply.group: "allow" or the appropriate surfaces.<id>.silentReply.group override; see Silent replies. In "automatic" mode, that opt-in enables NO_REPLY guidance. In tool-only mode, optional turns stay quiet by not calling the message tool; merely selecting tool-only delivery does not waive a required answer.

Plugin-owned conversation bindings are the exception. Once a plugin binds a thread and claims the inbound turn, the plugin's returned reply is the visible binding response; it does not need message(action=send). That reply is plugin runtime output, not private model final text.

Typing indicators are still sent for direct group requests. Ambient always-on room events, when enabled, stay strict and quiet unless the agent calls the message tool.

Sessions suppress verbose tool/progress summaries by default. Use /verbose on (or /verbose full) to show them for the current session while debugging, and /verbose off to return to final-reply-only behavior. Verbose state is per session and works the same in direct chats, groups, channels, and forum topics.

To submit unmentioned always-on group chatter as quiet room context instead of user requests, use Ambient room events:

{
  messages: {
    groupChat: {
      unmentionedInbound: "room_event",
    },
  },
}

The default is unmentionedInbound: "user_request". Mentioned messages, commands, abort requests, and DMs stay user requests.

To require visible output to go through the message tool for group/channel requests:

{
  messages: {
    groupChat: {
      visibleReplies: "message_tool",
    },
  },
}

To require it for every source chat:

{
  messages: {
    visibleReplies: "message_tool",
  },
}

The gateway picks up messages config changes without a restart after the file is saved. Restart only when config reload is disabled (gateway.reload.mode: "off").

Command turns bypass visibleReplies: "message_tool" and always reply visibly: native slash commands (Discord, Telegram, and other surfaces with native command support) and authorized text /... commands both post their response to the source chat. Unauthorized text /... turns in groups stay message-tool-only; ordinary chat turns follow the configured default.

Context visibility and allowlists

Two different controls are involved in group safety:

By default Apus keeps context as received: allowlists decide who can trigger actions, not what quoted or historical snippets the model sees. To also filter supplemental context, set contextVisibility:

Mode Behavior
"all" (default) Keep supplemental context as received.
"allowlist" Only inject history/thread/quote/forwarded context from allowlisted senders.
"allowlist_quote" allowlist, plus keep the explicitly quoted/replied-to message from any sender.

Set it per channel (channels.<channel>.contextVisibility), per account (channels.<channel>.accounts.<accountId>.contextVisibility), or globally (channels.defaults.contextVisibility). Channels that fetch supplemental context (Discord, Feishu, iMessage, Matrix, Mattermost, Microsoft Teams, QQBot, Signal, Slack, Telegram, WhatsApp) apply the policy when building inbound context; unknown policy combinations fail closed and omit the context.

These modes filter channel-supplied supplemental context only. Tool policy and the owner-only tool inventory are still selected from the current turn's originating requester, not every sender represented in the prompt. See Requester-scoped controls and prompt context.

Group message flow

If you want...

Goal What to set
Allow all groups but only reply on @mentions groups: { "*": { requireMention: true } }
Disable all group replies groupPolicy: "disabled"
Only specific groups groups: { "<group-id>": { ... } } (no "*" key)
Only you can trigger in groups groupPolicy: "allowlist", groupAllowFrom: ["+1555..."]
Reuse one trusted sender set across channels groupAllowFrom: ["accessGroup:operators"]

For reusable sender allowlists, see Access groups.

Session keys

Set a binding's session.groupScope to "main" when a trusted room should share the agent's main conversation:

{
  bindings: [
    {
      agentId: "main",
      match: { channel: "slack", peer: { kind: "channel", id: "C0123TEAM" } },
      session: { groupScope: "main" },
    },
  ],
}

The global session.groupScope supports "per-group" (default) or "main". This does not change group admission, mention gating, or reply routing.

Pattern: personal DMs + public groups (single agent)

Yes — this works well if your "personal" traffic is DMs and your "public" traffic is groups.

Why: in single-agent mode, DMs typically land in the main session key (agent:main:main), while groups use non-main session keys (agent:main:<channel>:group:<id>) under the default groupScope: "per-group". If you enable sandboxing with mode: "non-main", those group sessions run in the configured sandbox backend while your main DM session stays on-host. Docker is the default backend if you do not choose one.

A room configured with groupScope: "main" is a main session and is not covered by sandbox mode: "non-main". Do not merge untrusted or public rooms into main when you rely on that sandbox boundary.

This gives you one agent "brain" (shared workspace + memory), but two execution postures:

If you need truly separate workspaces/personas ("personal" and "public" must never mix), use a second agent + bindings. See Multi-Agent Routing.

DMs on host, groups sandboxed

```json5
{
  agents: {
    defaults: {
      sandbox: {
        mode: "non-main", // groups/channels are non-main -> sandboxed
        scope: "session", // strongest isolation (one container per group/channel)
        workspaceAccess: "none",
      },
    },
  },
  tools: {
    sandbox: {
      tools: {
        // If allow is non-empty, everything else is blocked (deny still wins).
        allow: ["group:messaging", "group:sessions"],
        deny: ["group:runtime", "group:fs", "group:ui", "nodes", "cron", "gateway"],
      },
    },
  },
}
```

Groups see only an allowlisted folder

Want "groups can only see folder X" instead of "no host access"? Keep `workspaceAccess: "none"` and mount only allowlisted paths into the sandbox:

```json5
{
  agents: {
    defaults: {
      sandbox: {
        mode: "non-main",
        scope: "session",
        workspaceAccess: "none",
        docker: {
          binds: [
            // hostPath:containerPath:mode
            "/home/user/FriendsShared:/data:ro",
          ],
        },
      },
    },
  },
}
```

Related:

Display labels

Group policy

Control how group/room messages are handled per channel:

{
  channels: {
    whatsapp: {
      groupPolicy: "disabled", // "open" | "disabled" | "allowlist"
      groupAllowFrom: ["+15551234567"],
    },
    telegram: {
      groupPolicy: "disabled",
      groupAllowFrom: ["123456789"], // numeric Telegram user id (setup resolves @username)
    },
    signal: {
      groupPolicy: "disabled",
      groupAllowFrom: ["+15551234567"],
    },
    imessage: {
      groupPolicy: "disabled",
      groupAllowFrom: ["chat_id:123"],
    },
    msteams: {
      groupPolicy: "disabled",
      groupAllowFrom: ["user@org.com"],
    },
    discord: {
      groupPolicy: "allowlist",
      guilds: {
        GUILD_ID: { channels: { help: { enabled: true } } },
      },
    },
    slack: {
      groupPolicy: "allowlist",
      channels: { "#general": { enabled: true } },
    },
    matrix: {
      groupPolicy: "allowlist",
      groupAllowFrom: ["@owner:example.org"],
      groups: {
        "!roomId:example.org": { enabled: true },
        "#alias:example.org": { enabled: true },
      },
    },
  },
}
Policy Behavior
"open" Groups bypass allowlists; mention-gating still applies.
"disabled" Block all group messages entirely.
"allowlist" Only allow groups/rooms that match the configured allowlist.
Per-channel notes
- `groupPolicy` is separate from mention-gating (which requires @mentions).
- WhatsApp/Telegram/Signal/iMessage/Microsoft Teams/Zalo: use `groupAllowFrom` (fallback: explicit `allowFrom`).
- Signal: `groupAllowFrom` can match either the inbound Signal group id or the sender phone/UUID.
- DM pairing approvals (`*-allowFrom` store entries) apply to DM access only; group sender authorization stays explicit to group allowlists.
- Discord: allowlist uses `channels.discord.guilds.<id>.channels`.
- Slack: allowlist uses `channels.slack.channels`.
- Matrix: allowlist uses `channels.matrix.groups`. Use room IDs (`!room:server`, or the suffixless `!room` form on room version 12+) or aliases (`#alias:server`); room-name keys match only with `channels.matrix.dangerouslyAllowNameMatching: true`, and unresolved entries are ignored at runtime. Use `channels.matrix.groupAllowFrom` to restrict senders; per-room `users` allowlists are also supported.
- Group DMs are controlled separately (`channels.discord.dm.*`, `channels.slack.dm.*`: `groupEnabled`, `groupChannels`).
- Telegram: sender allowlists accept numeric user IDs only (`"123456789"`; `telegram:`/`tg:` prefixes are stripped case-insensitively). `@username` entries do not match at runtime and log a warning; setup resolves `@username` to IDs. Negative chat IDs belong under `channels.telegram.groups`, not sender allowlists.
- Default is `groupPolicy: "allowlist"`; if your group allowlist is empty, group messages are blocked.
- Runtime safety: when a provider block is completely missing (`channels.<provider>` absent), group policy fails closed to `allowlist` instead of inheriting `channels.defaults.groupPolicy`, and the gateway logs the fallback once per account.

Quick mental model (evaluation order for group messages):

groupPolicy

`groupPolicy` (open/disabled/allowlist).

Group allowlists

Group allowlists (`*.groups`, `*.groupAllowFrom`, channel-specific allowlist).

Mention gating

Mention gating (`requireMention`, `/activation`).

Mention gating (default)

Group messages require a mention unless overridden per group. Defaults live per subsystem under *.groups."*".

Supported implicit mention facts are channel-specific:

Fact Current built-in producers
Reply to the bot Discord, Microsoft Teams, QQBot, Slack, Telegram
Quote of the bot LINE, WhatsApp, Zalo personal
Bot joined the thread Mattermost, Slack, Tlon

Each fact defaults to enabled when the channel produces it. Among bundled channels, LINE, Mattermost, Slack, and Tlon read the corresponding implicitMentions flag; set it to false to stop that fact from bypassing mention gating. LINE reads it from the shared channels.defaults.implicitMentions block only; it does not accept a channel- or account-scoped implicitMentions block of its own. Native explicit mentions remain unaffected. The other bundled producers listed above do not currently read implicitMentions, so their facts always count as mentions and the flag cannot turn them off. A flag also has no effect on channels that do not produce that fact.

{
  channels: {
    whatsapp: {
      groups: {
        "*": { requireMention: true },
        "123@g.us": { requireMention: false },
      },
    },
    telegram: {
      groups: {
        "*": { requireMention: true },
        "123456789": { requireMention: false },
      },
    },
    imessage: {
      groups: {
        "*": { requireMention: true },
        "123": { requireMention: false },
      },
    },
  },
  agents: {
    entries: {
      main: {
        groupChat: {
          mentionPatterns: ["@apus", "apus", "\\+15555550123"],
          historyLimit: 50,
        },
      },
    },
  },
}

Scope configured mention patterns

Configured mentionPatterns are regex fallback triggers. Use them when the platform does not expose a native bot mention, or when you want plain text such as apus: to count as a mention. Native platform mentions are separate: when Discord, Slack, Telegram, Matrix, Signal, or another channel can prove the message explicitly mentioned the bot, that native mention still triggers even if configured regex patterns are denied.

By default, configured mention patterns apply everywhere the channel passes provider and conversation facts into mention detection. To keep broad patterns from waking the agent in every group, scope them per channel with channels.<channel>.mentionPatterns.

Use mode: "deny" when regex mention patterns should be off by default for a channel, then opt in specific rooms with allowIn:

{
  messages: {
    groupChat: {
      mentionPatterns: ["\\bapus\\b", "\\bops bot\\b"],
    },
  },
  channels: {
    slack: {
      mentionPatterns: {
        mode: "deny",
        allowIn: ["C0123OPS"],
      },
    },
  },
}

Use the default mode: "allow" (or omit mode) when regex mention patterns should apply broadly, then turn them off in noisy rooms with denyIn:

{
  messages: {
    groupChat: {
      mentionPatterns: ["\\bapus\\b"],
    },
  },
  channels: {
    telegram: {
      mentionPatterns: {
        denyIn: ["-1001234567890", "-1001234567890:topic:42"],
      },
    },
  },
}

Policy resolution:

Field Effect
mode: "allow" Regex mention patterns are enabled unless the conversation ID is in denyIn. This is the default.
mode: "deny" Regex mention patterns are disabled unless the conversation ID is in allowIn.
allowIn Conversation IDs where regex mention patterns are enabled in deny mode.
denyIn Conversation IDs where regex mention patterns are disabled. denyIn wins over allowIn if both include the same ID.

Supported scoped regex policy today:

Channel IDs used in allowIn / denyIn
Discord Discord channel IDs.
Matrix Matrix room IDs.
Slack Slack channel IDs.
Telegram Group chat IDs, or chatId:topic:threadId for forum topics.
WhatsApp WhatsApp conversation IDs such as 123@g.us.

Account-level channel configs can set the same policy under channels.<channel>.accounts.<accountId>.mentionPatterns when that channel supports multiple accounts. Account policy takes precedence over the top-level channel policy for that account.

Mention gating notes
- `mentionPatterns` are case-insensitive safe regex patterns; invalid patterns and unsafe nested-repetition forms are ignored (with a warning).
- Pattern precedence: `agents.entries.*.groupChat.mentionPatterns` (useful when multiple agents share a group) overrides `messages.groupChat.mentionPatterns`; when neither is set, patterns are derived from the routed agent's `identity.name` and `identity.emoji`. An explicit `mentionPatterns: []` at the selected level suppresses this derivation; native mentions remain separate.
- Mention gating can apply without explicitly configured patterns: identity-derived patterns also enable detection. On channels that require detectable mentions before gating, only the absence of both usable patterns and native mention support prevents enforcement.
- Allowlisting a group or sender does not disable mention gating; set that group's `requireMention` to `false` when all messages should trigger.
- Automatic group chat prompt context includes `NO_REPLY` guidance only when the resolved silence policy explicitly allows it; workspace files should not duplicate these mechanics.
- Groups explicitly configured to allow automatic silent replies treat clean empty or reasoning-only model turns as silent, equivalent to `NO_REPLY`. Direct chats never receive `NO_REPLY` guidance, and optional message-tool-only group turns stay quiet by not calling `message(action=send)`.
- Always-on group messages use user-request semantics and require replies by default. Set `messages.groupChat.unmentionedInbound: "room_event"` to submit them as quiet context instead. See [Ambient room events](/channels/ambient-room-events) for supported channels and setup examples.
- Room events are not stored as fake user requests, and private assistant text from no-message-tool room events is not replayed as chat history.
- Discord defaults live in `channels.discord.guilds."*"` (overridable per guild/channel).
- Group history context is wrapped uniformly across channels. Mention-gated groups keep pending skipped messages; always-on groups may also retain recent processed room messages when the channel supports it. Use `messages.groupChat.historyLimit` for the global default and `channels.<channel>.historyLimit` (or `channels.<channel>.accounts.*.historyLimit`) for overrides. Set `0` to disable.

Group/channel tool restrictions (optional)

Some channel configs support restricting which tools are available inside a specific group/room/channel.

When a sender or group policy restricts the turn's tools, that requester can create only hidden helpers of the same agent. Visible and cross-agent sessions are refused, including through ACP or automatic session creation by sessions_send. Helpers retain the restricted tools and the requester's workspace and session root. The restriction follows delegated and queued turns; an unrestricted sender's later turn does not replace it. Owner-authorized automations retain their separate scheduling policy and workspace.

Resolution order (most specific wins):

Group toolsBySender

Group/channel `toolsBySender` match.

Group tools

Group/channel `tools`.

Default toolsBySender

Default (`"*"`) `toolsBySender` match.

Default tools

Default (`"*"`) `tools`.

Example (Telegram):

{
  channels: {
    telegram: {
      groups: {
        "*": { tools: { deny: ["exec"] } },
        "-1001234567890": {
          tools: { deny: ["exec", "read", "write"] },
          toolsBySender: {
            "id:123456789": { alsoAllow: ["exec"] },
          },
        },
      },
    },
  },
}

Group/channel tool restrictions are applied in addition to global/agent tool policy (deny still wins). Some channels use different nesting for rooms/channels (e.g., Discord guilds.*.channels.*, Slack channels.*, Microsoft Teams teams.*.channels.*).

Group allowlists

When channels.whatsapp.groups, channels.telegram.groups, or channels.imessage.groups is configured, the keys act as a group allowlist. Use "*" to allow all groups while still setting default mention behavior.

Common confusion: DM pairing approval is not the same as group authorization. For channels that support DM pairing, the pairing store unlocks DMs only. Group commands still require explicit group sender authorization from config allowlists such as groupAllowFrom or the documented config fallback for that channel.

Common intents (copy/paste):

Disable all group replies

```json5
{
  channels: { whatsapp: { groupPolicy: "disabled" } },
}
```

Allow only specific groups (WhatsApp)

```json5
{
  channels: {
    whatsapp: {
      groups: {
        "123@g.us": { requireMention: true },
        "456@g.us": { requireMention: false },
      },
    },
  },
}
```

Allow all groups but require mention

```json5
{
  channels: {
    whatsapp: {
      groups: { "*": { requireMention: true } },
    },
  },
}
```

Owner-only triggers (WhatsApp)

```json5
{
  channels: {
    whatsapp: {
      groupPolicy: "allowlist",
      groupAllowFrom: ["+15551234567"],
      groups: { "*": { requireMention: true } },
    },
  },
}
```

Activation (owner-only)

Group owners can toggle per-group activation with a standalone message:

/activation is a core owner-gated command and only applies in group chats. Owner means the sender matches commands.ownerAllowFrom; channel allowFrom lists only control ordinary channel and command access. The stored mode overrides that group's requireMention on channels that consult it (Google Chat, QQBot, Telegram, WhatsApp), and the group system-prompt intro reflects the active mode everywhere.

Context fields

Group inbound payloads set:

The agent system prompt includes a group intro on the first turn of a new group session (and after /activation changes). It reminds the model to respond like a human, minimize empty lines and follow normal chat spacing, and avoid typing literal \n sequences. Channels whose declared table mode does not preserve native or raw tables also discourage Markdown tables. Channel-sourced group names and participant labels are rendered as fenced untrusted metadata, not inline system instructions.

iMessage specifics

Related