zeroclaw-labs/zeroclaw · error · anyhow::Error

unsupported heartbeat.target channel: {channel}

Error message

unsupported heartbeat.target channel: {channel}

What it means

validate_heartbeat_channel_config checks heartbeat.target against the channel registry via config.channels.is_known_channel (case-insensitive). The registry covers telegram, discord, slack, mattermost, webhook, imessage, matrix, signal, whatsapp, linq, nextcloud_talk, email, gmail_push, irc, twitch, lark, line, dingtalk, wecom, wecom_ws, wechat, qq, twitter, mochat, nostr, clawdtalk, reddit, bluesky, git, voice_call, voice_wake, voice_duplex, mqtt, amqp, and filesystem; anything else is rejected before delivery is attempted.

Source

Thrown at crates/zeroclaw-runtime/src/daemon/mod.rs:2436

    }
    if !config.channels.discord.is_empty() {
        // Discord requires explicit target — can't auto-detect
        return None;
    }
    if !config.channels.slack.is_empty() {
        // Slack requires explicit target
        return None;
    }
    if !config.channels.mattermost.is_empty() {
        // Mattermost requires explicit target
        return None;
    }
    None
}

fn validate_heartbeat_channel_config(config: &Config, channel: &str) -> Result<()> {
    if !config.channels.is_known_channel(channel) {
        anyhow::bail!("unsupported heartbeat.target channel: {channel}");
    }
    if !config.channels.is_channel_configured(channel) {
        anyhow::bail!(
            "heartbeat.target is set to {channel} but channels.{channel} is not configured"
        );
    }
    if !config.channels.is_channel_deliverable(channel) {
        anyhow::bail!(
            "heartbeat.target is set to {channel} but {channel} is an input-only channel that cannot deliver outbound messages"
        );
    }
    Ok(())
}

fn has_supervised_channels(config: &Config) -> bool {
    config.channels.has_any_enabled()
}

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Fix the channel name spelling — matching is case-insensitive, so telegram, Telegram, and TELEGRAM all pass
  2. Pick a channel that exists in the registry (telegram, discord, slack, matrix, email, webhook, ...)

Example fix

# before
[heartbeat]
enabled = true
agent = "main"
target = "telgram"
to = "123456789"

# after
[heartbeat]
enabled = true
agent = "main"
target = "telegram"
to = "123456789"
Defensive patterns

Strategy: type-guard

Validate before calling

if let Some(channel) = &config.heartbeat.target {
    if !config.channels.is_known_channel(channel) {
        anyhow::bail!("unsupported heartbeat.target channel: {channel}");
    }
}

Type guard

fn is_known_heartbeat_channel(config: &zeroclaw_config::schema::Config, channel: &str) -> bool {
    config.channels.is_known_channel(channel)
}

Try / catch

if let Err(err) = run_heartbeat_worker(config.clone()).await {
    if err.to_string().contains("unsupported heartbeat.target channel") {
        eprintln!("config error: heartbeat.target names a channel not in the registry (typo?)");
    }
    return Err(err);
}

Prevention

When it happens

Trigger: heartbeat.target = "telgram" (typo); using a provider label like "openai" as a channel name; referencing a channel kind not present in this build's registry.

Common situations: Typos in channel names; assuming a plugin/add-on channel is registered; confusing model provider names with channel names.

Understand the failure class

Background: Config validation failed: what "invalid value for {key}" and settings-rejection errors mean across 19 open-source libraries — this error's family across 19 libraries.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/7265bd312d8e06c5. Report an issue: GitHub.