Hmbown/CodeWhale · error · ExecError
perform_action failed
Error message
perform_action failed: ${j.code} What it means
perform_action on the win32 backend resolves a UIA element by base64-encoded tree path and invokes one of its supported patterns (Invoke, ExpandCollapse, Toggle, etc.). As with set_value, the PowerShell script catches exceptions and reports {ok:false, code:"<message>"}; the Node layer rethrows as ExecError('perform_action failed: <code>'). A specific in-script failure, 'action_not_found: <names>', is emitted when the requested action does not match any pattern the element exposes.
Solutions
- Parse the code in the message: if it starts with 'action_not_found:', retry with one of the pattern names it lists (they are the element's actually supported actions).
- Re-snapshot the accessibility tree and rebuild target.path before retrying — stale paths are the most common cause of the null-element exceptions.
- Verify the element type supports the action (buttons/links => Invoke, expanders => ExpandCollapse, checkboxes => Toggle); use coordinate clicks for patternless elements.
- If the target app is hung (timeout-style message), unblock its UI thread or restart it, then retry.
- Fall back to raw pointer events: compute the element's screen rect from the fresh snapshot and use click().
Example fix
// before
await backend.perform_action({ target, action: "expand" });
// after
try {
await backend.perform_action({ target, action: "expand" });
} catch (e) {
const m = /action_not_found: (.+)/.exec(e.message);
if (m) {
const supported = m[1].split(","); // patterns the element really has
await backend.perform_action({ target: await refresh(target), action: supported[0] });
} else throw e;
} Defensive patterns
Strategy: try-catch
Validate before calling
function actionSupported(elementSnapshot, action) {
const map = { click: "Invoke", invoke: "Invoke", expand: "ExpandCollapse", toggle: "Toggle" };
const wanted = map[String(action ?? "Invoke").toLowerCase()] ?? String(action);
return (elementSnapshot?.patterns ?? []).some((p) => p.replace(/Pattern$/, "").toLowerCase() === wanted.toLowerCase());
} Type guard
function hasElementPath(t) {
return t != null && typeof t === "object" && Array.isArray(t.path) && t.path.length > 0 &&
!("app_ref" in t) && !("windowIndex" in t) && !("window_id" in t);
} Try / catch
try {
await backend.perform_action({ target, action });
} catch (e) {
const notFound = /action_not_found: (.+)/.exec(e.message);
if (notFound) {
const supported = notFound[1].split(",").map((s) => s.trim());
return backend.perform_action({ target: await refreshTarget(target), action: supported[0] });
}
if (e instanceof ExecError && e.message.startsWith("perform_action failed:")) {
return clickElementByRect(await refreshTarget(target)); // pointer fallback
}
throw e;
} Prevention
- Match the action to an element pattern that exists in the snapshot (Invoke for buttons, ExpandCollapse for expanders, Toggle for checkboxes).
- Refresh target.path right before perform_action; stale paths are the dominant failure mode.
- Use coordinate clicks for patternless elements (text, containers).
- A hang/timeout usually means the target app's UI thread is blocked — fix the app, not the call.
When it happens
Trigger: Calling perform_action({ target, action }) when: the requested action maps to a pattern the element does not implement (script returns action_not_found plus the list of available pattern names), the element path is stale so $cur is null, GetCurrentPattern returns null and the pattern method is invoked on null, or the UIA call itself throws (element vanished, COM failure, app busy).
Common situations: Requesting action:'click' on an element with no Invoke pattern (static text, containers); acting on an element snapshot taken before a UI change (menu closed, item re-indexed so the path points at the wrong node); 45s timeout because the target app's UI thread is blocked; invoking ExpandCollapse on an already-toggled control that throws.
Related errors
- set_value failed
- select_text is not implemented on the win32 backend yet —…
- set_value failed
- application not found or name is ambiguous in the AT-SPI…
- application window not found in UIA tree — pass…
AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15).
Data as JSON: /api/errors/d2d4e7a444f135aa.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/plugins/computer-use/src/backends/win32.mjs:519
foreach ($t in $targets) { $cur = $t; break }
foreach ($i in $indices) {
if ($i -eq 0) { continue }
$kids = $cur.FindAll([System.Windows.Automation.TreeScope]::Children, [System.Windows.Automation.Condition]::TrueCondition);
if ($i -ge $kids.Count) { Write-Output '{"ok": false, "code": "element_stale"}'; exit 0 }
$cur = $kids[$i];
}
$want = '${act}';
try {
$pats = $cur.GetSupportedPatterns();
$names = @($pats | ForEach-Object { $_.ProgrammaticName -replace 'Pattern\$','' -replace 'Pattern$','' });
$chosen = $names | Where-Object { $_ -ieq $want } | Select-Object -First 1;
if (-not $chosen -and ($want -ieq 'click' -or $want -ieq 'invoke')) { $chosen = 'Invoke' }
if (-not $chosen) { Write-Output ('{"ok": false, "code": "action_not_found: " + ($names -join ",") }'); exit 0 }
$pt = $cur.GetCurrentPattern($pats | Where-Object { ($_.ProgrammaticName -replace 'Pattern$','') -ieq $chosen } | Select-Object -First 1);
if ($chosen -eq 'Invoke') { $pt.Invoke() } elseif ($chosen -eq 'ExpandCollapse') { $pt.Expand() } elseif ($chosen -eq 'Toggle') { $pt.Toggle() } else { $pt.Invoke() }
Write-Output '{"ok": true, "sent": true}';
} catch { Write-Output ('{"ok": false, "code": "' + $_.Exception.Message.Replace('"','') + '"}') }`, { timeoutMs: 45_000 });
if (!j.ok) throw new ExecError(`perform_action failed: ${j.code}`);
return { action_sent: true, strategy: "a11y", action };
},
read_clipboard: async () => {
const j = await psJson(`$t = Get-Clipboard -Raw -ErrorAction SilentlyContinue;
Write-Output ('{"text": ' + ($t | ConvertTo-Json -Compress) + '}');`, { timeoutMs: 10_000 });
return { text: j.text ?? "", encoding: "utf8" };
},
write_clipboard: async ({ text }) => {
const b64 = Buffer.from(String(text ?? ""), "utf16le").toString("base64");
await psOk(`Set-Clipboard -Value ([System.Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('${b64}')));
Write-Output '{"ok": true}';`, { timeoutMs: 10_000 });
return { written: String(text ?? "").length };
},
cursor_position: async () => {
const j = await psJson(`${USER32_PRELUDE}
$p = New-Object User32+POINT;
[void][User32]::GetCursorPos([ref]$p);
Write-Output ('{"x": ' + $p.X + ', "y": ' + $p.Y + '}');`);View on GitHub (pinned to 433685b202)