# How Messaging Channels Handle Inbound and Outbound Communication in OpenHuman

> Discover how OpenHuman manages inbound and outbound communication through its unified messaging channels. Learn about event bus routing, agent runtime processing, and thread context preservation.

- Repository: [Tiny Humans/openhuman](https://github.com/tinyhumansai/openhuman)
- Tags: how-to-guide
- Published: 2026-05-12

---

**OpenHuman routes every external chat message through a unified channel abstraction: inbound messages are published to a global event bus, processed by an agent runtime, and returned via provider-specific `SendMessage` trait implementations that preserve thread context and platform metadata.**

The `tinyhumansai/openhuman` repository implements a Rust-based messaging layer that abstracts Telegram, Discord, Slack, and web interfaces into a single pipeline. Understanding how these messaging channels handle both inbound and outbound communication reveals a clean separation between transport concerns and agent logic, orchestrated through the [`src/openhuman/channels/runtime/dispatch.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/runtime/dispatch.rs) runtime.

## The Unified Channel Architecture

### Common Traits and Types

All channels implement the `Channel` trait defined in [`src/openhuman/channels/mod.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/mod.rs). This interface standardizes how messages enter and exit the system through the `ChannelMessage` and `SendMessage` structures, ensuring that provider-specific details (like Telegram's chat IDs or Discord's thread timestamps) remain isolated from the core agent logic.

### The Dispatch Runtime

The [`src/openhuman/channels/runtime/dispatch.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/runtime/dispatch.rs) file contains the orchestration logic that coordinates the full turn lifecycle. It manages the transition from external provider events to internal `AgentTurnRequest` objects, handling context assembly and acknowledgment generation through dedicated helper functions.

## Inbound Communication Flow

### Publishing to the Global Event Bus

When a provider listener receives a native message, it constructs a `ChannelMessage` and publishes a `DomainEvent::ChannelInboundMessage` using `event_bus::publish_global`. This decouples the transport layer from processing logic, allowing the system to scale message ingestion independently from agent computation.

```rust
// 1. Provider listener creates a ChannelMessage
let msg = ChannelMessage { channel: "telegram".into(), ... };

// 2. Publish to the event bus
event_bus::publish_global(DomainEvent::ChannelInboundMessage(msg));

// 3. In dispatch.rs the inbound message worker builds context
let context = build_channel_context_block(&msg);
let reaction = select_acknowledgment_reaction(&msg.content);

// 4. The context + reaction are spliced into the prompt sent to the agent

```

### Context Assembly and Acknowledgments

The **Channel Inbound Subscriber** (registered in [`src/openhuman/channels/bus.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/bus.rs)) consumes these events and prepares the agent prompt. Two critical functions in [`src/openhuman/channels/runtime/dispatch.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/runtime/dispatch.rs) handle this preparation.

The **build_channel_context_block** function injects channel metadata into the agent context, instructing the LLM how to route any scheduled reminders back to the correct platform:

```rust
fn build_channel_context_block(msg: &traits::ChannelMessage) -> String {
    if msg.channel.trim().eq_ignore_ascii_case("web") {
        return String::new();               // no context for web/CLI
    }
    let reply_target = msg.reply_target.trim();
    if reply_target.is_empty() {
        return String::new();
    }
    format!(
        "[Channel context]\n\
         You are responding via the \"{channel}\" channel. Reply target: \"{reply_target}\".\n\
         For any cron/scheduled reminder you create with `cron_add`, set `delivery` to \
         {{ \"mode\": \"announce\", \"channel\": \"{channel}\", \"to\": \"{reply_target}\" }} \
         so the reminder is delivered back here instead of the in‑app web stream.\n\n",
        channel = msg.channel,
        reply_target = msg.reply_target,
    )
}

```

The **select_acknowledgment_reaction** function analyzes message content to choose an appropriate emoji reaction, providing immediate user feedback while the agent processes the request:

```rust
fn select_acknowledgment_reaction(content: &str) -> &'static str {
    let l = content.to_lowercase();
    let v = content.len().wrapping_add(content.chars().next().map_or(0, |c| c as usize)) & 1;
    let opts = if contains_any(&l, &["thank", "thx"]) {
        &["❤️", "🙏"]
    } else if contains_any(&l, &["amazing", "awesome"]) {
        &["🔥", "🎉"]
    } else if contains_any(&l, &["price", "btc"]) {
        &["💯", "⚡"]
    } else if contains_any(&l, &["code", "function"]) {
        &["👨‍💻", "🤓"]
    } else if starts_with_any(&l, &["hi", "hello"]) {
        &["🤗", "😁"]
    } else if l.contains('?') {
        &["🤔", "✍️"]
    } else {
        &["👀", "✍️"]
    };
    opts[v % opts.len()]
}

```

## Outbound Communication Flow

### The SendMessage Trait

After the agent generates a response, the runtime calls the `send_message` method on the concrete channel implementation. This method receives a `SendMessage` struct containing the response text, target recipient, and preserved thread identifiers from the original inbound message:

```rust
// After the agent turn finishes, the runtime calls:
channel.send_message(SendMessage {
    text: response_text,
    reply_target: msg.reply_target.clone(),
    thread_ts: msg.thread_ts,          // preserved for threaded platforms
});

```

### Platform-Specific Delivery

Each provider implements `Channel::send_message` to handle API authentication, message formatting, and thread preservation. The implementations in [`src/openhuman/channels/providers/telegram/channel_send.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/providers/telegram/channel_send.rs) and [`src/openhuman/channels/providers/discord/channel_send.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/providers/discord/channel_send.rs) demonstrate how the same trait accommodates different platform constraints.

Telegram automatically handles thread timestamps via `set_thread_ts`:

```rust
impl Channel for TelegramChannel {
    async fn send_message(&self, msg: SendMessage) -> Result<()> {
        // Preserve thread_ts if the inbound message was part of a thread
        let mut payload = self.api.send_text(&msg.text, &msg.recipient).await?;
        if let Some(thread) = msg.thread_ts {
            payload.set_thread_ts(thread);
        }
        self.api.execute(payload).await?;
        Ok(())
    }
}

```

Discord requires explicit message referencing through `reference_message`:

```rust
impl Channel for DiscordChannel {
    async fn send_message(&self, msg: SendMessage) -> Result<()> {
        let mut builder = self.api.create_message(&msg.recipient);
        builder.content(&msg.text);
        // Discord does **not** fabricate a thread_ts when none is supplied
        if let Some(ts) = msg.thread_ts {
            builder.reference_message(ts);
        }
        builder.await?;
        Ok(())
    }
}

```

Both implementations preserve the `thread_ts` from the inbound `ChannelMessage` to ensure replies appear in the correct conversation thread.

## Summary

- OpenHuman abstracts Telegram, Discord, and other platforms through a unified `Channel` trait defined in [`src/openhuman/channels/mod.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/mod.rs).
- Inbound messages flow through `event_bus::publish_global` and are processed by [`src/openhuman/channels/runtime/dispatch.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/runtime/dispatch.rs) to build context blocks and select acknowledgment reactions.
- The **build_channel_context_block** function ensures agents know how to route scheduled reminders back to the correct channel and thread.
- Outbound replies use the `SendMessage` trait, with concrete implementations in provider-specific [`channel_send.rs`](https://github.com/tinyhumansai/openhuman/blob/main/channel_send.rs) files preserving `thread_ts` to maintain conversation continuity.
- Provider-specific logic handles platform API nuances while keeping the core agent runtime platform-agnostic.

## Frequently Asked Questions

### How does OpenHuman preserve conversation threads across different messaging platforms?

The system extracts thread identifiers from inbound messages and stores them in the `ChannelMessage` struct's `thread_ts` field. When the agent runtime calls `send_message`, it passes this identifier back to the provider implementation. Telegram uses `set_thread_ts` while Discord uses `reference_message`, ensuring replies appear in the same thread regardless of platform-specific API differences.

### Can new messaging channels be added without modifying the core agent logic?

Yes. New platforms only need to implement the `Channel` trait from [`src/openhuman/channels/mod.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/mod.rs), specifically the `send_message` method, and register an inbound listener that publishes `DomainEvent::ChannelInboundMessage` events. The dispatch runtime and agent processing logic remain unchanged because they operate on the abstract `ChannelMessage` type rather than platform-specific implementations.

### What determines which acknowledgment emoji gets sent?

The `select_acknowledgment_reaction` function in [`src/openhuman/channels/runtime/dispatch.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/runtime/dispatch.rs) analyzes message content using keyword matching. Questions trigger 🤔 or ✍️, gratitude triggers ❤️ or 🙏, and code-related messages trigger 👨‍💻 or 🤓. This provides immediate visual feedback while the LLM agent generates its formal response.

### Where does the channel context block appear in the agent prompt?

The `build_channel_context_block` function in [`src/openhuman/channels/runtime/dispatch.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/runtime/dispatch.rs) injects metadata at the beginning of the prompt. This block informs the agent which channel originated the message and specifies the `delivery` parameters for any cron reminders, ensuring scheduled messages return to the correct external platform rather than the default web stream.