zeroclaw-labs/zeroclaw · error
Composio v3 API error: {err}
Error message
Composio v3 API error: {err} What it means
list_actions_v3 sends GET {base}/tools with the x-api-key header and query params limit=200, toolkit_versions=latest, plus toolkits/toolkit_slug filters when an app is given (composio.rs:63-75, 320-335). If the response status is not 2xx, response_error formats it as 'HTTP <code>' plus the API's error message, sanitized (entity/user ids redacted, capped at 240 chars) (composio.rs:1039-1080). This bail wraps that text; it surfaces from tool action='list' and also inside execute_action's slug-cache priming as 'Failed to refresh action list for app ...'.
Source
Thrown at crates/zeroclaw-tools/src/composio.rs:74
pub async fn list_actions(
&self,
app_name: Option<&str>,
) -> anyhow::Result<Vec<ComposioAction>> {
self.list_actions_v3(app_name).await
}
async fn list_actions_v3(&self, app_name: Option<&str>) -> anyhow::Result<Vec<ComposioAction>> {
let url = format!("{COMPOSIO_API_BASE_V3}/tools");
let req = self
.client()
.get(&url)
.header("x-api-key", &self.api_key)
.query(&Self::build_list_actions_v3_query(app_name));
let resp = req.send().await?;
if !resp.status().is_success() {
let err = response_error(resp).await;
anyhow::bail!("Composio v3 API error: {err}");
}
let body: ComposioToolsResponse = resp
.json()
.await
.context("Failed to decode Composio v3 tools response")?;
self.update_action_slug_cache_from_v3_items(&body.items);
Ok(map_v3_tools_to_actions(body.items))
}
fn update_action_slug_cache_from_v3_items(&self, items: &[ComposioV3Tool]) {
for item in items {
let Some(slug) = item.slug.as_deref().or(item.name.as_deref()) else {
continue;
};
self.cache_action_slug(slug, slug);
if let Some(name) = item.name.as_deref() {
self.cache_action_slug(name, slug);View on GitHub (pinned to 88bb9c8533)
Solutions
- Verify the key out-of-band: curl -H 'x-api-key: <key>' 'https://backend.composio.dev/api/v3/tools?limit=1' should return 200; if 401/403, fix the composio api_key in config.
- Confirm the app slug against Composio's catalog and pass the bare toolkit name (e.g. 'gmail'), not an action name.
- On HTTP 429 or 5xx, retry with backoff (the client already uses 60s connect/10s pool timeouts via build_runtime_proxy_client_with_timeouts).
- If errors persist with a valid key, check Composio status/announcements for a v3 API change and update zeroclaw.
Example fix
// before
let actions = composio.list_actions(Some(app)).await?;
// after: retry transient failures, surface auth errors immediately
let actions = match composio.list_actions(Some(app)).await {
Ok(actions) => actions,
Err(e) if e.to_string().contains("HTTP 429") || e.to_string().contains("HTTP 5") => {
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
composio.list_actions(Some(app)).await?
}
Err(e) => return Err(e),
}; Defensive patterns
Strategy: retry
Validate before calling
fn composio_call_ready(api_key: &str, app: Option<&str>) -> Result<(), String> {
if api_key.trim().is_empty() {
return Err("Composio api_key is empty".into());
}
if let Some(app) = app {
let slug = app.trim().to_ascii_lowercase();
if slug.is_empty() || slug.chars().any(|c| c.is_whitespace()) {
return Err(format!("suspicious app slug: '{app}'"));
}
}
Ok(())
} Try / catch
let mut attempt = 0;
loop {
attempt += 1;
match composio.list_actions(Some(app)).await {
Ok(actions) => break actions,
Err(e) if attempt < 3 && (e.to_string().contains("HTTP 429") || e.to_string().contains("HTTP 5")) => {
tokio::time::sleep(std::time::Duration::from_secs(2u64 * attempt as u64)).await;
}
Err(e) => {
// 401/403/400 are not transient: surface immediately
return Err(e);
}
}
} Prevention
- Fail fast on empty/placeholder api_key at startup instead of at first list call.
- Pass verified toolkit slugs (e.g. 'gmail'), never action names, as the app filter.
- Retry only HTTP 429/5xx with backoff; treat 401/403 as a config fix, 400 as a slug fix.
- Pin a recent zeroclaw build so the v3 endpoint contract matches what Composio serves.
When it happens
Trigger: Calling ComposioTool::list_actions / tool action='list' when: the api_key is invalid/expired (401/403), the 'app' slug does not exist so the toolkits filter is rejected (400), the Composio quota is exhausted (429), or the backend is erroring (5xx). Also fires indirectly when action='execute' cannot resolve a slug and tries to refresh the action list.
Common situations: Rotated or mistyped Composio API keys, app slugs guessed by the LLM ('gmail_send' instead of 'gmail'), hitting workspace rate limits during heavy agent loops, and v3 API contract drift when running an older zeroclaw build.
Related errors
- Embedding API error {status}: {text}
- Composio v3 connected accounts lookup failed: {err}
- Composio v3 auth config lookup failed: {err}
- providers.models.ollama.{alias}.model uses ':cloud', but no
- GLM API key not set or invalid format. Expected 'id.secret'.
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/2b30146cc2c4ccb5.
Report an issue: GitHub.