zeroclaw-labs/zeroclaw · error

WhatsApp interactive buttons require 1..=3 options (got {});

Error message

WhatsApp interactive buttons require 1..=3 options (got {}); use send_interactive_list for more

What it means

ZeroClaw's WhatsApp Cloud channel rejects `send_interactive_buttons` before any HTTP call when `buttons` is empty or contains more than 3 entries. The 1..=3 cap mirrors Meta's interactive button-message contract (quick-reply buttons), so the library fails fast with a local validation error instead of letting the Graph API return a 400. The message itself points you to `send_interactive_list`, which handles up to 100 options via sections.

Source

Thrown at crates/zeroclaw-channels/src/whatsapp.rs:559

        messages
    }

    /// Send an interactive button message (≤ 3 buttons per Meta's Cloud
    /// API limit). Each tuple is `(id, label)`; `id` round-trips back
    /// through the inbound `interactive.button_reply.id` field as a
    /// `[choice]<id>` synthetic `ChannelMessage` content (see
    /// `parse_webhook_payload`).
    ///
    /// Meta's `interactive` body schema:
    /// https://developers.facebook.com/docs/whatsapp/cloud-api/reference/messages
    pub async fn send_interactive_buttons(
        &self,
        recipient: &str,
        body_text: &str,
        buttons: &[(String, String)],
    ) -> anyhow::Result<()> {
        if buttons.is_empty() || buttons.len() > 3 {
            anyhow::bail!(
                "WhatsApp interactive buttons require 1..=3 options (got {}); \
                 use send_interactive_list for more",
                buttons.len()
            );
        }
        let url = format!(
            "https://graph.facebook.com/v18.0/{}/messages",
            self.endpoint_id
        );
        ensure_https(&url)?;
        let to = recipient.strip_prefix('+').unwrap_or(recipient);
        let action_buttons: Vec<serde_json::Value> = buttons
            .iter()
            .map(|(id, label)| {
                // Meta caps button title at 20 chars and id at 256.
                let title = label.chars().take(20).collect::<String>();
                let id_trim = id.chars().take(256).collect::<String>();
                serde_json::json!({

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Branch on option count: use `send_interactive_buttons` for 1-3 options and `send_interactive_list` (or simply `send_choice`, which routes automatically) for 4-100.
  2. Trim or merge the options down to at most 3 before calling `send_interactive_buttons`.
  3. For >100 options, split into multiple prompts or paginate with a 'show more' option.

Example fix

// before
channel
    .send_interactive_buttons(to, "Pick one:", &all_options)
    .await?; // bails when all_options.len() > 3

// after
if (1..=3).contains(&all_options.len()) {
    channel.send_interactive_buttons(to, "Pick one:", &all_options).await?;
} else {
    // interactive list: 10 sections x 10 rows, up to 100 options
    channel.send_choice(to, "Pick one:", &all_options).await?;
}
Defensive patterns

Strategy: validation

Validate before calling

let n = options.len();
if n == 0 {
    // nothing to choose from: send prompt as text (or no-op)
    return channel.send(&SendMessage::new(prompt, recipient)).await;
}
if n <= 3 {
    channel.send_interactive_buttons(recipient, prompt, &options).await
} else {
    channel.send_choice(recipient, prompt, &options).await // interactive list
}

Type guard

fn fits_whatsapp_buttons(buttons: &[(String, String)]) -> bool {
    (1..=3).contains(&buttons.len())
}

Prevention

When it happens

Trigger: Calling `WhatsAppChannel::send_interactive_buttons(recipient, body_text, &buttons)` with `buttons.len() == 0` or `> 3`. Note that `send_choice` routes only 2-3 options here, so the direct-call path with 4+ options (e.g. a menu ported from Telegram inline keyboards) is the usual trigger.

Common situations: Dynamic option lists (menus, LLM-generated choices, approval actions) whose length varies at runtime and occasionally exceeds 3; migrating from channels with higher button limits; an empty button vec produced by a filter chain that removed every option.

Related errors


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