Deterministic rules-based router for Telegram bots in OpenClaw.
README
Claw Switchboard
claw-switchboard is an OpenClaw channel plugin for running one Telegram bot with deterministic routing.
It is designed for this pattern:
- one Telegram bot
- no receptionist LLM
- rules decide where a message goes
- targets can be OpenClaw agents or external services
What It Does
claw-switchboard can route a single chat across multiple targets:
- an OpenClaw general agent
- a coding agent
- an image backend
- any other HTTP or static target you define
It supports:
- Telegram Bot API long polling
- deterministic routing by commands, prefixes, start keywords, regex,
hasPhoto, andhasText - per-chat mode persistence
- explicit mode switching commands
- dynamic target labels in user-facing switch messages
- OpenClaw agent targets
- HTTP service targets
- static text and static photo targets
Workspace Compatibility
claw-switchboard does not require multiple workspaces.
It works in all of these layouts:
- single workspace, single agent
- single workspace, multiple agents
- multiple workspaces, multiple agents
The plugin routes by target and session, not by a requirement that each target must have a separate workspace.
Use multiple workspaces only when you want stronger separation between agents.
Route Priority
Incoming messages are resolved in this order:
- Explicit control command, such as
/s,/s mode,/s code - Strong route match from
routes - Active persisted target for the current chat
fallbackTarget
This means:
- a clear image request can override an active coding chat
- follow-up messages can stay on the current agent
- users can always force a switch with a command
Config Shape
The plugin is configured under:
{
channels: {
"claw-switchboard": {
}
}
}
Core fields:
enabledbotTokendefaultAgentIdfallbackTargetsessionPrefixpollingmodeCommandstargetsroutes
Example Config
This example shows:
general-> main OpenClaw agentcoding-> dedicated coding agentimage-> external HTTP image backend
{
channels: {
"claw-switchboard": {
enabled: true,
botToken: "YOUR_BOT_TOKEN",
defaultAgentId: "main",
fallbackTarget: "general",
sessionPrefix: "claw-switchboard",
polling: {
timeoutSeconds: 25,
retryDelayMs: 3000
},
modeCommands: {
command: "s",
clear: ["auto", "default", "clear"],
status: ["mode", "status"],
targets: {
coding: ["code", "coding"],
general: ["general"],
image: ["image", "img"]
}
},
targets: {
general: {
type: "agent",
label: "General Assistant",
persistMode: true,
agentId: "main",
provider: "openai-codex",
model: "gpt-5.4-mini"
},
coding: {
type: "agent",
label: "Coding Assistant",
persistMode: true,
agentId: "coding",
provider: "openai-codex",
model: "gpt-5.3-codex"
},
image: {
type: "http",
label: "Image Service",
persistMode: false,
clearModeOnUse: true,
url: "http://127.0.0.1:8080/generate",
responseFormat: "switchboard-v1",
timeoutMs: 30000
}
},
routes: [
{
id: "image",
priority: 200,
match: {
commands: ["image", "img", "imagine"],
prefixes: ["image:", "img:", "imagine:", "draw:"],
keywords: ["draw a", "draw me", "generate an image", "create an image"]
},
target: "image"
},
{
id: "coding",
priority: 120,
match: {
commands: ["code", "debug"],
prefixes: ["code:", "coding:", "debug:"],
keywords: ["write code", "fix bug", "debug", "python", "typescript", "fastapi"]
},
target: "coding"
}
]
}
}
}
Sample Configurations
Sample 1: Single Workspace, One Agent, One Image Service
Use this when you have:
- one OpenClaw agent
- one shared workspace
- one external image backend
{
agents: {
defaults: {
model: {
primary: "openai-codex/gpt-5.4-mini"
},
workspace: "/opt/openclaw/workspace"
}
},
channels: {
telegram: {
enabled: false
},
"claw-switchboard": {
enabled: true,
botToken: "YOUR_BOT_TOKEN",
defaultAgentId: "main",
fallbackTarget: "general",
modeCommands: {
command: "s",
clear: ["auto", "default", "clear"],
status: ["mode", "status"],
targets: {
general: ["general"],
image: ["image", "img"]
}
},
targets: {
general: {
type: "agent",
label: "General Assistant",
persistMode: true,
agentId: "main",
provider: "openai-codex",
model: "gpt-5.4-mini"
},
image: {
type: "http",
label: "Image Service",
persistMode: false,
clearModeOnUse: true,
url: "http://127.0.0.1:8080/generate",
responseFormat: "switchboard-v1"
}
},
routes: [
{
id: "image",
priority: 200,
match: {
commands: ["image", "img", "imagine"],
prefixes: ["image:", "img:", "imagine:", "draw:"],
keywords: ["draw a", "draw me", "generate an image", "create an image"]
},
target: "image"
}
]
}
}
}
Sample 2: Single Workspace, Two Agents
Use this when you want:
- one shared workspace
- one general agent
- one coding agent
- no workspace split
{
agents: {
defaults: {
model: {
primary: "openai-codex/gpt-5.4-mini"
},
workspace: "/opt/openclaw/workspace"
},
list: [
{
id: "coding",
model: {
primary: "openai-codex/gpt-5.3-codex"
}
}
]
},
channels: {
telegram: {
enabled: false
},
"claw-switchboard": {
enabled: true,
botToken: "YOUR_BOT_TOKEN",
defaultAgentId: "main",
fallbackTarget: "general",
modeCommands: {
command: "s",
clear: ["auto", "default", "clear"],
status: ["mode", "status"],
targets: {
coding: ["code", "coding"],
general: ["general"],
image: ["image", "img"]
}
},
targets: {
general: {
type: "agent",
label: "General Assistant",
persistMode: true,
agentId: "main",
provider: "openai-codex",
model: "gpt-5.4-mini"
},
coding: {
type: "agent",
label: "Coding Assistant",
persistMode: true,
agentId: "coding",
provider: "openai-codex",
model: "gpt-5.3-codex"
},
image: {
type: "http",
label: "Image Service",
persistMode: false,
clearModeOnUse: true,
url: "http://127.0.0.1:8080/generate",
responseFormat: "switchboard-v1"
}
},
routes: [
{
id: "image",
priority: 200,
match: {
commands: ["image", "img", "imagine"],
prefixes: ["image:", "img:", "imagine:", "draw:"],
keywords: ["draw a", "draw me", "generate an image", "create an image"]
},
target: "image"
},
{
id: "coding",
priority: 120,
match: {
commands: ["code", "debug"],
prefixes: ["code:", "coding:", "debug:"],
keywords: ["write code", "fix bug", "debug", "python", "typescript", "fastapi"]
},
target: "coding"
}
]
}
}
}
Sample 3: Multiple Workspaces, Two Agents
Use this when you want:
- one general workspace
- one coding workspace
- stronger separation between agents
{
agents: {
defaults: {
model: {
primary: "openai-codex/gpt-5.4-mini"
},
workspace: "/opt/openclaw/workspace-main"
},
list: [
{
id: "main",
workspace: "/opt/openclaw/workspace-main",
model: {
primary: "openai-codex/gpt-5.4-mini"
}
},
{
id: "coding",
workspace: "/opt/openclaw/workspace-coding",
model: {
primary: "openai-codex/gpt-5.3-codex"
}
}
]
},
channels: {
telegram: {
enabled: false
},
"claw-switchboard": {
enabled: true,
botToken: "YOUR_BOT_TOKEN",
defaultAgentId: "main",
fallbackTarget: "general",
modeCommands: {
command: "s",
clear: ["auto", "default", "clear"],
status: ["mode", "status"],
targets: {
coding: ["code", "coding"],
general: ["general"],
image: ["image", "img"]
}
},
targets: {
general: {
type: "agent",
label: "General Assistant",
persistMode: true,
agentId: "main",
provider: "openai-codex",
model: "gpt-5.4-mini"
},
coding: {
type: "agent",
label: "Coding Assistant",
persistMode: true,
agentId: "coding",
provider: "openai-codex",
model: "gpt-5.3-codex"
},
image: {
type: "http",
label: "Image Service",
persistMode: false,
clearModeOnUse: true,
url: "http://127.0.0.1:8080/generate",
responseFormat: "switchboard-v1"
}
},
routes: [
{
id: "image",
priority: 200,
match: {
commands: ["image", "img", "imagine"],
prefixes: ["image:", "img:", "imagine:", "draw:"],
keywords: ["draw a", "draw me", "generate an image", "create an image"]
},
target: "image"
},
{
id: "coding",
priority: 120,
match: {
commands: ["code", "debug"],
prefixes: ["code:", "coding:", "debug:"],
keywords: ["write code", "fix bug", "debug", "python", "typescript", "fastapi"]
},
target: "coding"
}
]
}
}
}
Target Types
agent
Routes a message to an OpenClaw agent.
Useful fields:
labelagentIdprovidermodelpersistModeclearModeOnUsestatelesstimeoutMssessionKeyPrefix
agent targets run through the host OpenClaw runtime. Per-target gatewayUrl and gatewayToken overrides are not supported.
By default, agent targets are stateless. claw-switchboard does not pass a persistent per-chat sessionFile to the embedded agent and generates a one-time session key for each request. That prevents the plugin from creating long-lived per-chat sessions/*.jsonl transcript files and avoids reusing prior chat context through the same session key.
Depending on the host OpenClaw runtime, a stateless request may still create a short-lived file under sessions/<agentId>/ephemeral/ while the run is in progress. Those files are treated as temporary runtime artifacts and are deleted after the request completes.
Set stateless: false only when you explicitly want OpenClaw's persistent per-chat session behavior for that target. In that mode, sessionKeyPrefix applies again and the plugin will create a per-chat sessions/*.jsonl file for the target.
http
Routes a message to an external HTTP service.
Useful fields:
labelurlmethodheaderstimeoutMsresponseFormatpersistModeclearModeOnUse
responseFormat controls which response shape is preferred first during parsing. The plugin still falls back to the other supported shape for compatibility.
For switchboard-v1, the plugin sends a JSON payload that includes:
- top-level
targetId,routeId,reason,text,prompt - nested
input.chatId,input.messageId,input.text,input.prompt - raw
update - raw
message
static-text
Returns a fixed text response.
static-photo
Returns a fixed local photo or base64 photo.
Mode Persistence
Two target fields control whether a routed message becomes the chat's active mode:
persistModeclearModeOnUse
In practice:
persistMode: truemeans "keep this target as the active mode after this request"clearModeOnUse: truemeans "clear the previous active mode when this target is used"
You can think of them as:
persistModecontrols what happens after successclearModeOnUsecontrols what happens to the previous mode before the request runs
persistMode
When persistMode is true, a successful request to that target becomes the active mode for the chat.
That means:
- follow-up messages stay on that target
- the chat keeps using that target until the user switches away or a stronger route match takes over
Typical use:
- general assistant
- coding assistant
- any target that should own a continuing conversation
Example:
general: {
type: "agent",
label: "General Assistant",
persistMode: true,
clearModeOnUse: false
}
Result:
- message routes to
general generalbecomes the active mode- the next follow-up stays on
generalunless the user switches or a stronger route match wins
clearModeOnUse
When clearModeOnUse is true, the plugin clears the previously active mode before using this target.
That means:
- the current request still goes to this target
- but the previous mode is not kept afterward
- the next unrelated message falls back to normal routing unless another target persists itself
Typical use:
- image generation
- one-off utility services
- temporary service calls that should not own the conversation
Example:
image: {
type: "http",
label: "Image Service",
persistMode: false,
clearModeOnUse: true
}
Result:
- message routes to
image - any previous active mode is cleared
imagedoes not become the new active mode- the next unrelated message falls back to normal routing
Recommended Combinations
Multi-shot conversational target
{
persistMode: true,
clearModeOnUse: false
}
Use this for targets that should keep the conversation:
- general agent
- coding agent
One-shot service target
{
persistMode: false,
clearModeOnUse: true
}
Use this for targets that should handle one request and then release the chat:
- image generation
- conversion services
- one-off automations
Stateless target without forced clear
{
persistMode: false,
clearModeOnUse: false
}
Use this only if you want a target to stay stateless without explicitly clearing the current mode.
In most cases, one-shot service target is the safer choice.
Quick Decision Guide
Use persistMode: true when:
- users will probably send follow-up messages to the same target
- you want the chat to stay with the same assistant
Use clearModeOnUse: true when:
- the target is a one-off tool or service
- you do not want the previous active mode to keep controlling the chat afterward
Typical patterns:
generalorcoding:persistMode: true,clearModeOnUse: falseimageone-shot service:persistMode: false,clearModeOnUse: trueimagemulti-shot mode:persistMode: true,clearModeOnUse: false
Telegram Commands
The command prefix is configurable with:
modeCommands.command
If you set:
command: "s"
then claw-switchboard adds these switchboard commands:
/s/s mode/s code/s general/s image
The Telegram command menu also keeps OpenClaw's native commands such as /model, /models, /status, /help, /new, and /reset.
Meaning:
/s- return to default switchboard routing
/s mode- show the current mode and runtime
/s code- lock the chat to the configured coding target
/s general- lock the chat to the configured general target
/s image- send this chat into the configured image target
Important:
/newand/resetuse OpenClaw's native session behavior- use
/sor/s clearwhen you want to clear switchboard mode only
The exact subcommands come from:
modeCommands.targets
So they are configuration-driven, not hardcoded to a specific agent name.
User-Facing Behavior
When a target changes, the plugin can send a dynamic switch message such as:
Switched to Coding Assistant.
Runtime: openai-codex/gpt-5.3-codex.
Those values come from the configured target:
labelprovidermodel
Example for an HTTP service:
Switched to Image Service.
Runtime: http service.
Image Route Notes
If an image target is configured like this:
{
persistMode: false,
clearModeOnUse: true
}
then it behaves like a one-shot route:
- image request goes to the image backend
- previous persisted agent mode is cleared
- the next unrelated message falls back to normal routing
This is usually the right behavior for image generation.
If you want image generation to behave like a continuing mode instead, use:
{
persistMode: true,
clearModeOnUse: false
}
That makes image requests multi-shot and keeps follow-up messages on the image target until the user switches away.
One-Shot Image Sample
image: {
type: "http",
label: "Image Service",
persistMode: false,
clearModeOnUse: true,
url: "http://127.0.0.1:8080/generate",
responseFormat: "switchboard-v1"
}
Behavior:
Draw a small cat-> routes toimageHow is the weather today?-> falls back togeneral
Multi-Shot Image Sample
image: {
type: "http",
label: "Image Service",
persistMode: true,
clearModeOnUse: false,
url: "http://127.0.0.1:8080/generate",
responseFormat: "switchboard-v1"
}
Behavior:
/s image-> switches the chat toimageDraw a small cat-> routes toimageMake it blue-> stays onimage
Install
Link-install locally:
openclaw plugins install -l ./claw-switchboard
Inspect and verify:
openclaw plugins inspect claw-switchboard
openclaw plugins doctor
Local Test Flow
- Keep the stock Telegram channel disabled.
- Enable
channels["claw-switchboard"]. - Configure at least one fallback target and one bot token.
- Start OpenClaw:
openclaw gateway
- If you use an external image route, start that backend too.
Cron Delivery
claw-switchboard now supports outbound text delivery for run summaries and cron announce messages through the same Telegram bot token.
In the delivery UI:
- set
ChanneltoClaw Switchboard - set
Toto the Telegram chat id
Accepted To formats:
YOUR_CHAT_IDtelegram:YOUR_CHAT_IDclaw-switchboard:YOUR_CHAT_ID
Repeated known prefixes are also normalized, so values like claw-switchboard:telegram:YOUR_CHAT_ID are accepted too.
For direct messages to your phone, the raw numeric chat id is the safest choice.
Example Conversations
General
User:
hello
Expected:
- routes to
general - reply comes from the general agent
Coding Follow-Up
User:
Write a compare-two-numbers program
Rewrite it in English
Expected:
- first message routes to
coding - second message stays on
coding
Image One-Shot
User:
Draw a small cat
How is the weather today?
Expected:
- first message routes to
image - second message does not stay on
image - second message falls back to
general
Image Multi-Shot
Target config:
image: {
type: "http",
label: "Image Service",
persistMode: true,
clearModeOnUse: false,
url: "http://127.0.0.1:8080/generate",
responseFormat: "switchboard-v1"
}
User:
/s image
Draw a small cat
Make it blue
Expected:
- the chat switches to
image - the first image request routes to
image - the follow-up request also stays on
image
Explicit Switch
User:
/s code
Write a FastAPI hello world
/s
Hello
Expected:
/s codeswitches the chat to the configured coding target- coding request stays on the coding target
/sclears the modeHelloreturns to default switchboard routing
Publish Readiness
Current publish check summary:
- no user-specific agent names are hardcoded in routing logic
- Telegram mode aliases are config-driven
- target labels are config-driven
- runtime labels are config-driven
- plugin behavior is generic across agent and HTTP targets
Capabilities
- Channels
- claw-switchboard
- configSchema
- Yes
- Executes code
- Yes
- HTTP routes
- 0
- Runtime ID
- claw-switchboard
- Setup entry
- Yes
Compatibility
- Built With Open Claw Version
- 2026.3.28
- Min Gateway Version
- >=2026.3.28
- Plugin Api Range
- 2026.3.28
Verification
- Tier
- source linked
- Scope
- artifact only
- Summary
- Validated package structure and linked the release to source metadata.
- Commit
- claw-switchb
- Tag
- claw-switchboard
- Provenance
- No
- Scan status
- suspicious
Tags
- latest
- 1.0.0
