zeroclaw-labs/zeroclaw · error

Unable to determine tool slug for '{action_name}'. Run actio

Error message

Unable to determine tool slug for '{action_name}'. Run action='list' with the relevant app first to prime the cache.{}

What it means

execute_action needs at least one v3 tool-slug candidate before calling Composio. Candidates come from the in-memory action_slug_cache (primed by action='list') plus spelling variants from build_tool_slug_candidates, which always includes the trimmed original name — so with the current code the candidate list can only be empty when the effective action name is blank (empty or whitespace-only). The optional ' (...)' suffix reports why an automatic action-list refresh failed, but listing cannot fix a blank name; the message's 'prime the cache' advice applies to slug accuracy, not to this blank-name case.

Source

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

        let mut slug_candidates = self.build_v3_slug_candidates(action_name);
        let mut prime_error = None;
        if slug_candidates.is_empty()
            && let Some(app) = app_hint.as_deref()
        {
            match self.list_actions(Some(app)).await {
                Ok(_) => {
                    slug_candidates = self.build_v3_slug_candidates(action_name);
                }
                Err(err) => {
                    prime_error = Some(format!(
                        "Failed to refresh action list for app '{app}': {err}"
                    ));
                }
            }
        }

        if slug_candidates.is_empty() {
            anyhow::bail!(
                "Unable to determine tool slug for '{action_name}'. Run action='list' with the relevant app first to prime the cache.{}",
                prime_error
                    .as_deref()
                    .map(|msg| format!(" ({msg})"))
                    .unwrap_or_default()
            );
        }

        let mut v3_errors = Vec::new();
        for slug in slug_candidates {
            self.cache_action_slug(action_name, &slug);
            match self
                .execute_action_v3(
                    &slug,
                    params.clone(),
                    text,
                    normalized_entity_id.as_deref(),
                    resolved_account_ref.as_deref(),

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Send a non-empty action_name or tool_slug — ideally the exact slug returned by action='list'.
  2. If both keys are sent, remove the blank tool_slug: its presence overrides action_name in execute().
  3. Validate and trim tool arguments at the call site before invoking the tool (see defense guard).
  4. Run action='list' with the relevant app to confirm the exact slug spelling and prime the cache.

Example fix

// before: empty tool_slug shadows a valid action_name
let args = json!({"action": "execute", "tool_slug": "", "action_name": "github-list-repositories", "params": {}});

// after: one clean, non-empty slug
let args = json!({"action": "execute", "tool_slug": "github-list-repositories", "params": {}});

// defensive: drop blank tool_slug before calling the tool
let obj = args.as_object_mut().unwrap();
if obj.get("tool_slug").and_then(|v| v.as_str()).map_or(false, |s| s.trim().is_empty()) {
    obj.remove("tool_slug");
}
Defensive patterns

Strategy: validation

Validate before calling

// Normalize execute args before calling the tool: drop a blank tool_slug,
// then require a non-blank action name.
fn normalize_execute_args(args: &mut serde_json::Value) -> anyhow::Result<()> {
    let obj = args
        .as_object_mut()
        .ok_or_else(|| anyhow::anyhow!("tool args must be an object"))?;
    let blank = |v: &serde_json::Value| v.as_str().map_or(true, |s| s.trim().is_empty());
    if obj.get("tool_slug").is_some_and(blank) {
        obj.remove("tool_slug");
    }
    let name = obj
        .get("tool_slug")
        .or_else(|| obj.get("action_name"))
        .and_then(|v| v.as_str());
    if name.map_or(true, |s| s.trim().is_empty()) {
        anyhow::bail!("execute requires a non-empty tool_slug or action_name");
    }
    Ok(())
}

Type guard

// Mirrors execute()'s precedence: tool_slug wins over action_name.
fn has_non_blank_action(args: &serde_json::Value) -> bool {
    let name = args
        .get("tool_slug")
        .or_else(|| args.get("action_name"))
        .and_then(|v| v.as_str());
    name.map(|s| !s.trim().is_empty()).unwrap_or(false)
}

Prevention

When it happens

Trigger: action='execute' where action_name is "" or all whitespace; or tool_slug present as an empty string — execute() reads tool_slug FIRST (args.get("tool_slug").or_else(|| args.get("action_name"))), so an empty tool_slug string shadows a valid action_name and reaches execute_action as a blank name, leaving zero candidates.

Common situations: An LLM emits {"action":"execute","tool_slug":""} or copies an empty optional field; upstream code defaults a missing tool_slug to "" instead of omitting the key; untrimmed user input that is only spaces.

Understand the failure class

Background: Missing required parameter errors: what 'X is required' and 'the required X param is missing' mean, and how to fix them — this error's family across 27 libraries.

Related errors


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