Moltis
Features

Channels

Moltis connects to messaging platforms through channels. Each channel type has a distinct inbound mode, determining how it receives messages, and a set of capabilities that control what features are available.

Supported Channels

ChannelInbound ModePublic URL RequiredKey Capabilities
TelegramPollingNoStreaming, voice ingest, reactions, OTP, location
DiscordGateway (WebSocket)NoStreaming, interactive messages, threads, voice ingest, reactions
MatrixGateway (sync loop)NoStreaming, voice ingest, interactive polls, threads, reactions, OTP, location, encrypted chats, device verification, ownership bootstrap
Microsoft TeamsWebhookYesStreaming, interactive messages, threads, reactions
WhatsAppGateway (WebSocket)NoStreaming, voice ingest, OTP, pairing, location
SlackSocket ModeNoStreaming, interactive messages, threads, reactions
NostrGateway (relay subscription)NoOTP, encrypted DMs (NIP-04)
SignalGateway (signal-cli SSE)NoOTP, DMs, groups, outbound text

Inbound Modes

Polling

The bot periodically fetches new messages from the platform API. No public URL or open port is needed. Used by Telegram.

Gateway / WebSocket

The bot opens a persistent outbound WebSocket connection to the platform and receives events in real time, or uses a persistent sync loop over outbound HTTP. No public URL needed. Used by Discord, Matrix, and WhatsApp.

Socket Mode

Similar to a gateway connection, but uses the platform’s Socket Mode protocol. No public URL needed. Used by Slack.

Webhook

The platform sends HTTP POST requests to a publicly reachable endpoint on your server. You must configure the messaging endpoint URL in the platform’s settings. Used by Microsoft Teams.

None (Send-Only)

For channels that only send outbound messages and do not receive inbound traffic. No channels currently use this mode, but it is available for future integrations (e.g. email, SMS).

Capabilities Reference

CapabilityDescription
supports_outboundCan send messages to users
supports_streamingCan stream partial responses (typing/editing)
supports_interactiveCan send interactive components (buttons, menus)
supports_threadsCan reply in threads
supports_voice_ingestCan receive and transcribe voice messages
supports_pairingRequires device pairing (QR code)
supports_otpSupports OTP-based sender approval
supports_reactionsCan add/remove emoji reactions
supports_locationCan receive and process location data

Setup

Channels can be configured in two places:

  • In moltis.toml under [channels], for file-managed setups
  • In the web UI under Settings -> Channels, which stores channel accounts in the internal channels table inside data_dir()/moltis.db

The web UI does not write channel settings back into moltis.toml. It includes an advanced JSON config editor so channel-specific settings remain reachable even when a dedicated form field has not been added yet.

The channel picker itself is controlled by [channels].offered in moltis.toml. If you edit that list by hand, reload the page so the web UI re-reads the current picker options.

Channel configs stored through the web UI currently live as JSON records in the internal channels table in data_dir()/moltis.db. They are not currently wrapped by the Moltis vault, so treat local access to that database as access to the configured channel credentials.

Some channel integrations also have platform-specific limits. For Matrix, encrypted chats require password auth. Access-token auth is only suitable for plain Matrix traffic because Moltis cannot import an existing device’s private E2EE keys from an access token alone. See Matrix for the full setup, ownership, verification, and troubleshooting flow.

moltis.toml and the web UI are both loaded at startup. If the same (channel_type, account_id) exists in both, the moltis.toml entry wins.

Manual file configuration looks like this:

[channels.telegram.my_bot]
token = "123456:ABC-DEF..."
dm_policy = "allowlist"
allowlist = ["alice", "bob"]

[channels.msteams.my_teams_bot]
app_id = "..."
app_password = "..."

[channels.discord.my_discord_bot]
token = "..."

[channels.slack.my_slack_bot]
bot_token = "xoxb-..."
app_token = "xapp-..."

[channels.matrix.my_matrix_bot]
homeserver = "https://matrix.example.com"
access_token = "syt_..."
user_id = "@bot:example.com"

[channels.whatsapp.my_wa]
dm_policy = "open"

[channels.signal.my_signal]
account = "+15551234567"
http_url = "http://127.0.0.1:8080"

For detailed configuration, see the per-channel pages: Telegram, Microsoft Teams, Discord, Slack, Matrix, WhatsApp, Nostr, Signal.

You can also use the web UI’s Channels tab for guided setup with each platform. Web-added channels do not get written back into moltis.toml.

For Matrix specifically, the web UI now supports the full normal setup flow:

  • password auth is the default because it unlocks encrypted chats
  • dedicated bot accounts default to moltis_owned so Moltis can bootstrap cross-signing and recovery
  • older Matrix accounts that need one external approval expose that approval flow in the channel card instead of failing silently

Proactive Outbound Messaging

Agents are not limited to replying in the current chat. Moltis supports three main outbound patterns:

  • send_message tool for direct proactive messages to any configured channel account/chat
  • update_channel_settings tool for safe in-chat edits to channel access rules, allowlists, and model routing
  • Cron job delivery for background jobs that should post their final output to a channel
  • Heartbeat delivery for periodic heartbeat acknowledgements sent to a chosen chat

Example send_message tool call:

{
  "account_id": "my-telegram-bot",
  "to": "123456789",
  "text": "Deployment finished successfully."
}

account_id is the configured channel account name, either from moltis.toml or from a channel account stored through the web UI, and to is the destination chat, peer, or room identifier for that platform.

Example update_channel_settings tool call:

{
  "account_id": "my-telegram-bot",
  "settings": {
    "dm_policy": "allowlist",
    "allowlist_add": ["alice"],
    "model": "openai/gpt-5"
  }
}

update_channel_settings intentionally supports a narrow patch surface. It is for non-secret channel settings only, not raw moltis.toml editing, token rotation, or arbitrary config mutation.

Access Control

All channels share the same access control model with three settings:

DM Policy

Controls who can send direct messages to the bot.

ValueBehavior
"allowlist"Only users listed in allowlist can DM (default for all channels except WhatsApp)
"open"Anyone can DM the bot
"disabled"DMs are silently ignored

Group Policy

Controls who can interact with the bot in group chats / channels / guilds.

ValueBehavior
"open"Bot responds in all groups (default)
"allowlist"Only groups on the allowlist are allowed
"disabled"Group messages are silently ignored

The group allowlist field name varies by channel: group_allowlist (Telegram, WhatsApp, MS Teams), guild_allowlist (Discord), channel_allowlist (Slack), room_allowlist (Matrix).

Mention Mode

Controls when the bot responds in groups (does not apply to DMs).

ValueBehavior
"mention"Bot only responds when @mentioned (default)
"always"Bot responds to every message
"none"Bot never responds in groups (DM-only)

Allowlist Matching

All allowlist fields across all channels share the same matching behavior:

  • Values are strings — even for numeric IDs, use "123456789" not 123456789
  • Case-insensitive"Alice" matches "alice"
  • Glob wildcards"admin_*", "*@example.com", "user_*_vip"
  • Multiple identifiers — both the user’s numeric ID and username are checked (where applicable)

Operators (Privileged Senders)

Passing the access gate lets someone talk to the bot. It does not let them run commands on your machine. That is a separate list:

[channels.telegram.my-bot]
allowlist = ["owner-id"]        # who may DM the bot
operators = ["owner-id"]        # who may use privileged access in proven DMs

```admonish warning title=“Upgrading: privileged commands are off until you set operators” Earlier versions treated the DM allowlist as the privileged list, so anyone allowed to DM the bot could run /sh, /approve, and /update. That fallback is gone: an empty operators list means nobody, including you.

After upgrading, add your own exact platform sender ID to operators for each account, or those commands will refuse everyone. If you do not know your ID, run /sh — the refusal message tells you what yours is on that channel.


```admonish warning title="Upgrading: shared chats start from an empty history once"
An untrusted turn only sees history from runs that were themselves untrusted, and
messages written before this change carry no such marker. The first turn after
upgrading in a group chat, guild, or channel-bound session therefore starts with
no prior context. This happens once per session; everything written from then on
accumulates normally.

A command requires an operator DM when the worst case reaches beyond the current chat. Everything scoped to the conversation you are already in stays open to any sender who clears the access gate:

Commands
Anyone who may chat/help, /new, /clear, /compact, /title, /fork, /stop, /model, /mode, /fast
Operators in proven direct chats only/sh, /update, /approve, /deny, /approvals, /sandbox, /attach, /sessions, /context, /insights, /peek, /btw, /rollback, /agent, /steer, /queue

The public set can only disrupt the room’s own conversation — something any member can already do by talking. The operator set runs host commands (/sh, /update), acts on the owner’s behalf (/approve and /deny resolve the owner’s pending exec requests, which is code execution by proxy), weakens isolation (/sandbox can turn the sandbox off), or reads state outside the current chat (/attach, /sessions, /context, /insights, /peek, /btw).

New commands default to operator direct-chat only, so adding one is safe until it is deliberately reviewed.

Guest, shared-room, and unknown-topology channel turns receive no tools. The gateway applies both the registry’s public audience ceiling and a deny-all name policy, so account, group, and per-sender tool policies cannot restore even a tool reviewed for other untrusted origins.

Every normal turn in a shared room is untrusted, including turns sent by an operator, because the shared history contains messages from other people. /sh, shell command mode, and every privileged command are denied there. In an operator’s conversation that the adapter can prove is direct, normal agent turns may use the full configured tool set. Unknown chat kinds fail closed as shared.

Untrusted turns also omit owner-private prompt context: user profile, project context, long-term memory, skills, and automatic memory extraction.

Channel-bound sessions

A session is bound to a chat when the chat created it, or when an operator ran /attach to move an existing session into that chat. Binding is what makes the chat’s replies land in the right place — and it also means the session’s history contains messages the bot did not author.

Every request into a bound session that does not come from the channel gateway is therefore treated as untrusted, whoever made it: web UI turns, cron jobs, webhooks, and the sessions_send tool all run with no tools and no private context. The downgrade is logged (session is bound to a channel; running this turn without tools or private context), because otherwise an affected cron job simply answers as though it had no tools.

Granting operator

Two places, and both state what it means:

  • When approving a sender. Approving asks for a role: Guest (may chat) or Operator (may run commands on this machine). While an account has no operators at all, Operator is pre-selected — on a fresh install the person holding the OTP code read it off this very UI, so they are almost certainly the owner. Once one operator exists, Guest is the default.
  • Settings → Channels → Edit → Operators, to add or remove IDs directly.

Approving as operator records the sender’s exact platform ID, not their username, because operator matching is exact.

Resolution is fail-closed:

operatorsallowlistWho is an operator
non-emptyanythingonly exact sender IDs in operators
emptyanythingnobody — privileged commands are disabled

A sender with no identifier (for example an unattributed button callback) is never an operator.

operators contains exact, case-sensitive platform sender IDs. Globs, usernames, and partial IDs do not grant privilege. For WhatsApp use the full JID; for Matrix use the full Matrix user ID.

Untrusted tool restrictions stack with the per-channel tool policy (channels.<type>.<account>.tools.groups.<chat_type>). Those policies can further restrict an operator DM, but cannot enable tools for a guest, shared room, or unknown chat. Grant eligibility for privileged access by adding the sender to operators; the sender must still use a proven direct chat.

OTP Self-Approval

Channels that support OTP (Telegram, Microsoft Teams, Slack, Discord, Matrix, WhatsApp) allow non-allowlisted users to self-approve by entering a 6-digit code. The code appears in the web UI under Channels > Senders. See each channel’s page for details.

FieldDefaultDescription
otp_self_approvaltrueEnable OTP challenges for non-allowlisted DM users
otp_cooldown_secs300Lockout duration after 3 failed attempts