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

  1. 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.
  2. Confirm the app slug against Composio's catalog and pass the bare toolkit name (e.g. 'gmail'), not an action name.
  3. On HTTP 429 or 5xx, retry with backoff (the client already uses 60s connect/10s pool timeouts via build_runtime_proxy_client_with_timeouts).
  4. 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

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


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