Architecture
Multi-Channel Communications
How goClaw handles email, SMS, Telegram, WhatsApp — and how to add custom channels.
goClaw supports four channels out of the box: Email, SMS, Telegram, and WhatsApp. All channels share a common Channel interface that normalizes messages to a single format regardless of origin. Adding a custom channel requires implementing the interface and registering it — no core changes needed.
Supported channels
Email uses Resend for outbound delivery and IMAP for inbound polling.
Outbound (Resend):
- Supports plain text and HTML
- Custom From name and address
- Signatures appended automatically
- Delivery webhooks update message status in CRM
Inbound (IMAP):
- Polls IMAP inbox at a configurable interval (default: every 5 minutes)
- Strips reply-chain quoting to isolate the new message
- Extracts attachments and stores metadata
- Thread-matching by In-Reply-To or References headers
Configuration:
channels:
email:
enabled: true
provider: resend
from_name: "Your Agent"
from_address: "agent@yourdomain.com"
imap:
host: "imap.gmail.com"
port: 993
check_interval_minutes: 5
SMS (Twilio)
SMS messages are normalized to 160-character segments. The agent is instructed to keep SMS responses concise.
Features:
- Inbound via Twilio webhook
- Opt-out keyword handling (
STOP,UNSUBSCRIBE) - Multi-segment messages for long content
- Delivery status tracking
Configuration:
channels:
sms:
enabled: true
provider: twilio
from_number: "+15550001234"
opt_out_keywords: ["STOP", "UNSUBSCRIBE"]
WhatsApp (Twilio + Meta)
WhatsApp delivery uses Twilio's WhatsApp Business API. Messages require a pre-approved template for initial outreach (Meta policy). Follow-up messages within a 24-hour window are unrestricted.
Features:
- Template message support for initial outreach
- Session messages (unrestricted within 24h window)
- Media attachment support
- Read receipt tracking
Configuration:
channels:
whatsapp:
enabled: true
provider: twilio
from_number: "whatsapp:+15550001234"
templates:
initial_outreach: "HX1234567890abcdef" # Twilio content template SID
Telegram (Grammy)
Telegram is the lowest-friction channel — free to send, no approval required, instant delivery.
Features:
- Bot-based (users must initiate contact with the bot)
- Markdown formatting support
- Inline keyboard buttons for structured responses
- No rate limiting at reasonable volumes
Configuration:
channels:
telegram:
enabled: true
bot_token: "${TELEGRAM_BOT_TOKEN}"
webhook_url: "https://your-agent.com/webhooks/telegram"
The Channel interface
All channels implement a standard interface from @clawrm/channels:
interface Channel {
id: string;
name: string;
// Send a message to a contact
send(
to: string,
message: OutboundMessage,
context: MessageContext
): Promise<SendResult>;
// Start listening for inbound messages
listen(handler: MessageHandler): Promise<void>;
// Stop listening
stop(): Promise<void>;
// Normalize channel-specific message format to NormalizedMessage
normalize(raw: unknown): NormalizedMessage;
}
interface NormalizedMessage {
id: string;
channel: string;
from: string; // Channel-specific identifier (email, phone, telegram_id)
body: string;
timestamp: number;
raw: unknown; // Original message object for debugging
attachments: Attachment[];
}
Adding a custom channel
To add a channel not in the default set (e.g., Slack, Discord, LinkedIn):
Step 1 — Implement the interface
import { Channel, NormalizedMessage } from "@clawrm/channels";
export class SlackChannel implements Channel {
id = "slack";
name = "Slack";
async send(to: string, message: OutboundMessage): Promise<SendResult> {
// Send message to Slack channel or DM
const response = await slackClient.chat.postMessage({
channel: to,
text: message.body,
});
return { success: true, external_id: response.ts };
}
async listen(handler: MessageHandler): Promise<void> {
// Set up Slack event listener (app_mention, message.im, etc.)
slackApp.event("message", async ({ event }) => {
const normalized = this.normalize(event);
await handler(normalized);
});
}
normalize(raw: SlackMessageEvent): NormalizedMessage {
return {
id: raw.client_msg_id,
channel: "slack",
from: raw.user,
body: raw.text,
timestamp: parseInt(raw.ts) * 1000,
raw,
attachments: [],
};
}
}
Step 2 — Register the channel
// In your agent configuration
import { ChannelRegistry } from "@clawrm/channels";
import { SlackChannel } from "./channels/slack";
ChannelRegistry.register(new SlackChannel(slackConfig));
Step 3 — Add config block
channels:
slack:
enabled: true
bot_token: "${SLACK_BOT_TOKEN}"
app_token: "${SLACK_APP_TOKEN}"
Message normalization
Regardless of origin, all messages flow through the same pipeline after normalization:
- Channel listener fires
normalize(raw)→NormalizedMessage - CRM lookup by
fromidentifier → find or create contact - Thread lookup or creation
- Message stored in thread
- Agent invoked with full context
- Response dispatched through originating channel
- Response stored in thread
This means the agent handles a Telegram message and an email with identical logic — the channel is just a delivery mechanism.
