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
- 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.
- 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.
- Use the exact Composio toolkit slug for app (for example 'gmail'); a genuinely wrong app name still fails server-side.
- Confirm entity_id exists in the workspace behind the API key; a mismatched user_ids filter fails or returns nothing useful.
- 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
- Pre-flight the API key with action='list' at startup
- Pass connected_account_id explicitly on execute to skip the auto-resolution call that hits this endpoint
- Cache list_accounts results per (app, entity) instead of re-querying in loops
- Keep toolkit slugs in sync with the Composio catalog
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
- Composio v3 API error: {err}
- Embedding API error {status}: {text}
- Composio v3 connect failed: {err}
- Composio v3 auth config lookup failed: {err}
- OpenAI token refresh is in backoff for {remaining}s due to p
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/7ce9c26a1edc002b.
Report an issue: GitHub.