Channels
Channels let you interact with a Qwen Code agent from messaging platforms like Telegram, WeChat, QQ, DingTalk, WeCom, or Feishu, instead of the terminal. You send messages from your phone or desktop chat app, and the agent responds just like it would in the CLI.
Code-hosting platforms (starting with GitHub) and authenticated workspace accounts (starting with DingTalk Workspace) are also supported through channels. Email connects a dedicated mailbox through IMAP and SMTP.
How It Works
When you run qwen channel start, Qwen Code:
- Reads channel configurations from your
settings.json - Spawns a single agent process using the Agent Client Protocol (ACP)
- Connects to each messaging platform and starts listening for messages
- Routes incoming messages to the agent and sends responses back to the correct chat
All channels share one agent process with isolated sessions per user. Each channel can have its own working directory, model, and instructions.
Quick Start
- Set up a bot or authenticated workspace account (see channel-specific guides: Telegram, WeChat, QQ Bot, DingTalk, DingTalk Workspace, WeCom, Feishu, GitHub, Email)
- Add the channel configuration to
~/.qwen/settings.json - Run
qwen channel startto start all channels, orqwen channel start <name>for a single channel
Want to connect a platform that isn’t built in? See Plugins to add a custom adapter as an extension.
Configuration
Channels are configured under the channels key in settings.json. Each channel has a name and a set of options:
{
"channels": {
"my-channel": {
"type": "telegram",
"token": "$MY_BOT_TOKEN",
"privatePolicy": "allowlist",
"allowedUsers": ["123456789"],
"sessionScope": "user",
"cwd": "/path/to/working/directory",
"instructions": "Optional system instructions for the agent.",
"groupPolicy": "disabled",
"groups": {
"*": { "requireMention": true }
}
}
}
}Options
| Option | Required | Description |
|---|---|---|
type | Yes | Channel type: telegram, weixin, qq, dingtalk, dws, wecom, feishu, github, gitlab, email, or a custom type from an extension (see Plugins) |
token | Telegram | Bot token. Supports $ENV_VAR syntax to read from environment variables. Not needed for WeChat, DingTalk, WeCom, or Feishu |
clientId | DingTalk, Feishu | DingTalk AppKey or Feishu App ID. Supports $ENV_VAR syntax |
clientSecret | DingTalk, Feishu | DingTalk AppSecret or Feishu App Secret. Supports $ENV_VAR syntax |
botId | WeCom | WeCom intelligent robot Bot ID. Supports $ENV_VAR syntax. See WeCom |
secret | WeCom | WeCom intelligent robot Secret. Supports $ENV_VAR syntax. See WeCom |
model | No | Model to use for this channel (e.g., qwen3.5-plus). Overrides the default model. Useful for multimodal models that support image input |
privatePolicy | No | Private access: disabled, allowlist, pairing, or open; new managed channels default to pairing |
allowedUsers | No | Private user IDs allowed without pairing (used by private allowlist and pairing policies) |
sessionScope | No | How sessions are scoped: user (default), chat_thread, or single. Legacy thread remains compatible when already configured but is not offered for new Web Shell configurations |
sessionRotation | No | Start a fresh session after maxTurns routed messages or maxAgeHours elapsed, whichever comes first. Requires a positive bound and cannot be combined with multiSession. |
multiSession | No | Retain up to eight owner-scoped named tasks in one chat. Requires daemon-managed mode, sessionScope: "user", no webhooks or group-history backfill, and no enabled Channel loops |
cwd | No | Working directory for the agent. Defaults to the current directory |
approvalMode | No | Tool approval mode for channel sessions. Unattended webhook tasks require yolo; the setting applies to every session on the channel |
instructions | No | Custom instructions prepended to the first message of each session |
webhooks | No | Webhook sources and delivery targets for daemon-managed channels. See Webhook-triggered tasks |
groupPolicy | No | Group chat access: disabled (default), allowlist, pairing, or open. See Group Chats |
senderPolicy | No | Deprecated: allowlist, pairing, or open; used only when privatePolicy is absent and DM access is enabled |
dmPolicy | No | Deprecated: open or disabled; used only when privatePolicy is absent |
operators | No | Who may operate a shared session (/approve, /clear, /loop, …). Unset or empty grants no shared-session operator permissions. See Shared-Session Operators |
groupHistoryLimit | No | Opt-in group history backfill. 0 or omitted disables it. A positive number persists that many unmentioned group messages from senders admitted by the group member policy for the next bot mention/reply. |
groups | No | Per-group settings. Keys are group chat IDs or "*" for defaults. See Group Chats |
dispatchMode | No | What happens when you send a message while the bot is busy: steer (default), collect, or followup. See Dispatch Modes |
Message Routes
Use messageRoutes to run multiple workflows through one Channel connection.
Each key is a case-sensitive message prefix and each value contains instructions
for that route:
{
"messageRoutes": {
"/review": "Review the requested pull request.",
"/QA": "Answer questions about this repository."
},
"defaultMessageRoute": "/QA"
}A prefix must be followed by whitespace and a non-empty message. The longest
matching prefix wins; leading mentions are skipped when matching. The matched prefix is removed before the
message reaches the agent. Route instructions are combined with the Channel’s
common instructions on the first turn of each session. Routes use separate
sessions within the configured sessionScope. Existing Channel memory remains
shared at its configured chat/thread scope; routes are not a permissions sandbox.
Without defaultMessageRoute, unmatched chat messages are ignored. To accept
ordinary messages, set it to a key in messageRoutes; those messages keep their
original text and use that route’s instructions and session. Empty prefixes and
empty route maps are invalid. Route instructions may be empty. Prefixes and
instructions are trimmed when loading configuration. messageRoutes cannot be
combined with multiSession.
Routing does not grant access: private and group policies still apply. Shared
and agent commands also need a route prefix, for example /QA /help. Messages
without user-authored text and provider-generated system events remain outside
prefix routing.
Private Policy
privatePolicy controls private conversations independently of groups:
disabled— Ignore private messages without creating pairing requests.allowlist— Only users listed in the top-levelallowedUsersmay send private messages.pairing— Users inallowedUsersare admitted directly. Other users need a locally approved user pairing. See DM Pairing.open— Anyone can send private messages.
New managed channels default to pairing. The deprecated senderPolicy and dmPolicy keys remain supported: an explicit privatePolicy takes precedence; otherwise dmPolicy: "disabled" disables private access, and otherwise senderPolicy supplies the policy (default allowlist). These old keys do not restrict group members. Saving from the editor uses privatePolicy and preserves existing deprecated keys.
Shared-Session Operators
In a shared session (sessionScope: "chat_thread" or "single", and group chats under "thread"), some commands affect everyone in the conversation: /approve, /deny, /cancel, /clear, /who, /status, /loop, /btw, the loop tool, and steering an in-flight turn. Only the session’s operators may use them; other members’ messages queue instead of steering. Sessions that are not shared belong to their own sender, who may always use them.
Set operators to the user IDs allowed to manage shared sessions. Omitted or empty operators grants nobody this role. Private allowlists, user pairing, group pairing, and group member lists never imply operator permissions. Operators must still pass normal conversation admission.
For example, groups: { "*": { "senders": "open" } } with operators: ["alice"] lets every admitted group member start a turn, but only Alice can manage its shared session.
A saved loop is checked against the same rule when it fires, using its creator as the sender, and is disabled if the creator is no longer an operator.
Session Scope
Controls how conversation sessions are managed:
user(default) — One session per user. All messages from the same user share a conversation.chat_thread— One session per chat thread/topic, shared by participants in that thread.thread— Legacy thread/topic routing retained for existing configurations.single— One shared session for all users. Everyone shares the same conversation.
Named Tasks
Daemon-managed Channels can retain several named conversations for the same user in one chat:
{
"channels": {
"my-channel": {
"type": "telegram",
"sessionScope": "user",
"multiSession": true
}
}
}The catalog is private to the exact channel, chat, and sender. Task names use 1–32 ASCII letters, numbers, underscores, or hyphens, and are unique case-insensitively. Up to eight tasks may be open; closing a task detaches it without deleting its transcript, so selecting it later reopens the exact conversation. Session IDs are never accepted by or shown in chat commands.
Named results identify their originating task: direct chats use [task], while group chats use [sender · task]. Named text permission prompts also show the exact request ID and the corresponding /approve <id>, /approve-always <id>, and /deny <id> commands. The label is presentation-only and is not stored in the model transcript.
One task remains selected to receive the next normal message, but other named tasks may keep running concurrently. /session new <name> shares the configured workspace, while /session new <name> --worktree creates an isolated checkout for that task under the daemon workspace’s .qwen/worktrees/ directory. The daemon verifies the persisted worktree owner before reopening the task after a restart; a missing, changed, or foreign ownership record fails closed instead of silently moving the task into the shared workspace. Creating or selecting another task does not cancel or retarget earlier work, and late results retain their originating task label. A busy task cannot be closed, but its active prompt can be cancelled with /session cancel [<name>] through the existing Channel cancellation behavior. Independently queued turns are not cancelled, but in collect dispatch mode any follow-ups buffered behind the cancelled prompt are discarded by that existing behavior. Media preparation is not targeted. Bare permission commands apply only to the selected task, while an explicit request ID can answer an owned inactive task. /clear, /new, and /reset also work on a selected worktree task: the task gets a fresh conversation while its worktree and files are kept. A busy worktree task refuses the reset until its prompt finishes, and a task whose worktree record was damaged reports the failure without touching files. Channel memory remains scoped to the chat rather than to a named task.
This mode is unavailable in standalone qwen channel start, with webhooks, with non-zero channel or group groupHistoryLimit, or with Channel loops. If an enabled loop already exists for that channel, the daemon worker refuses to start until the loop is disabled.
Session Rotation
Long-lived routes can start a fresh conversation after a configured limit:
{
"channels": {
"my-bot": {
"type": "dingtalk",
"sessionScope": "chat_thread",
"sessionRotation": { "maxTurns": 200, "maxAgeHours": 24 }
}
}
}Either positive bound may be used alone; the first one reached triggers rotation before the next routed message. maxTurns counts messages routed to the session, including messages handled without a model turn. The count and age start persist across restarts. A route with a running or queued message waits for a later idle message to rotate, so steady traffic may pass the configured bound. Rotation clears that route’s conversation context, retires its old session, and posts a notice in the triggering chat or thread. With sessionScope: "single", other chats sharing the session do not receive the notice. Omitting sessionRotation preserves ongoing session reuse. Named tasks (multiSession) cannot use this option.
Channel Memory
Channel memory stores durable context for one chat or thread. Entries have stable IDs, so a list response can be used for deterministic follow-up operations.
记住:默认使用 staging 环境is the deterministic form and saves exactly one scalar entry for the current chat or thread.- To save several separate facts in one request, use a natural phrase routed
through the classifier. For example:
请记住这三条约定:使用 staging;发布前测试;优先中文回复creates entries that you can manage independently. Exact duplicate facts are skipped and reported without creating another entry. Requests containing credential-like text are rejected; remove secrets and save the non-sensitive facts separately. 查看记忆lists entries and their stable IDs. Use查看第 2 页记忆to view a later page,查看记忆 <id>to view one entry, or a natural filtered request such as只看中文偏好to list the matching entries.查看刚才那条记忆,把关于 staging 的记忆改成默认使用 production, and忘掉刚才那条work when the natural reference resolves to exactly one entry. Natural updates and removals first show the proposed change. Confirm an update with确认更新记忆orconfirm memory update, or a removal with确认删除记忆orconfirm memory removal, within 60 seconds. Exact-ID updates and removals remain immediate and do not need confirmation.清空记忆starts the clear-all confirmation flow;确认清空记忆completes it.
When a natural inspect, update, or removal request matches multiple entries,
the bot returns the candidate IDs and previews without changing memory. There
is no pending selection for an ambiguous result: retry the request with one
exact ID, such as 忘掉 m-a31f0d82c7e4. Exact-ID operations remain the
deterministic fast path. A natural request with no match reports that no entry
matched.
Pending update, removal, and clear confirmations apply only to the sender and chat or thread that created them. A newer clear, natural update, or natural removal proposal replaces an older pending one for that sender and target. Pending confirmations are discarded when the channel process restarts.
The legacy slash aliases /remember-channel, /channel-memory, and
/forget-channel have been removed. They are no longer channel-memory
commands.
Channel memory follows the channel access gates. Any message accepted by
private access, group admission, group member settings,
pairing, and mention requirements can read, write, update, or clear memory for
that chat or thread. Accepted members of the same group share that group’s
target store. Use a group’s senders: "allowlist" with its allowedUsers when group memory should be
limited to trusted senders — under senders: "open" every member of an
admitted group shares that group’s memory store.
Existing legacy CHANNEL.md memory is migrated automatically to structured
CHANNEL.json storage on the first mutation. Structured memory persists across
standalone channel and daemon-managed channel restarts, and is injected when a
fresh target-scoped session starts, including after /clear.
After that initial injection, each accepted message also recalls up to three relevant entries for that message. This keeps durable facts available during a long-running session without adding every stored entry to every turn. Recall is based on the current message and does not modify the stored memory.
Memory remains keyed to the current chat or thread. It is not injected or
recalled in a sessionScope: single session, because that session is shared
across the whole channel rather than scoped to one target.
Channel memory does not automatically learn facts from normal conversation or
accept 第一个 as confirmation for an ambiguous natural reference. Use a clear
remember request and an exact entry ID when a natural reference is ambiguous.
Token Security
Bot tokens should not be stored directly in settings.json. Instead, use environment variable references:
{
"token": "$TELEGRAM_BOT_TOKEN"
}Set the actual token in your shell environment or in a .env file that gets loaded before running the channel.
DM Pairing
When privatePolicy is set to "pairing", unknown senders go through an approval flow:
- An unknown user sends a message to the bot
- The bot replies with an 8-character pairing code (e.g.,
VEQDDWXJ) - The user shares the code with you (the bot operator)
- You approve them via CLI:
qwen channel pairing approve my-channel VEQDDWXJOnce approved, the user’s ID is saved to the channel’s workspace-scoped allowlist (~/.qwen/channels/<workspace-scope>/<name>-allowlist.json) and all future messages go through normally. Pairing state is scoped per workspace, so two workspaces using the same channel name keep separate approvals.
Pairing CLI Commands
# List pending pairing requests
qwen channel pairing list my-channel
# Approve a request by code
qwen channel pairing approve my-channel <CODE>Run these from the channel’s workspace directory (or pass --cwd <dir>) — pairing state is stored per workspace.
Pairing Rules
- Codes are 8 characters, uppercase, using an unambiguous alphabet (no
0/O/1/I) - Codes expire after 1 hour
- Maximum 3 pending requests per channel at a time, and at most one per sender — additional requests are declined until one expires or is approved
- Users listed in
allowedUsersinsettings.jsonskip user pairing; undergroupPolicy: "pairing", the group itself must still be approved - Approved users are stored per workspace in
~/.qwen/channels/<workspace-scope>/<name>-allowlist.json— treat this file as sensitive
Group Chats
By default, the bot only works in direct messages. To enable group chat support, set groupPolicy to "allowlist", "pairing", or "open".
Group Policy
Controls whether the bot participates in group chats at all:
disabled(default) — The bot ignores all group messages. Safest option.allowlist— The bot only responds in groups explicitly listed ingroupsby chat ID. The"*"key provides default settings but does not act as a wildcard allow.pairing— A deliberate mention or reply from an unknown group creates one pairing request for the group. Once approved, members can use the bot subject to the group’ssenderssetting (defaultopen);privatePolicycontinues to control direct messages.open— The bot responds in all groups it’s added to. Use with caution.
Approve a group with the same CLI command used for user pairing. The pending request identifies the group and the member who initiated it:
qwen channel pairing approve my-channel <CODE>Group approvals are stored by the group’s chat ID in the channel’s workspace scope. On GitHub and GitLab the chat ID is the repository/project path, so a rename or transfer detaches the stored approval — re-approve the group after renaming. A repo or project re-created under the same path inherits any stale approval — revoke group approvals after any rename, transfer, or deletion.
An unmentioned message never creates a group pairing request, even when a group
sets requireMention to false; after approval, the configured mention policy
applies normally.
Group pairing requests share the same pending queue as DM pairing requests: a channel holds at most 3 pending requests overall, and a sender holds at most one pending request across user and group requests (see Pairing Rules).
Mention Gating
In groups, the bot requires an @mention or a reply to one of its messages by default. This prevents the bot from responding to every message in a group chat.
Configure per-group with the groups setting:
{
"groups": {
"*": { "requireMention": true },
"-100123456": { "requireMention": false }
}
}"*"— Default settings for all groups. Only sets config defaults, not an allowlist entry.- Group chat ID — Override settings for a specific group. Overrides
"*"defaults. requireMention(default:true) — Whentrue, the bot only responds to messages that @mention it or reply to one of its messages. Whenfalse, the bot responds to all messages (useful for dedicated task groups).
Group Senders
Admitted groups allow all members by default. Set senders in groups to restrict members independently of private access:
{
"privatePolicy": "pairing",
"groupPolicy": "open",
"groups": {
"*": { "senders": "open" },
"-100123456": { "senders": "allowlist", "allowedUsers": ["alice", "bob"] }
}
}open(default) — Any member of an admitted group may use the bot. Private policy and user pairing have no effect on group members.allowlist— only the group’sallowedUsersmay speak. This list is separate from the channel-levelallowedUsers.
All group settings use field-by-field inheritance: the specific group, then "*", then the channel default where applicable, then the built-in default. Empty objects inherit defaults; arrays replace rather than merge, including allowedUsers: []. For example, a group that sets only senders: "allowlist" uses the allowedUsers from "*". Under groupPolicy: "allowlist" a group ID key also admits the group, so a per-group entry there both admits the group and sets its senders.
pairing is deliberately not a senders value: pairing approvals are stored per user, so approving someone through a group message would also unlock their direct messages. Use groupPolicy: "pairing" to admit an entire group instead.
On the github and gitlab channels all inbound traffic is group traffic, so only groupPolicy and group member settings govern admission — see those channels’ Security notes before using it on a public repository or project. Under sessionScope: "single" direct messages and groups share one conversation, so opening a group also exposes direct-message history to its members.
In the Web Shell channel editor, Who can talk in groups and Allowed group member IDs edit groups["*"], and Session operators edits operators. When senders is unset, the editor shows its effective default and writes nothing until you pick a value. Per-group overrides and an explicitly empty "operators": [] are set in settings.json; saving from the editor keeps them.
Group History Backfill
By default, Qwen ignores unmentioned group messages and does not store them as session turns. To let the next @mention include recent group context, set groupHistoryLimit to a positive number.
{
"channels": {
"my-dingtalk": {
"type": "dingtalk",
"clientId": "$DINGTALK_CLIENT_ID",
"clientSecret": "$DINGTALK_CLIENT_SECRET",
"groupPolicy": "open",
"groupHistoryLimit": 50,
"groups": {
"*": { "requireMention": true },
"sensitive-group-id": {
"requireMention": true,
"groupHistoryLimit": 0
}
}
}
}
}- Omitted or
0disables backfill. - Group-level
groupHistoryLimitoverrides the channel-level value. - Only messages from senders the group’s
senderssetting admits are persisted (all admitted group members by default) — sosenders: "open"also widens whose messagesgroupHistoryLimitrecords. - Messages rejected by
groupPolicyor group allowlist are not persisted. - Pending group history is stored as local JSONL under
~/.qwen/channels/<channel-name>-group-history.jsonlor$QWEN_HOME/channels/<channel-name>-group-history.jsonl. - Cached messages are injected as untrusted context on the next real trigger and are not written as standalone session turns.
How group messages are evaluated
1. groupPolicy — is this group disabled, listed, paired, or open? (no → ignore/pairing flow)
2. requireMention — was the bot mentioned/replied to? (no → no task; eligible history may be cached)
3. senders — is this member allowed? (open, or group member allowlist)
4. Route to sessionTelegram Setup for Groups
- Add the bot to a group
- Disable privacy mode in BotFather (
/mybots→ Bot Settings → Group Privacy → Turn Off) — otherwise the bot won’t see non-command messages - Remove and re-add the bot to the group after changing privacy mode (Telegram caches this setting)
Finding a Group Chat ID
To find a group’s chat ID for the groups allowlist:
- Stop the bot if it’s running
- Send a message mentioning the bot in the group
- Use the Telegram Bot API to check queued updates:
curl -s "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getUpdates" | python3 -m json.toolLook for message.chat.id in the response — group IDs are negative numbers (e.g., -5170296765).
Media Support
Channels support sending images and files to the agent, not just text.
Images
Send a photo to the bot and the agent will see it — useful for sharing screenshots, error messages, or diagrams. The image is sent directly to the model as a vision input.
To use image support, configure a multimodal model for the channel:
{
"channels": {
"my-channel": {
"type": "telegram",
"model": "qwen3.5-plus",
...
}
}
}Files
Send a document (PDF, code file, text file, etc.) to the bot. The file is downloaded and saved to a temporary directory, and the agent is told the file path so it can read the contents using its file-reading tools.
Files work with any model — no multimodal support required.
Platform differences
| Feature | Telegram | DingTalk | Feishu | QQ Bot | |
|---|---|---|---|---|---|
| Images | Direct download via Bot API | CDN download with AES decryption | downloadCode API (two-step) | Open API resources endpoint (authenticated GET, 50MB limit) | Vision input, also saved to a local path (8MB cap) |
| Files | Direct download via Bot API (20MB limit) | CDN download with AES decryption | downloadCode API (two-step) | Open API resources endpoint (50MB limit) | Not supported — images and videos only |
| Captions | Photo/file captions included as message text | Not applicable | Rich text: mixed text + images in one message | Rich text (post): text extracted; embedded images ignored | Message text is kept as the caption |
QQ Bot accepts images and videos. An image is passed to the agent as vision input and is also saved to a temporary local path; a video is saved to a temporary local path and the agent is told that path. Image input requires a model that accepts images — with a text-only model the agent still gets the path. See QQ Bot for details.
WeCom accepts text, images, mixed text plus images, files, videos, and voice messages (transcribed). Images are passed to the agent as attachments; files and videos are downloaded to temporary local paths. See WeCom for details.
Dispatch Modes
Controls what happens when you send a new message while the bot is still processing a previous one.
steer(default) — The bot cancels the current request and starts working on your new message. Best for normal chat, where a follow-up usually means you want to correct or redirect the bot.collect— Your new messages are buffered. When the current request finishes, all buffered messages are combined into a single follow-up prompt. Good for async workflows where you want to queue up thoughts.followup— Each message is queued and processed as its own separate turn, in order. Useful for batch workflows where each message is independent.
{
"channels": {
"my-channel": {
"type": "telegram",
"dispatchMode": "steer",
...
}
}
}You can also set dispatch mode per group, overriding the channel default:
{
"groups": {
"*": { "requireMention": true, "dispatchMode": "steer" },
"-100123456": { "dispatchMode": "collect" }
}
}Response delivery
Channels use their normal response delivery path. The shared delivery layer sends completed responses, and adapters may provide native progressive display, such as updating an interactive card in place. Platform message-length limits may still split long responses.
The obsolete blockStreaming, blockStreamingChunk, and blockStreamingCoalesce settings are no longer supported and can be removed from channel configuration. They do not affect delivery. Channel settings management rejects newly added or changed values for these fields. An unchanged stored value is retained, or removed, when the edit keeps the channel’s type; changing a channel’s type requires removing these fields first.
Turn output mode
outputMode is a shared channel setting with adapter opt-in. Currently only DingTalk supports it and defaults to per_turn when the setting is omitted. Other adapters retain their existing behavior and receive no output-mode default: the channel editor does not offer this field, and configuration parsing or management saves reject an explicit value on unsupported adapters.
per_taskwaits for the main task and its associated background tasks and notifications, then delivers one final result containing the last non-empty assistant reply for that task.per_responsedelivers each complete assistant response, not each token chunk.per_turndelivers the last non-empty assistant reply within each turn. The main turn finishes immediately when its prompt ends; later background notification turns deliver separate results.
In the default per_turn mode, a later background callback cannot reopen or replace the completed main result. A main result followed by eleven independent callback turns can therefore produce twelve result messages or cards. Choose per_task when the final result should wait for the associated background work. These modes select assistant output; they do not generate an extra summary or concatenate every intermediate reply.
The selected policy applies whether interactive cards are enabled or replies use ordinary messages. The shared layer owns output selection and task/turn coordination; native rendering, media and fallback delivery remain adapter-specific. See DingTalk turn output mode for presentation details and the conversation scope. Channel loops and webhook runs are unchanged.
Scheduled Channel Loops
Channels have a persistent scheduler for prompts that should run later and push
their result back to the same chat. You can ask the agent naturally, for
example, Every 15 minutes, check the deployment and report any change, or use
the local commands directly:
/loop add "*/15 * * * *" check the deployment and report any change
/loop list
/loop inspect <id>
/loop cancel <id>The agent uses the channel_loop_create, channel_loop_list, and
channel_loop_cancel tools when it manages these jobs for you. Schedules use
standard five-field cron expressions in the machine’s local time. The job runs
unattended and its final response is delivered automatically to the chat that
created it.
Channel loops differ from the session-scoped tasks described in Run Prompts on a Schedule:
- They are stored under
$QWEN_HOME/channels/— standalone channels usecron.jsondirectly, while daemon-managed channels use a per-workspace file underdaemon/. Both survive channel restarts. - They are scoped to the current channel chat or thread. Each target can have up to 10 enabled loops, and each prompt is limited to 4,000 characters.
- They require an adapter and target that support proactive delivery. Telegram, DingTalk, Feishu, and WeCom opt in, subject to platform-specific target restrictions.
- They are unavailable with
sessionScope: "single"because that scope is not tied to one chat target. - A saved loop is disabled if its target is no longer authorized when it is due.
Background Agent Results
When the agent delegates work to a background subagent or fork, the completion result is delivered back to the channel chat that owns the session. Delivery can happen after the original turn has ended, so keep the channel service or daemon running while background work is active.
Slash Commands
Channels support slash commands. These are handled locally (no agent round-trip):
/help— List available commands/clear— Clear your session and start fresh (aliases:/reset,/new)/status— Show session info and access policy/btw <question>— Ask a side question without interrupting the current task; text-only questions up to 4096 characters, requires an agent connection with side-question support/sessions [all]— List open named tasks, or include closed tasks; available only withmultiSession: true/session current— Show the selected named task/session new <name>— Create and select a shared-workspace task/session new <name> --worktree— Create and select a task in its own Git worktree; daemon-managed named-task mode only/session use <name>— Select an open task or reopen a closed task/session cancel [<name>]— Cancel the selected task’s active prompt, or name another owned task; independently queued turns are not cancelled, butcollect-mode follow-ups buffered behind the cancelled prompt are discarded by the existing cancellation behavior; media preparation is not targeted/session close <name>— Close a task without deleting its transcript/loop add "<cron>" <prompt>— Create a persistent scheduled channel loop/loop list— List loops for the current chat/loop inspect <id>— Show loop status and run details/loop cancel <id>— Disable a loop
All other slash commands (e.g., /compress, /summary) are forwarded to the agent. Named-task commands are registered only when the mode is enabled, so /sessions remains agent-visible for existing configurations.
Named-task commands work on all channel types (Telegram, WeChat, QQ, DingTalk, WeCom, Feishu, GitHub). /cancel is currently registered only by Telegram, and loop creation requires proactive delivery support for the current adapter and target.
Running
# Start all configured channels (shared agent process)
qwen channel start
# Start a single channel
qwen channel start my-channel
# Check if the service is running
qwen channel status
# Stop the running service
qwen channel stopThe bot runs in the foreground. Press Ctrl+C to stop, or use qwen channel stop from another terminal.
Experimental Daemon-Managed Mode
You can also run configured channels under qwen serve:
# Start one channel under the daemon lifecycle
qwen serve --channel my-channel
# Start all configured channels
qwen serve --channel all
# Or enable channels later on a token-protected daemon
QWEN_SERVER_TOKEN=secret qwen serve
qwen channel set my-channel --token secret
# Query or stop the daemon-managed selection
qwen channel status --daemon-url http://127.0.0.1:4170 --token secret
qwen channel stop --daemon-url http://127.0.0.1:4170 --token secretThis mode starts workspace-grouped channel worker processes owned by qwen serve. Workers connect back to the daemon through the SDK and use the same channel adapters. They are separate from the daemon process, so a channel adapter crash does not crash the daemon. An explicit --channel selection takes precedence and fails daemon startup if it cannot become ready. On a flagless boot, every trusted registered workspace’s own serve.channels setting is restored, and a name that cannot be hosted is skipped with a log instead of stopping the others. all remains primary-workspace only. Registering a workspace later restores its channels too — without holding the registration open — once per daemon run, and not at all when the daemon was started with an explicit --channel or after qwen channel stop. Unlike the boot restore, one name it cannot host costs that workspace its whole list. Without any of these sources, the daemon does not load channel adapters or reserve the lease until the first qwen channel set, or until a workspace that configures its own serve.channels registers.
Automatic restore skips invalid startup settings and validation or lease failures that occur before workers start, while preserving unrelated settings. After a worker startup fails, the daemon continues only once cleanup succeeds. A global runtime startup timeout or an unconfirmed worker stop still follows the normal startup-failure path; the service lease remains held while worker termination is unconfirmed. Check the daemon log for messages identifying serve.channels when a channel does not restore.
Stored startup names must be non-empty, have no leading or trailing whitespace, and contain no unsafe control or invisible characters. Invalid entries are skipped individually and logged by their array index. Startup does not rename instances or rewrite the configuration. The channel startup toggle reflects the saved setting; runtime state shows whether the channel is running.
qwen serve --channel is not the same service as qwen channel start. Standalone qwen channel start still uses the ACP-backed channel service and can run channel configs with different cwd values. Daemon-managed channels require every selected channel’s cwd to resolve to a workspace registered by the daemon. In multi-workspace mode, a selection replacement keeps workers for workspaces whose ordered channel list did not change; all remains primary-workspace-only.
Without --daemon-url, qwen channel status and qwen channel stop retain standalone pidfile behavior. Their --daemon-url variants query or stop the daemon manager. Runtime selections are not written to settings and do not survive daemon restart. If a ready worker exits unexpectedly, the daemon continues running and reports a channel-worker warning in /daemon/status.
Webhook-triggered tasks
Daemon-managed channels can also accept authenticated webhook events. Qwen receives the event as context, summarizes and decides what matters, and then delivers the final response to the configured chat target. This is not a raw notification relay.
Webhook tasks require approvalMode: "yolo" because they run without interactive approval. That setting applies to the whole channel, not only webhook turns, so use a dedicated webhook channel or tightly restrict normal chat senders for that channel.
Example channel config:
{
"channels": {
"dingtalk-main": {
"type": "dingtalk",
"clientId": "$DINGTALK_CLIENT_ID",
"clientSecret": "$DINGTALK_CLIENT_SECRET",
"cwd": "/repo",
"privatePolicy": "allowlist",
"allowedUsers": ["12345"],
"approvalMode": "yolo",
"sessionScope": "user",
"webhooks": {
"sources": {
"github-ci": {
"secretEnv": "QWEN_CHANNEL_GITHUB_CI_SECRET",
"targets": {
"operator": {
"chatId": "DINGTALK_USER_ID",
"senderId": "webhook:github-ci",
"isGroup": false
},
"team": {
"chatId": "OPEN_CONVERSATION_ID",
"senderId": "webhook:github-ci",
"isGroup": true
}
}
}
}
}
}
}
}For DingTalk, set isGroup explicitly on every target. A direct-message target uses the DingTalk user ID as chatId with isGroup: false; a group target uses the group openConversationId with isGroup: true. Other adapters may require their own proactive target shape.
Daemon-managed DingTalk, Feishu, Telegram, and WeCom channels dynamically observe contacts from authorized inbound messages. List contacts observed in the primary workspace during the default seven-day freshness window:
curl -H "Authorization: Bearer $QWEN_SERVER_TOKEN" \
http://127.0.0.1:4170/workspace/channel/observed-contactsUse GET /workspaces/:workspace/channel/observed-contacts to select another registered, trusted workspace. Add ?freshWithinSeconds=N to choose a window from one second through 365 days. The daemon advertises this API with the workspace_channel_observed_contacts capability.
The response returns complete platform IDs and labels. Group labels use names already present in accepted inbound messages when available: DingTalk supplies conversationTitle, and Telegram supplies chat.title. Feishu and WeCom group labels currently fall back to their complete IDs; no platform directory or group-detail API is queried. Topic labels also fall back to complete IDs. Each lastObservedAt is a canonical ISO 8601 UTC timestamp with millisecond precision; clients can convert it to the user’s local time zone for display. Top-level users contains users observed in direct messages. groups contains observed group conversations, groups[].users contains users observed in each group, and groups[].topics[].users contains users observed in Feishu or Telegram topics:
{
"users": [
{
"channelName": "feishu-main",
"label": "Example User",
"id": "ou_complete_user_id",
"lastObservedAt": "2026-07-17T08:00:00.000Z"
}
],
"groups": [
{
"channelName": "feishu-main",
"label": "oc_complete_chat_id",
"id": "oc_complete_chat_id",
"lastObservedAt": "2026-07-17T08:05:00.000Z",
"users": [
{
"label": "Example User",
"id": "ou_complete_user_id",
"lastObservedAt": "2026-07-17T08:05:00.000Z"
}
],
"topics": []
}
]
}These nested users are observed participants, not authoritative group membership. Only messages that pass direct/group, mention, sender, and pairing gates are recorded. Repeated observations refresh labels and timestamps; passive observation cannot detect a leave or deletion until the relationship becomes stale. Message content is never stored. The bounded registry lives under $QWEN_HOME/channels/daemon/<workspaceHash>/observed-contacts.json, outside the workspace checkout and partitioned per workspace. Its 500-observation limit is shared by all channels and conversations in that workspace, and observations older than 365 days are removed on the next accepted write. If the registry becomes malformed or uses an unsupported version, delete that file to reset it; accepted traffic recreates it. Webhook configuration and delivery are unchanged.
Start qwen serve with the channel worker enabled:
QWEN_SERVER_TOKEN="$QWEN_SERVER_TOKEN" qwen serve --require-auth --channel dingtalk-mainExample request:
curl -X POST "http://127.0.0.1:4170/channels/dingtalk-main/webhooks/github-ci" \
-H "x-qwen-webhook-secret: $QWEN_CHANNEL_GITHUB_CI_SECRET" \
-H "Content-Type: application/json" \
-d '{
"eventType": "push",
"targetRef": "operator",
"title": "CI pipeline finished",
"payload": {
"targetRef": "refs/heads/main",
"repository": "qwen-code",
"status": "success"
}
}'Webhook routes authenticate with the webhook secret header, even when qwen serve is running with bearer auth enabled. Do not share the daemon bearer token with webhook providers. Webhook config and secretEnv values are loaded when the daemon starts; restart qwen serve after changing webhook sources or rotating secrets. A 202 {"accepted": true} response means the channel worker accepted ownership of the task, not that the final response has already been delivered to chat. Check daemon and channel worker logs, plus /daemon/status, when troubleshooting delivery failures.
Multi-Channel Mode
When you run qwen channel start without a name, all channels defined in settings.json start together sharing a single agent process. Each channel maintains its own sessions — a Telegram user and a WeChat user get separate conversations, even though they share the same agent.
Each channel uses its own cwd from its config, so different channels can work on different projects simultaneously.
Service Management
The channel service uses a PID file (~/.qwen/channels/service.pid) to track the running instance:
- Duplicate prevention: Running
qwen channel startwhile a service is already running will show an error instead of starting a second instance qwen channel stop: Gracefully stops the running service from another terminalqwen channel status: Shows whether the service is running, its uptime, and session counts per channel
Crash Recovery
If the agent process crashes unexpectedly, the channel service automatically restarts it and attempts to restore all active sessions. Users can continue their conversations without starting over.
- Sessions are persisted to
~/.qwen/channels/sessions.jsonwhile the service is running - On crash: the agent restarts within 3 seconds and reloads saved sessions
- After 3 consecutive crashes, the service exits with an error
- On clean shutdown (Ctrl+C or
qwen channel stop): session data is cleared — the next start is always fresh