B Buffy Agent
Buffy Agent Blog · Engineering

Building Multi-Channel Bots on Top of One Behavior Core

How to plug Telegram, Slack, ChatGPT and internal bots into a single Buffy behavior engine instead of duplicating logic.

It’s tempting to build a separate bot for every channel: one for Telegram, one for Slack, one for your internal dashboard. It works—until your product logic starts drifting between surfaces and every “small change” becomes three rewrites.

Buffy Agent is built around the opposite pattern: one behavior core, many thin interfaces. The bots are replaceable. The behavior model and history are not.

Definition: behavior core vs. adapter

  • Behavior core: the single place where you model activities, reminders, and memory—and decide what the agent should do next.
  • Adapter: a thin layer that turns platform events (Telegram/Slack/ChatGPT/internal) into a unified message, and renders replies back in that platform’s UX.

If you keep this separation strict, adding a new channel becomes an integration task, not a new product.

The anti-pattern: a bot brain per channel

Multi-bot stacks usually fail in the same ways:

  • Feature drift: Telegram supports snooze; Slack doesn’t. The experience diverges.
  • Copy-paste logic: “completion rules” and scheduling get duplicated, then updated inconsistently.
  • Fragmented state: each channel has “its own truth”, so you can’t reason over the whole user journey.

If you’re building an agent that coordinates habits/tasks/routines across a day, drift isn’t just annoying—it breaks correctness.

The pattern: one core, thin adapters

The core idea is simple:

  • Adapters receive events from channels (Telegram/Slack/ChatGPT/internal).
  • They normalize events into a unified message format.
  • The behavior core owns the logic: parse intent → update activities/history → schedule reminders → generate replies.

Adapters should not contain rules like “what counts as done” or “how to schedule a routine.” That’s how drift happens.

A minimal unified message format

You don’t need a giant schema. You need enough to keep behavior consistent:

  • Identity: user_id (and optionally workspace_id)
  • Channel: platform (telegram/slack/chatgpt/internal) + conversation/thread ids
  • Message: user text or structured command
  • Timestamp: when it happened
  • Context (optional): timezone, locale, user preferences

The adapter’s job is mapping whatever the platform gives you into this envelope.

Example: normalized envelope (illustrative JSON)

Your exact schema can differ; the point is one shape every adapter produces before calling the core:

{
  "user_id": "usr_01",
  "platform": "telegram",
  "conversation_id": "tg:123456",
  "message": { "text": "snooze 20", "raw": {} },
  "occurred_at": "2026-03-28T07:41:12-07:00",
  "context": { "timezone": "America/Los_Angeles" }
}

The core returns a reply payload (text, optional buttons, follow-up schedule)—the adapter turns that into Telegram messages, Slack blocks, or ChatGPT structured responses. Same decision, different chrome.

Example flow: one user, two surfaces

  1. Telegram: user taps “Done” on a morning routine nudge → adapter sends completed semantics in the envelope → core logs episodic event → no second ping in-window.
  2. Slack: teammate posts “weekly review done” in a ritual thread → adapter maps to the same completed event for that activity id → one history stream; the weekly briefing in ChatGPT sees both.

Without a shared core, those become two unrelated logs and coaching breaks.

Reference architecture (mental model)

At a high level:

  • Interface layer: Telegram handler, Slack app, ChatGPT surface, internal bot
  • Gateway: auth, rate limits, normalization
  • Behavior core:
    • Activity model (habit/task/routine)
    • Reminder engine
    • Memory system (short-term + episodic + semantic)
    • Reply generation

If you want an OpenClaw-centric reference boundary, start here:

Concrete example: adding a new channel in one week

Say you already support Telegram and you want to add Slack.

What you don’t want to do

  • Re-implement “snooze 20m” in Slack.
  • Rebuild routine scheduling logic in Slack.
  • Invent a second database for Slack-specific state.

What you do instead (thin adapter)

  1. Receive Slack events (message, thread, user).
  2. Normalize into your unified envelope.
  3. Call the behavior core API.
  4. Render the core’s reply back into Slack’s UX conventions (threads, DMs, channel messages).

The result: behavior stays consistent across channels because the same core decisions drive both.

Practical checklist (to avoid drift)

  • Keep adapters “dumb”: I/O + normalization + rendering.
  • Centralize behavior rules: completion, snooze/skip semantics, scheduling, escalation.
  • Use one canonical event history so the core can learn cross-channel patterns.
  • Prefer windows over fixed times; avoid platform-specific scheduling hacks.
  • Add “further reading” links in your product docs so users know where to start.

Where to go next

Further reading