tinyhumansai/openhuman · error · anyhow::Error

composio.disable_trigger: triggerId must not be empty

Error message

composio.disable_trigger: triggerId must not be empty

What it means

ComposioClient::disable_trigger rejects a triggerId that is empty after trimming, before the DELETE /agent-integrations/composio/triggers/{triggerId} URL is built. The triggerId is the instance id returned by create/enable_trigger — not the slug — and must not be blank or the DELETE hits a malformed URL.

Source

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

        }
        let mut body = json!({ "connectionId": connection_id, "slug": slug });
        if let Some(config) = trigger_config {
            body["triggerConfig"] = config;
        }
        tracing::debug!(slug = %slug, connection_id = %connection_id, "[composio] enable_trigger");
        self.inner
            .post::<ComposioEnableTriggerResponse>("/agent-integrations/composio/triggers", &body)
            .await
    }

    /// `DELETE /agent-integrations/composio/triggers/:triggerId`.
    pub async fn disable_trigger(
        &self,
        trigger_id: &str,
    ) -> Result<ComposioDisableTriggerResponse> {
        let trigger_id = trigger_id.trim();
        if trigger_id.is_empty() {
            anyhow::bail!("composio.disable_trigger: triggerId must not be empty");
        }
        tracing::debug!(trigger_id = %trigger_id, "[composio] disable_trigger");
        self.raw_delete::<ComposioDisableTriggerResponse>(&format!(
            "/agent-integrations/composio/triggers/{}",
            urlencoding::encode(trigger_id)
        ))
        .await
    }

    // ── Raw DELETE ──────────────────────────────────────────────────

    /// Perform an HTTP DELETE and parse the standard backend envelope.
    ///
    /// [`IntegrationClient`] only exposes `get` / `post` today, and the
    /// composio route actually requires a DELETE. We re-implement the
    /// envelope handling here so we don't have to widen the shared
    /// client's public surface just for one caller.
    async fn raw_delete<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T> {

View on GitHub (pinned to 7491200858)

Solutions

  1. Carry the triggerId returned by create_trigger/enable_trigger through to the disable call — do not substitute the slug
  2. Guard the toggle handler on a non-empty instance id and refresh the list when it is blank
  3. Disable the UI toggle until the row's id is populated

Example fix

// before
client.disable_trigger(row.slug.as_str()).await?;

// after
let Some(id) = row.trigger_id.as_deref().map(str::trim).filter(|s| !s.is_empty()) else {
    anyhow::bail!("row has no trigger instance id; refresh enabled triggers");
};
client.disable_trigger(id).await?;
Defensive patterns

Strategy: validation

Validate before calling

let Some(id) = trigger_id.as_deref().map(str::trim).filter(|s| !s.is_empty()) else {
    anyhow::bail!("trigger instance id is required to disable a trigger");
};
client.disable_trigger(id).await?;

Type guard

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

Prevention

When it happens

Trigger: Calling disable_trigger("") — usually a toggle fired from a row whose instance id is missing, or a caller passing the trigger slug where the instance id belongs.

Common situations: UI row keyed by slug instead of triggerId; instance id lost after re-fetching the enabled-trigger list; double-toggle racing a list refresh that cleared the row.

Related errors


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