zeroclaw-labs/zeroclaw · error
WhatsApp send_choice: {} options exceeds list cap ({}); spli
Error message
WhatsApp send_choice: {} options exceeds list cap ({}); split the prompt or call send_interactive_list directly What it means
`send_choice` renders more than 3 options as a WhatsApp interactive list, and WhatsApp lists cap at 10 sections x 10 rows = 100 options total. Passing more than 100 options bails before any send. Calling `send_interactive_list` directly (as the message suggests) does not raise the ceiling — the underlying Meta caps are the same — so splitting or trimming is the real fix.
Source
Thrown at crates/zeroclaw-channels/src/whatsapp.rs:891
if options.len() >= 2 && options.len() <= 3 {
let buttons: Vec<(String, String)> = options
.iter()
.map(|(id, label)| (id.clone(), label.clone()))
.collect();
return self
.send_interactive_buttons(recipient, prompt, &buttons)
.await;
}
if options.len() > 3 {
// List messages: max 10 rows per section, max 10 sections per
// message → up to 100 options. Chunk into sections of ≤10 rows
// each so we don't blow past the per-section row cap. Section
// titles mark the index range so list UIs render cleanly.
const MAX_ROWS_PER_SECTION: usize = 10;
const MAX_SECTIONS: usize = 10;
let max_total = MAX_ROWS_PER_SECTION * MAX_SECTIONS;
if options.len() > max_total {
anyhow::bail!(
"WhatsApp send_choice: {} options exceeds list cap ({}); split the prompt or call send_interactive_list directly",
options.len(),
max_total
);
}
let sections: Vec<InteractiveListSection> = options
.chunks(MAX_ROWS_PER_SECTION)
.enumerate()
.map(|(chunk_idx, chunk)| {
let start = chunk_idx * MAX_ROWS_PER_SECTION + 1;
let end = start + chunk.len() - 1;
let title = if options.len() <= MAX_ROWS_PER_SECTION {
"Options".to_string()
} else {
format!("Options {start} to {end}")
};
InteractiveListSection {
title,View on GitHub (pinned to 88bb9c8533)
Solutions
- Limit choices to the top 100 or fewer and add a 'more' option that pages through the remainder.
- Split into multiple prompts by category, each within the cap.
- For very large structured sets, send a text summary or a link instead of an interactive list.
Example fix
// before
channel.send_choice(to, "Pick a doc:", &all_150_options).await?; // > 100 -> bails
// after
let mut top: Vec<(String, String)> = all_150_options.iter().take(99).cloned().collect();
top.push(("more".into(), "Show more results…".into())); // page on 'more'
channel.send_choice(to, "Pick a doc:", &top).await?; Defensive patterns
Strategy: validation
Validate before calling
const CHOICE_CAP: usize = 100; // WhatsApp list: 10 sections x 10 rows
if options.len() > CHOICE_CAP {
let top: Vec<_> = options.iter().take(CHOICE_CAP - 1).cloned().collect();
// add a paging option and handle it in your reply handler
channel.send_choice(recipient, prompt, &with_more_option(top)).await
} else {
channel.send_choice(recipient, prompt, &options).await
} Type guard
fn within_choice_cap(options: &[(String, String)]) -> bool {
options.len() <= 100
} Prevention
- Bound choice generation at the source: cap LLM tool menus and search results to <= 100.
- Implement paging ('more' option) instead of ever sending unbounded lists.
- Test the 100/101 boundary in your choice-rendering code.
When it happens
Trigger: `send_choice(recipient, prompt, &options)` with `options.len() > 100` — e.g. an LLM emitting an unbounded tool-option list, or a search/directory flow returning 150 entries.
Common situations: Unbounded LLM-generated menus; product catalogs passed wholesale as choices; assuming the channel will silently truncate (it does not — it fails loudly).
Related errors
- WhatsApp interactive list capped at 10 sections (got {})
- WhatsApp interactive list section '{}' capped at 10 rows (go
- WhatsApp interactive list requires at least one section
- WhatsApp interactive buttons require 1..=3 options (got {});
- 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/ae2316c31b3ea563.
Report an issue: GitHub.