zeroclaw-labs/zeroclaw · error

WhatsApp interactive list section '{}' capped at 10 rows (go

Error message

WhatsApp interactive list section '{}' capped at 10 rows (got {})

What it means

Each section of a WhatsApp interactive list may hold at most 10 rows; `send_interactive_list` validates every section up front and names the first offending section's title and row count. This mirrors Meta's list-message contract and fails before any HTTP call. The total message cap is 100 rows (10 sections x 10 rows).

Source

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

    pub async fn send_interactive_list(
        &self,
        recipient: &str,
        body_text: &str,
        button_text: &str,
        sections: &[InteractiveListSection],
    ) -> anyhow::Result<()> {
        if sections.is_empty() {
            anyhow::bail!("WhatsApp interactive list requires at least one section");
        }
        if sections.len() > 10 {
            anyhow::bail!(
                "WhatsApp interactive list capped at 10 sections (got {})",
                sections.len()
            );
        }
        for s in sections {
            if s.rows.len() > 10 {
                anyhow::bail!(
                    "WhatsApp interactive list section '{}' capped at 10 rows (got {})",
                    s.title,
                    s.rows.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_sections: Vec<serde_json::Value> = sections
            .iter()
            .map(|s| {
                let rows: Vec<serde_json::Value> = s
                    .rows
                    .iter()

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Chunk rows into sections of at most 10 (`options.chunks(10)`), the same way `send_choice` does.
  2. Trim the list or paginate interactively (e.g. a final 'More' row that triggers the next page).
  3. For flat option lists, prefer `send_choice`, which chunks automatically.

Example fix

// before
let sections = vec![InteractiveListSection {
    title: "All".into(),
    rows: all_25_rows, // > 10 rows -> bails
}];

// after
let sections: Vec<_> = all_rows
    .chunks(10)
    .enumerate()
    .map(|(i, c)| InteractiveListSection {
        title: format!("Options {}-{}", i * 10 + 1, i * 10 + c.len()),
        rows: c.to_vec(),
    })
    .collect();
Defensive patterns

Strategy: validation

Validate before calling

for s in &sections {
    anyhow::ensure!(s.rows.len() <= 10, "section '{}' has {} rows (max 10)", s.title, s.rows.len());
}
channel.send_interactive_list(recipient, body, button_text, &sections).await

Type guard

fn rows_within_cap(section: &InteractiveListSection) -> bool {
    section.rows.len() <= 10
}

Prevention

When it happens

Trigger: Passing any `InteractiveListSection` whose `rows.len() > 10` — e.g. a single 'All items' section containing 25 rows, or uneven chunking that leaves a long tail in one section.

Common situations: Flat item lists dumped into one section; forgetting that the per-section cap (10) is far smaller than the total cap (100); manual section construction that bypasses the chunking `send_choice` does.

Related errors


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