tinyhumansai/openhuman · error · anyhow::Error

composio.list_available_triggers: toolkit must not be empty

Error message

composio.list_available_triggers: toolkit must not be empty

What it means

ComposioClient::list_available_triggers rejects a toolkit name that is empty after trimming, before building the GET /agent-integrations/composio/triggers/available?toolkit=... query. A blank toolkit would return a meaningless catalog (or a backend 400), so the client fails fast.

Source

Thrown at src/openhuman/integrations/composio/client.rs:389

        tracing::debug!(slug = %slug, "[composio] create_trigger");
        self.inner
            .post::<ComposioCreateTriggerResponse>("/agent-integrations/composio/triggers", &body)
            .await
    }

    // ── Trigger management (PR #671) ────────────────────────────────

    /// `GET /agent-integrations/composio/triggers/available` — catalog of
    /// triggers the user could enable for a toolkit. For GitHub the
    /// backend fans out into per-repo entries scoped by `connection_id`.
    pub async fn list_available_triggers(
        &self,
        toolkit: &str,
        connection_id: Option<&str>,
    ) -> Result<ComposioAvailableTriggersResponse> {
        let toolkit = toolkit.trim();
        if toolkit.is_empty() {
            anyhow::bail!("composio.list_available_triggers: toolkit must not be empty");
        }
        let toolkit_q = urlencoding::encode(toolkit);
        let path = match connection_id.map(str::trim).filter(|id| !id.is_empty()) {
            Some(id) => format!(
                "/agent-integrations/composio/triggers/available?toolkit={toolkit_q}&connectionId={}",
                urlencoding::encode(id)
            ),
            None => format!(
                "/agent-integrations/composio/triggers/available?toolkit={toolkit_q}"
            ),
        };
        tracing::debug!(path = %path, "[composio] list_available_triggers");
        self.inner
            .get::<ComposioAvailableTriggersResponse>(&path)
            .await
    }

    /// `GET /agent-integrations/composio/triggers` — currently enabled

View on GitHub (pinned to 7491200858)

Solutions

  1. Skip the catalog call (render an empty picker) until a toolkit is selected
  2. Guard the polling loop on toolkit non-emptiness so it does not fire with empty state
  3. Pass a connection-derived toolkit when the UI is connection-scoped

Example fix

// before
let triggers = client.list_available_triggers(toolkit.as_str(), conn).await?;

// after
let toolkit = toolkit.trim();
let triggers = if toolkit.is_empty() {
    Vec::new()
} else {
    client.list_available_triggers(toolkit, conn).await?.data
};
Defensive patterns

Strategy: validation

Validate before calling

let toolkit = toolkit.trim();
let triggers = if toolkit.is_empty() {
    Vec::new() // picker not ready — skip the catalog call
} else {
    client.list_available_triggers(toolkit, connection_id).await?.data
};

Type guard

fn is_non_empty_toolkit(s: &str) -> bool {
    !s.trim().is_empty()
}

Prevention

When it happens

Trigger: Calling list_available_triggers("", connection_id) — typically the trigger-picker UI loaded before a toolkit was selected, or a polling loop running with an unset toolkit field.

Common situations: Trigger picker screen rendered with no toolkit context; background 5 s poll started with empty state; toolkit field cleared while the picker stayed mounted.

Related errors


AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17). Data as JSON: /api/errors/0ba60604f5c982fe. Report an issue: GitHub.