View as markdown

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
        │
        ▼
      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 linked Zalo account. Hub encrypts it and never shows it again.
  • A Channel account is the behaviour attached to that Connection: transport settings, ordered Routes, access, reply synchronization.
  • A Route picks what a matching conversation runs — a direct agent, or an Automation. The first Route whose audience rules admit the sender wins; a sender no Route admits is refused.

How Hub works covers the resource model. This section covers the platforms.

The seven channels

ChannelCredentialInbound transportNeeds a public HTTPS URL
SlackBot token + app tokenSocket Mode, or Events API webhookWebhook only
TelegramBot tokenBot API polling, or webhookWebhook only
DiscordBot tokenGatewayNo
Google ChatService-account JSONHTTP webhookYes, always
Feishu / LarkApp ID + app secret (+ webhook secrets)Long connection, or webhookWebhook only
Zalo Official BotBot tokenBot API polling, or webhookWebhook only
Zalo PersonalA QR-linked session for a personal accountPush socketNo

Zalo Personal is the odd one: there is no token to paste. You link it by scanning a QR code with a phone, and the session expires and has to be relinked. Everything that account can do, a human on that account can do. Read its page before you use it.

Capability matrix

What each vertical implements. A blank cell means the platform or the vertical has no such thing; footnoted cells work with a restriction.

CapabilitySlackTelegramDiscordGoogle ChatFeishuZalo BotZalo 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 only; the Zalo Bot API has 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. ⁵ Slash commands and interaction callbacks are not wired yet.

The same matrix appears per account under Channels → Channel Integrations in the Clisbot app, where a claimed capability shows as Not verified until the account has actually exercised it. The catalog is a claim; a green check in the app means evidence.

Commands in a conversation

The same verbs work on every channel, with or without a leading /, and with or without a mention in front. An addressed @bot /new and a Telegram-style /new@yourbot both normalize to the same command.

CommandWhat it does
/new, /resetStart a fresh agent session for this conversation.
/stop, /cancelStop what the agent is doing.
/status, /stateReport what the conversation is bound to.
/helpList the commands.
/agent <name>Switch this conversation to another agent.
/model <name>Switch this conversation to another model.
/approve, /denyAnswer a pending permission request.
/routedefaultShow the route serving this conversation and its default.
/promoteroutedefaultMake this conversation's setup that route's default; undo reverts its last change.

Approve and deny also arrive as button presses where the platform has buttons.

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 appWhat happens
ask (default)Ask in the conversationThe question is posted; anyone the route admits answers it.
recommendedPick the recommended answerEach question gets the option labelled recommended, else the first option.
agent-decidesLet the Agent decideThe 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.

/agent and /model only offer what the route lists:

routes:
  - audience: [{ who: { roles: [member] }, where: { conversations: [C0APP] } }]
    agent: worker
    environment: repo
    agents: [reviewer] # plus `worker`, the route's own — always offered
    models: [gpt-5.6-luna, claude-sonnet-4.6]

A route that lists neither refuses both commands. A bare /model prints the menu. Switching ends the running session, because an agent's model is fixed when it starts; the next message opens a session on the new choice, and the choice outlives /new.

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

Each conversation gets its own workspace on the daemon, named from the first message it sends — Fix the flaky login test, not repo, repo, repo. Commands that continue that work (/fork, /side) open their session in the same workspace; commands that start something else (/new, /quick) open a new one. Renaming a workspace yourself is safe: automatic naming never overwrites a name you set.

Turn it off per organization, account, or route and the daemon places and names sessions as it did before:

defaults:
  workspace: { organize: false }

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.

Who may talk, where

Each route carries audience rules. A rule is one sentence, "[who] may talk in [where]", and a sender is admitted when any rule of the route matches. Rules are the only place that decides who may chat; the People & access › Access tab grants only Connection Admin.

  • Who: Owner, Admins, Members (every linked Member), Teams, named Members, Anyone (unlinked senders included), or senders outside the Hub, picked from the people who already messaged the bot (or typed as channel user ids).
  • Where: two switches, both off on a new route. Direct messages is either all of them or only named senders (dmTeams, dmMembers, and dmIdentities for Guests: of the Who, only these may DM). Group chats is exactly one of all group chats, public only, private only (Slack and Telegram), or specific conversations picked from one search box that lists every conversation the bot has seen. A rule that covers group chats does not cover DMs unless its Direct messages switch is on.

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, admin] }
        where: { dm: true, groups: all }
      - who: { teams: [team-qc] }
        where: { groups: public }
      - who: { teams: [team-qc] }
        where: { dmMembers: [membership-id-of-the-lead], conversations: [C0QCPRIVATE] }
      - who: { anyone: true }
        where: { conversations: [C0HELP] }
    contains: deploy # optional text filter, route-level
    agent: worker
    environment: repo

Routes stay ordered. A route applies when a rule's Where covers the conversation (and contains, if set, matches); 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 /status, /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.

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.

Commands never need a mention. requireMention decides when a plain message wakes the agent.

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, tool requests denied.

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 are available on every route and on the account, with the same fields everywhere:

FieldCounts
maxInputCharacterscharacters in one incoming message
messagesPerMinutePerSendermessages received from one sender
messagesPerMinutemessages received
messagesSentPerMinutenew messages the bot posts
maxConcurrentRunsagent turns running at the same time
maxRuntimeSecondshow long one turn may run before it stops
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: [member] }, where: { conversations: [C0SUPPORT] } }]
    limits: { maxConcurrentRuns: 5, maxRuntimeSeconds: off }

A message must fit the bot, its conversation and its route. Each field is a positive number, off, or left out. Left out means no limit, except on an open-audience route, which defaults to 8000 characters, 10 messages per sender and 60 per minute, 8 concurrent runs and 900 seconds. There is no ceiling: set any number or turn a default off.

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 the whole access: block and nothing is gated by it — the Hub's own roles decide, exactly as they did before the block existed. Add one leaf and the gate runs in front of them: both have to allow.

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 methodWhat the user sees
HybridThe agent's answer as text, plus the files, reactions and edits it sends with its message tool. The default for a new Route open to members.
Text forwardThe agent's answer as text. The agent cannot send files or act on messages.
Channel tool onlyOnly 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.

A file has to live under the Project the session is bound to. A path outside it is refused and the agent is told why; the same goes for a file over the channel's upload cap (Telegram 50 MB, Slack 250 MB, Discord 10 MB), which posts a visible notice in place of the file rather than dropping it silently.

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 — another account can be using it — 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.

ChannelLive evidence
SlackYes — mention → reply in thread, long chunked replies, outbound files (2026-09-07).
TelegramYes — mention → reply, forum topics, files in and out, edit and poll actions, /new and /stop (2026-09-07).
DiscordNo. Tested against a simulated platform only; needs a bot token and a test guild.
Google ChatNo. Blocked on a public HTTPS endpoint and Workspace admin approval.
Feishu / LarkNo. Tested against a faked Lark SDK client only; needs Lark app credentials.
Zalo Official BotNo. Polling E2E is runnable with a bot token and a human sender; not yet run.
Zalo PersonalNo. Needs a human to scan a QR code.

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.