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

heartbeat.target is set to {channel} but channels.{channel}

Error message

heartbeat.target is set to {channel} but channels.{channel} is not configured

What it means

The heartbeat target passes is_known_channel but is_channel_configured returns false: the channel kind exists in the registry yet no [channels.<name>] instance is present in the loaded config. Delivery needs a configured channel to send through, so validation fails before the worker starts.

Source

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

        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()
}

// run_mqtt_sop_listener has been moved to zeroclaw-channels::orchestrator::mqtt.
// The daemon now receives it as a starter via DaemonRegistry::register_mqtt.

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Add a [channels.<name>] configuration block (token, chat id, etc.) to the config the daemon actually loads
  2. Or point heartbeat.target at a channel that is already configured
  3. Confirm which config file the daemon resolves (data dir / config path) before editing

Example fix

# before
[heartbeat]
enabled = true
agent = "main"
target = "telegram"
to = "123456789"
# no [channels.telegram] anywhere

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

[channels.telegram]
bot_token = "..."
Defensive patterns

Strategy: validation

Validate before calling

if let Some(channel) = &config.heartbeat.target {
    if !config.channels.is_channel_configured(channel) {
        anyhow::bail!("heartbeat.target = {channel} but channels.{channel} is not configured");
    }
}

Type guard

fn heartbeat_channel_is_configured(config: &zeroclaw_config::schema::Config) -> bool {
    match config.heartbeat.target.as_deref() {
        Some(channel) => config.channels.is_channel_configured(channel),
        None => true,
    }
}

Try / catch

if let Err(err) = run_heartbeat_worker(config.clone()).await {
    if err.to_string().contains("is not configured") {
        eprintln!("config error: add a [channels.<name>] block for the heartbeat target channel");
    }
    return Err(err);
}

Prevention

When it happens

Trigger: heartbeat.target = "telegram" while [channels.telegram] is absent or empty; the daemon loading a different zeroclaw.toml/profile than the one edited; channel credentials supplied via environment variables that are not set in the daemon's environment.

Common situations: Config split across multiple files or profiles; env-driven deployments where the daemon misses the channel env vars; copying heartbeat config to a fresh install without the channel block.

Related errors


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