Communication Channels
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
Section titled “channels.yaml schema”channels: - id: quinn-pr-reviews # unique ID (used in logs and API) platform: discord # discord | github | signal | slack | plane 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
Section titled “Platform setup”Discord — multiple bot identities
Section titled “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
Section titled “GitHub”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
Section titled “Signal” - id: signal-ops platform: signal groupId: "YOUR_SIGNAL_GROUP_ID" agent: ava description: Ops escalation channelRequires SignalPlugin to be fully wired (currently a stub — see lib/plugins/signal.ts).
- id: slack-eng platform: slack slackChannelId: "C1234567890" agentSlackTokenEnv: QUINN_SLACK_TOKEN agent: quinnRequires SlackPlugin (not yet implemented).
Adding a channel at runtime
Section titled “Adding a channel at runtime”No restart required. Use the API:
curl -X POST http://localhost:3000/api/channels \ -H "Content-Type: application/json" \ -d '{ "id": "frank-infra", "platform": "discord", "channelId": "1122334455667788", "agent": "frank", "agentBotTokenEnv": "FRANK_DISCORD_TOKEN", "description": "Infrastructure and deployments" }'The entry is written to workspace/channels.yaml and the registry reloads within 5 seconds.
List all channels:
curl http://localhost:3000/api/channelsResearch sharing across channels
Section titled “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
Section titled “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, Plane 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
Section titled “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