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
- Send a non-empty action_name or tool_slug — ideally the exact slug returned by action='list'.
- If both keys are sent, remove the blank tool_slug: its presence overrides action_name in execute().
- Validate and trim tool arguments at the call site before invoking the tool (see defense guard).
- 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
- Never emit both tool_slug and action_name when one can be blank
- Validate LLM-produced tool calls against parameters_schema() before execute
- Prefer exact slugs captured from a prior action='list' response
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
- Missing 'app' or 'auth_config_id' for connect
- Composio v3 NLP execute failed on candidates ({v3_error_summ
- WhatsApp interactive buttons require 1..=3 options (got {});
- WhatsApp interactive list requires at least one section
- WhatsApp interactive list capped at 10 sections (got {})
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/eb674f6cb479f752.
Report an issue: GitHub.