goClaw/Documentation

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

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:

  1. Channel listener fires normalize(raw)NormalizedMessage
  2. CRM lookup by from identifier → find or create contact
  3. Thread lookup or creation
  4. Message stored in thread
  5. Agent invoked with full context
  6. Response dispatched through originating channel
  7. 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.