This guide documents how Tiny Claw channel plugins work so you can build your own. Two reference implementations ship with the repo: Discord (external platform integration) and Friends (built-in web chat).
A channel plugin connects an external messaging platform (Discord, Telegram, Slack, etc.) — or any custom interface — to the Tiny Claw agent.
At runtime it:
- Starts a client connection to the external platform.
- Receives inbound platform messages.
- Converts each incoming message to a Tiny Claw
userId+ text payload. - Sends the payload into the agent loop via
context.enqueue(userId, message). - Sends the agent's response back to the platform.
The startup flow is:
- CLI loads plugin IDs from the
plugins.enabledconfig array. - Plugin modules are imported dynamically by package name.
- Pairing tools from channel (and provider) plugins are merged into the agent tool list.
- Tiny Claw creates a
PluginRuntimeContextcontainingenqueue,secrets,configManager, andagentContext. - Each channel plugin is started via
channel.start(pluginRuntimeContext). - On shutdown, Tiny Claw calls
channel.stop().
Channel plugins must default-export an object that satisfies ChannelPlugin from @tinyclaw/types:
export interface ChannelPlugin extends PluginMeta {
readonly type: 'channel';
start(context: PluginRuntimeContext): Promise<void>;
stop(): Promise<void>;
getPairingTools?(
secrets: SecretsManagerInterface,
configManager: ConfigManagerInterface,
): Tool[];
}Required fields:
| Field | Description |
|---|---|
id |
Package name, e.g. @tinyclaw/plugin-channel-telegram |
name |
Human-readable label |
description |
Short summary |
type |
Must be 'channel' |
version |
SemVer string |
start(context) |
Connect to the platform and begin listening |
stop() |
Disconnect and clean up resources |
Optional field:
| Field | Description |
|---|---|
getPairingTools(secrets, configManager) |
Return tools that let the user pair/unpair this channel conversationally |
Create your package inside:
plugins/channel/plugin-channel-<name>/
Suggested structure:
plugins/channel/plugin-channel-<name>/
package.json
tsconfig.json
src/
index.ts # default export of ChannelPlugin
pairing.ts # pair/unpair tool factories (optional)
tests/
pairing.test.ts # tests for pairing flow
Minimal package.json:
{
"name": "@tinyclaw/plugin-channel-<name>",
"version": "0.1.0",
"type": "module",
"main": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"dependencies": {
"@tinyclaw/logger": "workspace:*",
"@tinyclaw/types": "workspace:*"
}
}Note: The root workspace already includes
plugins/channel/*in its workspaces array. Use the package ID (not a file path) inplugins.enabled.
import type {
ChannelPlugin,
PluginRuntimeContext,
Tool,
SecretsManagerInterface,
ConfigManagerInterface,
} from '@tinyclaw/types';
function createPairingTools(
secrets: SecretsManagerInterface,
configManager: ConfigManagerInterface,
): Tool[] {
return [];
}
const plugin: ChannelPlugin = {
id: '@tinyclaw/plugin-channel-<name>',
name: '<Name>',
description: '<Platform> channel plugin for Tiny Claw',
type: 'channel',
version: '0.1.0',
getPairingTools(
secrets: SecretsManagerInterface,
configManager: ConfigManagerInterface,
): Tool[] {
return createPairingTools(secrets, configManager);
},
async start(context: PluginRuntimeContext): Promise<void> {
// 1. Resolve token/settings from context.secrets + context.configManager
// 2. Connect to platform SDK
// 3. On inbound message:
// const response = await context.enqueue(userId, text);
// 4. Send response back to the channel
},
async stop(): Promise<void> {
// Disconnect SDK clients and clean up
},
};
export default plugin;Pairing tools allow conversational setup from inside Tiny Claw. The agent exposes them as callable tools so a user can say "connect my Discord bot" and the agent handles the rest.
<name>_pairtool validates user input (token, webhook URL, etc.).- Store the secret via
secrets.store(key, value). - Set config flags via
configManager.set(key, value). - Add the plugin package ID to
plugins.enabledif not already present. - Return success text instructing the user to call
tinyclaw_restart.
- Disable the channel config flag.
- Remove the plugin ID from
plugins.enabled. - Optionally remove the stored secret (or keep it for easy re-pairing).
- Instruct the user to restart.
Follow the naming conventions used by existing plugins:
| Key | Pattern | Example |
|---|---|---|
| Secret key | channel.<name>.token |
channel.discord.token |
| Enabled flag | channels.<name>.enabled |
channels.discord.enabled |
| Token reference | channels.<name>.tokenRef |
channels.discord.tokenRef |
| Plugin package ID | @tinyclaw/plugin-channel-<name> |
@tinyclaw/plugin-channel-discord |
When implementing start(context):
- Ignore bot/self messages to prevent infinite loops.
- Define clear trigger rules — DM only, @mention only, specific prefix, etc.
- Normalize inbound content — strip mentions, trim whitespace.
- Use a namespaced user ID to avoid cross-channel collisions:
- Format:
<platform>:<platform-user-id>(e.g.discord:123456789)
- Format:
- Handle platform limits — message max length, rate limits, file attachments.
- Return user-friendly fallback text on errors.
-
Install dependencies:
bun install
-
Add your plugin package ID to config (choose one):
- Via the agent tool:
config_setforplugins.enabled - Or programmatically:
configManager.set('plugins.enabled', [...current, '@tinyclaw/plugin-channel-<name>'])
- Via the agent tool:
-
Start Tiny Claw:
bun start
-
Run your pairing tool (if implemented), then call
tinyclaw_restart. -
Confirm logs show
Channel plugin started: <Name>.
| Plugin | Path | Description |
|---|---|---|
| Discord | plugins/channel/plugin-channel-discord/ |
External platform integration via discord.js |
| Friends | plugins/channel/plugin-channel-friends/ |
Built-in invite-based web chat channel |
Key files in the Discord plugin:
src/index.ts— channel runtime (start/stoplifecycle)src/pairing.ts— pair/unpair tool definitionstests/pairing.test.ts— pairing flow tests