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
- 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.
- Trim or merge the options down to at most 3 before calling `send_interactive_buttons`.
- 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
- Route variable-length choices through `send_choice`; it picks buttons vs list automatically and handles 0 and 1 options gracefully.
- Bound menu size at the source (config or LLM prompt constraint): 3 buttons or 100 list rows.
- Unit-test option-count boundaries (0, 1, 3, 4, 100, 101) against the channel.
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
- WhatsApp interactive list requires at least one section
- WhatsApp interactive list capped at 10 sections (got {})
- WhatsApp interactive list section '{}' capped at 10 rows (go
- WhatsApp API error: {status}
- WhatsApp location marker must be `lat,lng[,name[,address]]`
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/c55459209134ea23.
Report an issue: GitHub.