此页面尚未翻译成你的语言,因此以英文显示。
Channels
A channel puts an agent in a chat platform. Someone mentions the bot in Slack, DMs it on Telegram, or messages it in a Feishu group; Hub routes that conversation to an agent on one of your daemons and posts the answer back into the same conversation.
Slack / Telegram / Discord / Google Chat / Feishu / Zalo / WhatsApp
│
▼
Hub ──► daemon ──► agent session
▲ │
└──────── reply ─────────┘
Three resources make that work, and they are separate on purpose:
- A Connection owns the credential — one Slack workspace installation, one bot token, one Zalo account you logged in to. Hub encrypts it and never shows it again. A bot has one Connection: adding a second one for a bot Hub already holds is refused and names the Connection that has it, since two would both run the bot and split its messages.
- A Channel account is the behaviour attached to that Connection: transport settings, ordered Routes, access, reply synchronization. Adding a Connection adds its account, so the bot runs at once: before any Route it answers
/statuswith chat IDs and/mewith the sender's ID (use a DM where group IDs are private) and remembers who wrote, which is how you find the people to name in a Rule. Nobody reaches an agent until a Route lets them in. - A Route is where a message goes — a direct agent, a Bot, or an Automation — and its Rules are the ways in. The first Route with a Rule that lets the message in wins; a sender no Route admits is refused.
How Hub works covers the resource model. This section covers the platforms.
The eight channels
| Channel | Credential | Inbound transport | Needs a public HTTPS URL |
|---|---|---|---|
| Slack | Bot token + app token | Socket Mode | No |
| Telegram | Bot token | Bot API polling | No |
| Discord | Bot token | Gateway | No |
| Google Chat | Service-account JSON + subscription | Cloud Pub/Sub | No |
| Feishu / Lark | App ID + app secret | Long connection | No |
| Zalo Official Bot | Bot token | Bot API polling | No |
| Zalo Personal | A QR login to a personal account | Push socket | No |
| A QR login | WhatsApp Web socket | No |
Channel webhook transports are not supported. Hub refuses Slack Events API, Telegram, Google Chat HTTP, Feishu and Zalo webhook configurations even when credentials are supplied. Use the supported transport in the table; none needs a public URL. Provider event triggers are separate integrations.
Zalo Personal and WhatsApp are the odd ones: there is no token to paste. You log in to each by scanning a QR code with a phone, and the session ends from time to time, so you log in again. Everything that account can do, a human on that account can do. Read their pages before you use them.
Capability matrix
Integration capabilities, subject to each page's direction, settings and limits. A cell does not promise both inbound and outbound support or live verification. A blank cell means this integration does not provide the capability.
| Capability | Slack | Telegram | Discord | Google Chat | Feishu | Zalo Bot | Zalo Personal | |
|---|---|---|---|---|---|---|---|---|
| Text messages | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Threads | ✅ | ✅ | ✅ | ✅ | ✅ | |||
| Mentions | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Rich formatting | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Long-message chunking | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Media | ✅ | ✅ | ⁷ | ¹ | ⁷ | ✅ | ||
| File attachments | ✅ | ✅ | ⁷ | ⁷ | ✅ | |||
| Reactions | ✅ | ✅ | ² | ✅ | ✅ | ✅ | ||
| Edit messages | ✅ | ✅ | ✅ | ✅ | ✅ | |||
| Delete messages | ✅ | ✅ | ✅ | ✅ | ||||
| Voice messages | ✅ | ✅ | ✅ | ✅ | ||||
| Video | ✅ | ✅ | ✅ | |||||
| Video notes | ✅ | |||||||
| Location | ✅ | ✅ | ||||||
| Polls | ✅ | ✅ | ✅ | ✅ | ||||
| Forum topics | ✅ | |||||||
| Tables and charts | ✅ | ⁹ | ✅ | |||||
| Buttons | ✅ | ✅ | ⁸ | ³ | ⁴ | |||
| Select menus | ✅ | ✅ | ⁸ | ³ | ⁴ | |||
| Approval prompts | ✅ | ✅ | ⁶ | |||||
| Native commands | ✅ | ✅ | ✅ | ⁵ | ||||
| Group DMs | ✅ | ✅ | ||||||
| Emoji discovery | ✅ | ✅ | ✅ |
¹ Inbound images and outbound images by public HTTPS link; no upload endpoint.
² Outbound reactions only; inbound reaction events are not wired yet.
³ Card clicks arrive inbound; Hub renders no card of its own.
⁴ Card clicks arrive inbound; Hub renders no Lark card of its own.
⁵ The operator registers /clisbot in the Google Chat app console; its command normalization is implemented but not verified live.
⁶ Answered by reacting to the prompt (👍, 👎, 1️⃣–4️⃣); WhatsApp has no buttons.
⁷ Outbound only; inbound attachments are not downloaded.
⁸ Outbound components render, but their clicks are not routed to Hub.
⁹ Native tables; charts fall back to text.
In the app, Channels → Channel Integrations → What it supports describes the integration. A Connection's Status describes its runtime health. There is no per-Connection capability verification state; dated live evidence is listed separately below.
Commands in a conversation
Use Channel slash commands to choose a Project or Workspace, inspect Git, start or fork a session, change its configuration, answer approvals, and open the same work in the app. Start with /help or @bot /help in a group.
Commands act on this conversation's session. Outside a DM, name the bot even when ordinary messages need no mention; a Telegram /status@yourbot and an addressed @bot /status reach the same command. Native /clisbot is another entry point where configured. The guide covers channel-specific spellings and permissions.
Permission requests
Each route decides what happens when the agent's provider asks before running a
tool. In the app this is Permissions on the route: Ask authorized
members, Deny, or Accept automatically. Accept automatically answers
every request with Allow, for providers whose own modes still stop for approval
(OpenCode's build mode, Claude's auto mode review). The Hub saves it with a
warning.
A question from the agent (Claude's AskUserQuestion) is not a permission: the
permission choice never decides it, and anyone the route lets talk in the
conversation may answer it. The route's questions: setting, under
Permissions in the app, decides how it is answered:
questions: | In the app | What happens |
|---|---|---|
ask (default) | Ask in the conversation | The question is posted; anyone the route admits answers it. |
recommended | Pick the recommended answer | Each question gets the option labelled recommended, else the first option. |
agent-decides | Let the Agent decide | The agent is told nobody can answer and picks the option it recommends. |
routes:
- audience: [...]
agent: worker
approval: [{ match: "*", mode: auto-allow }]
questions: recommended
questions: is also a defaults: leaf, inherited organization → account →
route.
Configuration commands offer only profiles, providers, models and effort allowed by your own Access grants. A bare /model shows current/next configuration; /model list discovers choices. Same-provider model changes apply live; a different provider is staged until /new or /fork. See Choose an Agent configuration.
Route defaults
A route starts new sessions with its agent: from hub.yml. agentControls:
overrides that for one route without editing an agent other routes or
automations share:
routes:
- audience: [{ who: { roles: [member] }, where: { conversations: [C0APP] }, contains: deploy }]
agent: worker
environment: repo
agentControls: { provider: claude, model: claude-opus-5, thinkingOptionId: high }
Naming a provider makes the block the whole setup: the agent's model, mode,
thinking option and feature values are replaced, anything you leave out is
unset, and the agent's options stay only when the provider is the same.
Without a provider, the block overrides only the fields you set.
/promoteroutedefault writes this leaf from a conversation: it publishes a new
Channel revision that makes the conversation's current setup the default of the
route that served it. It needs channel.manage — an organization owner or admin,
or Manage on that Connection in Access — and the new default must be
something the sender could start themselves. Only the route being changed is
checked against the sender's access; the organization's other routes are not. Running sessions keep their setup; the next
session on the route uses the new default, and no account restarts.
What the agent receives
Every message reaches the agent with its sender, on every route:
An Nguyễn (slack:U0000000001, @an.example): Create a CS card for QR tickets
Messages in the same conversation that did not wake the agent (no mention, or
the follow-up window had closed) are kept and sent before the next message that
does, marked as quoted context rather than instructions. Each is sent once. A
message the Hub could not deliver (its session failed to start) is kept the same
way, and the sender is asked to write again. /new and /fork start without
the earlier context.
defaults:
interaction:
whenBusy: steer # steer | queue: add to the running turn, or wait for it to end
context:
unmentioned: everyone # everyone | allowed-senders | none
maxMessages: 20 # 0–200
batching: off # or { pauseSeconds: 3, maxWaitSeconds: 10, maxMessages: 20 }
batching holds a message until no new one has arrived for pauseSeconds, the
first has waited maxWaitSeconds, or maxMessages are waiting, then sends them
as one prompt. maxWaitSeconds must be greater than pauseSeconds. Write
batching: off on a route to turn off what its account turned on. Every leaf is
inherited organization → account → route.
Where a conversation's work lands
Without an explicit location choice, Hub gives each new channel session a Workspace named from its first message. /fork and /side continue in the source Workspace; /new and /quick start another. Renaming a Workspace yourself preserves its name.
Use /project and /workspace to choose where the next session runs, /workspace new to create it before the Agent starts, and /branch or /worktree to inspect Git. Choices are shared within the conversation's binding scope. See Choose a Project and Workspace for examples, permissions and /new//fork behavior.
An operator can disable automatic organization with defaults.workspace.organize: false; sessions then use the Host's placement and naming behavior.
Editing a route while conversations are running
A deployed change applies to live conversations, it does not strand them. Changing how a route talks — synchronization, templates, mention rules, approval rules, who may talk to it — keeps every bound conversation on its session, and the new rules apply from the next message. Two edits do end a session, because they change where the conversation goes:
- pointing the route at another
agent:or at a workflow retires the session and starts one at the new target; - narrowing or removing the route so it no longer matches the conversation leaves it unserved, and the bot stops answering there.
Rules: how a message gets in
A route is where messages go: the agent or Automation, the Host, how it replies, its limits. Its Rules are the ways in, and a message gets in when any Rule matches it. Rules are the only place that decides who may chat; the People & access › Access tab grants only Connection Admin. Each Rule has:
- Where: direct messages, or group chats — every chat the bot is in, public only, private only (Slack and Telegram), or the chats you pick from one search box that lists every conversation the bot has seen.
- Who: only owners, owners and admins, everyone on the Hub (every linked Member), people you pick (roles, Teams, Members, or senders outside the Hub, picked from the people who already messaged the bot or typed as channel user ids), or anyone (unlinked senders included).
- When: Require a mention, Continue without a mention for some
minutes after the bot's last reply (
interaction.followUp), and an optional text the first message must contain (contains). A Rule that leaves one out inherits it from the account and organization defaults.
A new route starts with one Rule: only owners, in direct messages, answered without a mention. A group-chat Rule starts with a mention required. One route can answer DMs without a mention and a support room only when mentioned, with two Rules instead of two copies of the same route.
A Hub member is recognized on a channel only through a linked identity, so a Rule for owners lets nobody in until the owner links their account. The route form offers Link my Slack account (or the channel at hand) in place when a Rule names you and the bot cannot recognize you yet.
Public or private is what the platform says on the message: a Slack channel's type, and on Telegram a group with a public @username is public, any other group private. Other channels do not say, so a public-only or private-only rule matches no group chat there; the editor warns when a rule asks for it.
routes:
- audience:
- who: { roles: [owner] }
where: { dm: true }
interaction: { requireMention: false }
- who: { teams: [team-qc] }
where: { groups: public }
interaction: { requireMention: true, followUp: { mode: auto, ttlMinutes: 10 } }
- who: { anyone: true }
where: { conversations: [C0HELP] }
contains: deploy # optional text filter for this Rule
interaction: { whenBusy: queue } # what happens once a message is in
agent: worker
environment: repo
Hubs upgraded from a release that kept requireMention, followUp or
contains on the route move them onto each Rule at start. On a route that set
requireMention, a DM-only Rule that did not set its own is saved as answering
without a mention, which is what the app always promised.
Routes stay ordered. A route applies when a Rule's Where covers the
conversation (and, for a new conversation, that Rule's contains matches if
set); if the sender matches none of that route's Rules, the next route is tried. A bound conversation does not fall
through: the route that started the session owns it, and a sender that route
refuses is told in the thread to start their own conversation with a new
message outside it. There is no catch-all: a
sender no route admits is refused. To answer everyone else, add a last route
whose rule covers them.
Talking to a bot and reaching a Host or Project stay separate. A chatting
sender can start sessions with the route's configuration and use
/stop, /new, /fork, /side, /quick, /steer, /queue, /skill and
/command; chat gives no access to the Host, the Project, or the Clisbot app.
/agent, /model, /provider, /effort, /permission, /cowork and
/resume need the sender's own Access grants on the route's Project, and
answering an approval needs an approval.* privilege.
To find the IDs a Rule takes, send /status in the chat (its chat and
thread/topic IDs) and /me (your sender ID; on Telegram, send it to the bot in
a DM). Both answer anyone, so a person the bot does not let in can send them
to an admin. They show the session only to a Hub Member the route lets in, and
why a sender is refused only to someone who manages the Connection. A sender
the route does not let in gets one answer per command every 30 seconds. To
make the bot silent to them instead, set:
defaults:
interaction:
publicCommands: false # /help, /me and /status answer only senders a route lets in
The person who publishes a route vouches for what it runs: saving checks that they may hand out its Host, Project, Agent configuration and automatic approvals. A Connection Admin edits the routes of one account through the app and keeps its Connection as it is. Only a route whose target, approvals, reply path or Agent controls changed is checked against their access, so a Connection Admin can change who may talk to a route they could not publish. The bot token and every credential in the account's settings are never shown to a Connection Admin and survive their saves unchanged.
Outside a DM, commands must address this bot. Each Rule's requireMention
and follow-up window decide when an ordinary message wakes the agent; they
do not remove command addressing.
Routes open to Anyone
A rule with anyone: true works like any other. The Hub saves it and lists a
warning on the route for each wide choice: no named conversations, no mention
needed, a follow-up window that lets anyone talk without a mention, output
beyond the final answer, permission requests accepted automatically, Fast mode, or a
mode that runs tools without asking. The app lists them before you confirm a
save. New routes start from the safe side: a mention in groups, final answers
only, and permission requests put to authorized members (chat admission alone grants no approval authority).
A route takes its audience only as a list of rules. A file that still carries a
route match:, a one-value audience: { kind: … } or an account fallback:
fails validation.
Limits
Limits sit on each Rule, on the route as a whole, and on the account (the whole bot, and each conversation):
| Field (app label) | Counts | Rule | Route | Account |
|---|---|---|---|---|
maxInputCharacters (Message length) | characters in one incoming message | ✓ | ✓ | |
messagesPerMinutePerSender (Messages handled per minute, per person) | messages from one person that start or continue work | ✓ | ✓ | |
messagesPerMinute (Messages handled per minute) | messages that start or continue work | ✓ | ✓ | ✓ |
messagesSentPerMinute (Bot messages per minute) | new messages the bot posts | ✓ | ||
maxConcurrentRuns (Concurrent runs) | agent turns running at the same time | ✓ | ✓ | ✓ |
maxRuntimeSeconds (Run time) | how long one turn may run before it stops | ✓ | ✓ |
Chatter the bot ignores and status commands are not "handled" messages.
limits: # the whole bot
maxConcurrentRuns: 20
messagesSentPerMinute: 120
perConversation: # each channel, group or DM; threads count toward their channel
messagesPerMinute: 30
routes:
- audience:
- { who: { roles: [owner] }, where: { dm: true } }
- who: { anyone: true }
where: { conversations: [C0SUPPORT] }
limits: { messagesPerMinutePerSender: 3 } # this way in only
limits: { maxConcurrentRuns: 5 } # every Rule of the route together
A message must fit the bot, its conversation, its route and the Rule that let
its sender in. A sender let in by several Rules counts in one that names them
before one that lets anyone in, so an owner never shares the strangers' limits.
Each field is a positive number, off, or left out. A Rule's field left out
takes the route's; left out there too, it means no limit, except on a Rule that
lets anyone in, which defaults to 8000 characters, 10 messages per person and 60
per minute, 8 concurrent runs and 900 seconds. A route field set or turned off
replaces that default, as it always did. There is no ceiling: set any number or
turn a default off. In the app, a Rule's limits are under the Rule, the route's
under Route limits, and the bot's under the Connection's … menu,
Limits.
Over a limit, an incoming message waits in the ingress queue and runs when there
is room, and the bot says once that it is queued. A message longer than
maxInputCharacters is refused with a short reply. /fork, /side, /quick,
/steer and /queue count like messages. Outgoing messages over
messagesSentPerMinute are delayed and keep their order within a conversation.
Typing, reactions, edits and streaming drafts are not counted. Delayed outgoing
messages are held in memory: saving a change to the account, or restarting the
Hub, drops the ones still waiting.
Allowlists, pairing, and who may talk to the bot
access: is the sender gate. It uses OpenClaw's names and semantics, so an
account file written for OpenClaw works unchanged, and it is authorable at any
layer — organization policy, account, or a single route:
defaults:
access:
dmPolicy: pairing # open | pairing | allowlist | disabled
groupPolicy: allowlist # open | allowlist | disabled
allowFrom: ["U0ALICE"] # `*` admits anyone
groupAllowFrom: ["U0ALICE", "U0BOB"] # falls back to allowFrom when absent
deniedReply: "Ask an operator for access." # omit to refuse silently
Omit access: and the Route's Rules still decide who may talk. Add a leaf and
this sender gate runs before the Rules: both must allow. Personal Host/Project
Access is checked separately when a command needs it.
A refused sender never reaches an agent. The refusal is recorded in channel
activity with its reason (dm_policy_not_allowlisted, group_policy_disabled,
…) and answered only if you set deniedReply.
Pairing
With dmPolicy: pairing, an unknown sender's first DM gets a six-character code
instead of an agent, and you decide:
curl -H "authorization: Bearer $KEY" \
"$HUB/api/management/organizations/$ORG/channel-accounts/telegram/support/pairing"
curl -X POST -H "authorization: Bearer $KEY" -H "content-type: application/json" \
-d '{"senderIdentity":"telegram:77001"}' \
"$HUB/api/management/organizations/$ORG/channel-accounts/telegram/support/pairing/approve"
.../pairing/deny is the other half. Approving adds the sender to that
account's allowlist; denying is final — a denied sender is never handed a new
code. The code is a handle so you can tell two strangers apart, not a secret:
knowing it grants nothing.
The requests belong to one organization and one account. Approving someone on one account does not admit them on another, and never on another organization's.
How the agent replies
A Route's Reply method decides what reaches the conversation:
| Reply method | What the user sees |
|---|---|
| Hybrid | The agent's answer as text, plus the files, reactions and edits it sends with its message tool. The default for a new Route, including Anyone routes. |
| Text forward | The agent's answer as text. The agent cannot send files or act on messages. |
| Channel tool only | Only what the agent sends with its message tool, including short progress updates on long work. |
The Clisbot app shows the same answer the conversation got on the first two. On Channel tool only, the agent's own messages stay in the app and the conversation sees the tool's posts; if the agent finishes without sending a reply, its last message is sent instead. A turn started in the Clisbot app is not sent to the conversation on this method. Progress updates are limited to one every 30 seconds.
If a turn fails, the conversation gets one notice with the error. A turn you
stop on purpose (/stop, or a message that interrupts it) posts nothing.
Files the agent sends
One tool sends both. The agent's message tool takes attachments alongside the
text — one file or many, any type, in the order it names them. Each file becomes
its own post in the same conversation, so a reply that says "here is the report"
and attaches it arrives as two messages, not one.
Local files are read from the Agent's Host using the permissions of the Host user running it, including readable paths outside the Project and symlinks. They are not confined to the Project root. An older Host asks you to update for file transfer. Files over the upload cap (Telegram 50 MB, Slack 250 MB, Discord 10 MB, Zalo Personal 100 MB, WhatsApp 50 MB) produce a visible notice.
Channels with no upload endpoint — Google Chat, Feishu, Zalo Bot — refuse the attachment by name. See each channel's page.
Manage accounts from the CLI
Four verbs, all against a running Hub:
clisbot channels add <channel> --account <id> [--connection-id <uuid> | --secret-file <path>]
clisbot channels ls
clisbot channels status
clisbot channels rm <channel> --account <id> --yes
add never takes a credential on the command line — a secret in argv lands in ps output and shell history. Pass a file and delete it afterwards. Each channel's page shows the file shape it expects.
status adds the pin, integrity, and load-trace columns to what ls shows, and is the first thing to run when an account is not behaving.
rm drops the account from the channel configuration and stops its transport. It refuses without --yes. The Connection holding the credential stays, and rm prints its id so you can retire it from the app.
ls also answers to list, the name it shipped under.
All four accept --hub <origin> to target a Hub other than the local one (with --api-key), --home <path> to pick a different Clisbot home, and --json.
Verified live
A capability is only "verified live" when a message from a real account on the real platform reached an agent and the agent's answer came back to the same conversation, read back through the platform's own API.
| Channel | Live evidence |
|---|---|
| Slack | Recorded — mention → reply in thread, long chunked replies, outbound files (2026-09-07). |
| Telegram | Recorded — mention → reply, forum topics, files in and out, edit and poll actions, /new and /stop (2026-09-07). |
| Discord | No. Tested against a simulated platform only; needs a bot token and a test guild. |
| Google Chat | Recorded, DMs over Cloud Pub/Sub (2026-10-06). Spaces, cards and the HTTP endpoint are not verified. |
| Feishu / Lark | No. Tested against a faked Lark SDK client only; needs Lark app credentials. |
| Zalo Official Bot | No. Polling E2E is runnable with a bot token and a human sender; not yet run. |
| Zalo Personal | No. Needs a human to scan a QR code. |
| No message loop. QR start/poll/cancel recorded 2026-10-06; scan and messages need a spare number. |
Each channel page carries its own per-capability line. The running record of what passed and what failed, with message ids, is docs/tests/channels/p0-live-scenarios.md in the repository.
Before you connect one
A channel is an untrusted input. Anyone who can post in a conversation your Route matches can send text to an agent that has a working directory, a shell, and network access. Read Hub security and set the account's access rules before you point a Route at a real project.