Communication channels define how messages flow between external platforms (Discord, GitHub, Signal, Slack) and the agent fleet.
Each channel entry in workspace/channels.yaml does three things:
- Routes inbound messages from that platform channel to a specific agent — no keyword matching required
- Identifies the agent — replies come from that agent's own bot identity (its own Discord bot, Slack token, etc.)
- Enables the flow sensor — every message becomes a
flow.item.*event that FlowMonitorPlugin tracks
channels.yaml schema
# workspace/channels.yaml
channels:
- id: quinn-pr-reviews # unique ID (used in logs and API)
platform: discord # discord | github | linear | signal | slack
channelId: "1234567890" # Discord channel ID
agent: quinn # agent that handles this channel
agentBotTokenEnv: QUINN_DISCORD_TOKEN # env var for Quinn's bot token
description: PR review requests
- id: github-protoWorkstacean
platform: github
repo: protoLabsAI/protoWorkstacean
agent: quinnCopy workspace/channels.yaml.example to workspace/channels.yaml and edit.
The file hot-reloads every 5 seconds — no restart needed.
Platform setup
Discord — multiple bot identities
Each Discord channel can have its own bot. When a message arrives, the router looks up the channel in the registry and routes to the assigned agent. Replies come from that agent's own bot account.
Setup:
- Create a Discord application + bot for each agent at discord.com/developers
- Copy each bot token to an env var:
QUINN_DISCORD_TOKEN,AVA_DISCORD_TOKEN, etc. - Add entries to
workspace/channels.yaml:
channels:
- id: quinn-prs
platform: discord
channelId: "YOUR_CHANNEL_ID" # right-click channel → Copy Channel ID
agent: quinn
agentBotTokenEnv: QUINN_DISCORD_TOKEN- Invite each bot to your server with the
bot+applications.commandsscopes
If agentBotTokenEnv is not set, the default DISCORD_BOT_TOKEN bot handles that channel.
How it works end-to-end:
User @mentions bot in #pr-reviews
→ DiscordPlugin publishes message.inbound.discord.{channelId}
→ RouterPlugin looks up channelId in ChannelRegistry
→ finds agent: quinn
→ agent.skill.request with targets: ["quinn"]
→ SkillDispatcherPlugin routes to Quinn's executor
→ Quinn replies
→ DiscordPlugin sends reply FROM Quinn's bot clientGitHub
Route @mentions on a specific repository to a specific agent:
- id: github-protoWorkstacean
platform: github
repo: protoLabsAI/protoWorkstacean
agent: quinnWhen someone @mentions the bot on this repo, RouterPlugin injects targets: ["quinn"] into the skill request, bypassing keyword matching.
Signal
- id: signal-ops
platform: signal
groupId: "YOUR_SIGNAL_GROUP_ID"
agent: ava
description: Ops escalation channelSignalPlugin (lib/plugins/signal.ts) is fully wired — a WebSocket listener for inbound messages plus an outbound sender. Configure it with SIGNAL_URL + SIGNAL_NUMBER.
Slack
- id: slack-eng
platform: slack
slackChannelId: "C1234567890"
agentSlackTokenEnv: QUINN_SLACK_TOKEN
agent: quinnThe channel registry accepts platform: slack entries, but no SlackPlugin exists yet — there is nothing to deliver or receive Slack messages. The schema is registry-ready; the plugin is not implemented.
Adding a channel at runtime
Edit workspace/channels.yaml directly — the file hot-reloads every 5 seconds, so no restart is required. There is no POST /api/channels write endpoint; channels are managed through the YAML file.
A read endpoint exists but is currently a hardcoded empty stub:
curl http://localhost:3000/api/channels # GET — always returns { "success": true, "data": [] }It is wired in src/api/operations.ts and does not yet reflect the live registry.
Research sharing across channels
Any agent can publish a knowledge.shared event on the bus to share context with other channels:
bus.publish("knowledge.shared", {
id: crypto.randomUUID(),
correlationId: crypto.randomUUID(),
topic: "knowledge.shared",
timestamp: Date.now(),
payload: {
source: "quinn",
content: "PR #123 has a type safety issue in the executor layer",
tags: ["pr-review", "type-safety"],
projectSlug: "protoWorkstacean",
},
});From Discord, use the /share slash command (if configured in workspace/discord.yaml) or POST /publish:
curl -X POST http://localhost:3000/publish \
-H "Content-Type: application/json" \
-d '{
"topic": "knowledge.shared",
"payload": {
"source": "user",
"content": "The auth refactor is blocked on legal review",
"tags": ["auth", "blocked"]
}
}'Agents subscribed to knowledge.shared (via their skill definitions or workspace plugins) receive this context automatically.
How routing priority works
When a message arrives, RouterPlugin resolves the agent in this order:
| Priority | Source | Example |
|---|---|---|
| 1 | channels.yaml channel assignment | channelId: "1234" → agent: quinn |
| 2 | payload.skillHint from surface plugin | Discord slash command, Linear webhook |
| 3 | Keyword match from workspace/agents/*.yaml | message content contains "review" → quinn |
| 4 | ROUTER_DEFAULT_SKILL env var | catch-all for unmatched messages |
Channel assignments always win. If a channel is assigned to Quinn, she gets the message even if the content looks like it belongs to another agent.
Disabled channels
Set enabled: false to temporarily disable a channel without removing it:
- id: frank-deployments
platform: discord
channelId: "1111222233334444"
agent: frank
enabled: false # Frank is offline — messages fall back to keyword routing