zeroclaw-labs/zeroclaw · error

Composio v3 connected accounts lookup failed: {err}

Error message

Composio v3 connected accounts lookup failed: {err}

What it means

Raised by ComposioTool::list_connected_accounts when the GET to https://backend.composio.dev/api/v3/connected_accounts (filters: limit=50, statuses INITIALIZING/ACTIVE/INITIATED, optional toolkit_slugs and user_ids) returns a non-2xx status. The {err} part is built by response_error(): the HTTP status code plus the API's own error.message field when the body is JSON, with connected_account_id/entity_id/user_id values redacted and the text truncated to 240 chars. It surfaces directly from action='list_accounts'/'connected_accounts', and indirectly from action='execute' when connected_account_id is omitted and resolve_connected_account_ref must auto-resolve the account on cache miss (the anyhow error propagates unchanged through execute_action).

Source

Thrown at crates/zeroclaw-tools/src/composio.rs:129

            ("statuses", "ACTIVE"),
            ("statuses", "INITIATED"),
        ]);

        if let Some(app) = app_name
            .map(normalize_app_slug)
            .filter(|app| !app.is_empty())
        {
            req = req.query(&[("toolkit_slugs", app.as_str())]);
        }

        if let Some(entity) = entity_id {
            req = req.query(&[("user_ids", entity)]);
        }

        let resp = req.send().await?;
        if !resp.status().is_success() {
            let err = response_error(resp).await;
            anyhow::bail!("Composio v3 connected accounts lookup failed: {err}");
        }

        let body: ComposioConnectedAccountsResponse = resp
            .json()
            .await
            .context("Failed to decode Composio v3 connected accounts response")?;
        Ok(body.items)
    }

    fn cache_connected_account(&self, app_name: &str, entity_id: &str, connected_account_id: &str) {
        let key = connected_account_cache_key(app_name, entity_id);
        self.recent_connected_accounts
            .write()
            .insert(key, connected_account_id.to_string());
    }

    fn get_cached_connected_account(&self, app_name: &str, entity_id: &str) -> Option<String> {
        let key = connected_account_cache_key(app_name, entity_id);

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Inspect the HTTP code embedded in the message: 401/403 means the Composio API key is wrong or revoked — fix composio.api_key and verify with a cheap action='list' call.
  2. On HTTP 429, back off and retry: the tool makes a single attempt per call with no built-in retry, so pace or wrap the call site.
  3. Use the exact Composio toolkit slug for app (for example 'gmail'); a genuinely wrong app name still fails server-side.
  4. Confirm entity_id exists in the workspace behind the API key; a mismatched user_ids filter fails or returns nothing useful.
  5. On HTTP 5xx, check Composio status and retry later.

Example fix

// before: execute with no connected_account_id dies inside account auto-resolution
let args = json!({"action": "execute", "tool_slug": "github-list-repositories"});
let res = tool.execute(args).await; // Err: 'connected accounts lookup failed' when key is bad

// after: pre-flight the key with a cheap call, then pass connected_account_id explicitly
let probe = tool.execute(json!({"action": "list", "app": "github"})).await?;
if !probe.success {
    anyhow::bail!("composio credentials invalid: {:?}", probe.error);
}
let accounts = tool.execute(json!({"action": "list_accounts", "app": "github"})).await?;
// take the account id from `accounts.output` and send it as connected_account_id
Defensive patterns

Strategy: retry

Validate before calling

// Fail fast on missing credentials before wiring the tool into the agent
let api_key = config.composio_api_key.trim();
if api_key.is_empty() {
    anyhow::bail!("composio.api_key is not configured");
}
let tool = ComposioTool::new(api_key, config.entity_id.as_deref(), security);
// Cheap auth probe: surfaces 401 immediately instead of during execute
let probe = tool.execute(json!({"action": "list", "app": "gmail"})).await?;
if !probe.success {
    anyhow::bail!("composio auth probe failed: {:?}", probe.error);
}

Type guard

fn is_retryable_composio_error(msg: &str) -> bool {
    ["HTTP 429", "HTTP 500", "HTTP 502", "HTTP 503", "HTTP 504"]
        .iter()
        .any(|code| msg.contains(code))
}

Try / catch

match tool.execute(args).await {
    Ok(result) if result.success => { /* use output */ }
    Ok(result) => {
        let err = result.error.unwrap_or_default();
        if is_retryable_composio_error(&err) {
            // backoff (e.g. 2s then 10s) and retry once; the lookup is an idempotent GET
        } else {
            // config problem (401/403/400): surface to the operator, do not retry
        }
    }
    Err(e) => { /* transport/timeout: also retryable */ }
}

Prevention

When it happens

Trigger: Running action='list_accounts' (with or without app) while the x-api-key is invalid or revoked (401/403); a toolkit_slugs filter value Composio rejects (400); rate limiting during high-frequency agent loops (429); Composio backend errors (5xx). Also fires inside action='execute' with no connected_account_id: execute_action -> resolve_connected_account_ref -> this endpoint, so a bad API key or outage makes every execute fail with this message.

Common situations: Rotated or mistyped composio.api_key in zeroclaw config; free-tier rate limits hit by an agent that executes many Composio actions; an app slug with wrong spelling so the server-side filter fails (normalize_app_slug fixes case and underscores, not typos); an entity_id that does not exist in the Composio workspace behind the key; a Composio v3 incident.

Related errors


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