| name | invoke |
| description | Drive and inspect macOS app UIs from the command line with the `invoke` CLI: read accessibility trees, click buttons, simulate keys/scroll, extract on-screen data (including any website open in a browser — browsers expose the rendered page through the AX tree), and author/run "packs" (TypeScript modules exposing an app's operations as callable functions). Use whenever a task means automating, controlling, reading, or scraping a Mac app or website (Finder, Mail, Messages, Ableton, Safari, Chrome, Music, Logic…) through its UI/accessibility rather than a dedicated API — e.g. "click Play in Ableton", "how many unread emails do I have", "get the video titles from the YouTube page I have open", "automate this app's UI", "write or run an Invoke pack". Also trigger on bare mentions of the `invoke` command, packs, AX automation on macOS, or simulating keyboard/mouse input aimed at an app. Do NOT trigger for invoking a cloud function/lambda, calling a web/REST API, or a generic "invoke this method/function" in code. |
invoke CLI
invoke turns macOS application UIs into small APIs. Most desktop apps already do
useful things, but those operations are trapped behind menus, buttons, keyboard
shortcuts, and accessibility elements. invoke lets you reach in and drive them.
The whole point is GUI automation made deterministic: computer-use you can write
down. You figure out an operation once by poking at the live UI, then freeze that exact
sequence into TypeScript so it runs the same way every time, callable by anything.
That gives the core workflow, and it's two phases you move between:
- Explore / prototype (
app, element, key, scroll) — drive an app's
accessibility (AX) tree live to work out how to do something ("get me all the
iMessage threads", "open a new window"). One-off, ad-hoc. Needs nothing but the app
running and accessibility permission.
- Capture into a pack (
pack) — once it works, write it down as a pack: a
TypeScript module that names the operation as a plain function (zoomIn,
searchBrowser, bounceSelection) and hides the awkward AX/key work inside. Now any
caller — CLI, keyboard shortcut, MIDI, AI client — can invoke it deterministically.
In practice a user explores something one-off, then says "now put that into a pack" /
"memorize this workflow". That second step — turning the figured-out exploration into
durable, reusable TypeScript — is where Invoke's value lives. Treat "make this
repeatable / save this as a pack" as the natural follow-up to any exploration.
Before anything
- This is macOS only. Every command works against a running app addressed by its
bundle ID (
com.apple.finder, com.ableton.live), not its display name. Get
bundle IDs with invoke app list.
- The controlling process needs Accessibility permission (System Settings →
Privacy & Security → Accessibility). If reads return empty or you get a permission
error, that's the cause.
- Output is NDJSON — one compact JSON line per command on stdout. Pipe it through
jq for readability. Errors go to stderr as error: CodeName: detail with a
nonzero exit.
The query language (the thing to understand first)
Every element and app get command takes a bundle ID and a query path: a JSON
array that walks down the AX tree. Each array element is one step — an object whose
keys are attributes and whose values are patterns the element must match. Multiple keys
in one step are ANDed against a single element.
invoke element get com.apple.finder '[{"role": "menuBar"}, {"title": "Edit"}]'
Matching rules — these are easy to get wrong:
- Each step matches a direct child, just like CSS
>. [{toolbar}, {textField}] matches a textField only if it's an immediate child of the toolbar; if it's toolbar → group → textField the query fails ("walked 1/2 steps, no child matched") — it never looks inside group. You must spell every level: [{toolbar}, {group}, {textField}]. (To match a step by a stable child instead of pinning its exact position, use has: — see below.)
- No backtracking across ambiguous steps. When a step matches the first of several candidate siblings and a later step then fails under it, the engine gives up — it does not rewind to try the other siblings. So
[{table}, {row}, {cell}] binds row to the first row and fails if that row lacks the cell, even when another row would match. Push the disambiguator up to the ambiguous step (e.g. {role:"row", has:{…}}) so the right sibling is chosen in the first place.
- String values are globs:
* = any sequence, ? = any single char.
{"identifier": "TrackView.Device*"} matches a prefix.
role and subrole are literals, not globs, and use camelCase names
(window, menuBar, popUpButton, standardWindow) — they're normalized to AX
constants internally. See references/vocabulary.md for the full list. A role outside
the camelCase vocabulary (web-only ones like AXWebArea, which walk prints as
{"literal": "AXWebArea"}) still matches if you pass the raw AX string:
{"role": "AXWebArea"} works (proven anchoring a Safari query on it to read url).
- An empty path
[] (the default for walk) means the app's root element.
- Queries are matched against the tree as it is right now; if multiple elements match a step, the first in tree order is used — make queries specific (add
title/identifier/subrole) when it matters.
Explore first, then act
Don't guess at queries — look. The reliable loop is list → walk → narrow → act:
invoke app list
invoke app get com.apple.finder
invoke element walk com.apple.finder '[{"role": "menuBar"}]' -d 1
invoke element actions com.apple.finder '[{"role": "menuBar"}, {"title": "File"}]'
walk prints a tree of the basic attributes (id, title, role, subrole, value, …),
recursing -d/--depth levels (default 3). By default it omits empty/default-valued
attributes to cut noise; pass --full to keep them. When recursion stops at a node
that still has children, it reports "children": <count> so you don't mistake a
truncated branch for a leaf — walk deeper there if you need it.
get with no attribute names returns every present attribute; with names
(... title value) it returns exactly those (keeping nulls, so you can tell an
attribute exists but is unset).
Two force-multipliers for the explore phase:
- Mine the menu bar first. An app's menus enumerate without opening them — titles, enabled state, keyboard shortcuts — and menu items can be pressed even in the background. It's the app's complete command surface in one dump; check it before hunting the window tree. Recipe in
references/native-apps.md.
- Locating in a deep tree: dump once and search offline —
invoke element walk com.app.id -d 25 > /tmp/tree.json, then jq 'paths(. == "Seek slider")' /tmp/tree.json. Caveat: that gives a positional path, and the query language has no index step — you still need to find attribute anchors along the path to express it as a query. Use the dump to learn where the element lives and what's matchable around it, not as an address.
Know the app's archetype
What works in one app fails in another for framework reasons, not app reasons. After your first shallow walk, identify what you're driving and read the matching reference:
- A browser, or an app built on web tech (Electron/CEF — Spotify, VS Code, …):
references/web.md. Critical there: several engines need a wake-up poke before they expose anything, and value writes into web content silently no-op.
- A native app (SwiftUI, Catalyst, terminals, PDFs, hybrid native/web, menu-bar apps):
references/native-apps.md. Critical there: SwiftUI roles lie (buttons with no actions — check element actions before trusting role), and an empty-looking or erroring tree often means "wake the app" rather than "nothing there".
If every app suddenly has no windows and queries that worked stop matching (a self-referential app element returning itself as its own window, {"role":"window"} matching nothing, osascript activation hanging), the screen is locked or asleep — a degenerate tree, not an app bug. A screenshot coming back all-black confirms it. Don't diagnose the app; wait for an unlocked session and the trees come back.
Write queries that don't break
This is the heart of using Invoke well. A query is like a CSS selector: it can be
ambiguous, and a query that happens to work right now but breaks the moment the app's
state shifts is a latent bug. Invoke's whole value is determinism — GUI automation
you can rely on — so spend the exploration time to build a query that keeps matching the
intended element across the app's normal states. The explore phase isn't a formality;
it's where you learn how the app is structured so your query survives.
What makes a query fragile, and the robust alternative:
-
Positional / structural paths break. toolbar → group → button (grab "the button
in the group") matches whatever happens to be there; add a button, reorder, and you're
pressing the wrong thing. Prefer a stable identifier (or a distinctive
description/title) on the element itself: {role: "button", description: "Share"}.
Use walk to find the most stable distinguishing attribute, then match on that.
-
View-/mode-specific containers break when the view changes. The classic trap:
reading Finder's files by targeting the icon grid ({identifier: "IconView"}). The
moment the user (or another agent) switches that window to list or column view, the
container is a different role entirely (an outline, or a browser) and your query
returns nothing — and you'll wrongly conclude the items don't exist. If an operation
must work across modes, explore each mode and either match on something common or
branch per view. Ask "what does this look like in the app's other states?" before
committing.
-
Ambiguous role matches grab the wrong element. {role: "outline"} in a Finder
window matches the sidebar, not the file list — both are outlines. {role: "window"} is ambiguous whenever more than one window is open. Narrow with a
distinguishing attribute (identifier, subrole, a has: on a stable child), or you
get a random match among the candidates.
-
Localized titles break across languages/layouts. Menu titles, button titles, and
many descriptions are localized. For anything that might run on another machine, prefer
a stable identifier and reserve title-matching for when it's the only handle (and say
so). has: {identifier: "…"} lets you find a container by a stable child even when the
container's own title is localized.
-
Elements can live in a window you didn't check. Some apps spread content across
windows, and the layout isn't fixed. Ableton is the canonical case: it has an
Arrangement view and a Session view, and opening a second window (Cmd+Shift+W) puts the
opposite view in it — so an element you want may be in the first window or the
second depending on the user's layout. Grabbing "the first window" and giving up when
the element isn't there is a real, common bug: the element exists, just elsewhere. When
an element isn't found where you expect, enumerate the app's windows and search
each before concluding it's absent. (See the getinvoke.com/abletonlive pack's
cross-window resolver for the pattern.)
The test for a good query: would this still match the right element after the user
switches views, opens another window, reorders a toolbar, or changes language? If not,
keep exploring for a more stable anchor.
Reading data from the UI
Browsers expose the rendered web page through the AX tree, so you can read and drive any website — and big lists/tables need care, since a naive read of a huge list can hang. Both patterns (scraping a web page without drowning in noise, and reading a virtualized list via visibleRows) are in references/reading.md — read it when pulling on-screen data.
Waiting for the UI to change
Most UI updates are asynchronous: after an action (selecting a row, navigating a page), the content you want often renders a moment later. Prefer waiting for an AX notification — apps post one (e.g. loadComplete) when the change finishes — over a loop that re-reads or sleeps until it appears; such a loop is slow and can read half-drawn UI. (In a pack you subscribe with .on(name, cb); from the one-shot CLI you can't hold a subscription, so there you re-run by hand.) The mechanics — a waitFor sketch, the notification names, the stale-event caveat — are in references/pack-api.md.
Invoke is one tool among many
Reach for invoke when the job is driving or reading a live app's UI — clicking a
control, reading on-screen state, automating a GUI workflow that has no clean API. It's a
first-class option for that, not a replacement for everything else: when a task is
genuinely better served by a file, an API, or a database the user is fine with, use that.
Having this skill installed shouldn't crowd out tools you'd otherwise reach for.
That said, the UI usually exposes more than it first appears — a count is often a badge,
a window title, or a status string sitting right in the tree — so when UI is the right
approach (or the user asked to do it with invoke), explore before concluding it can't be
done.
Acting on elements: get / set / perform, and piping
invoke element get com.app.id '[{"role": "slider"}]' value
invoke element set com.app.id '[{"role": "textField"}]' value "hi"
invoke element perform com.app.id '[{"role": "button", "title": "Play"}]' press
Values are typed. Bare true/false and numbers are sent as bool/number; JSON objects
map to structured attribute values by their shape:
| Shape | Attribute |
|---|
{"x","y"} | position |
{"width","height"} | size |
{"x","y","width","height"} | frame |
{"location","length"} | selectedTextRange |
{"red","green","blue","alpha"} | color |
These are the same shapes get emits, so get \| set round-trips. Types JSON can't
express come #-tagged — pass the tagged form back to set to write the real type (a
bare string would set a plain string):
{"#date": "2026-06-12T09:30:00Z"}
{"#url": "https://…"}
{"#data": "<base64>"}
invoke element get com.app.id '[{"role": "window"}]' position size
invoke element set com.app.id '[{"role": "window"}]' position '{"x": 400, "y": 140}'
invoke element set com.app.id '[... textArea]' selectedTextRange '{"location": 0, "length": 5}'
set and perform print a descriptor ({"a": "<app>", "q": "<query>"}) instead
of a value — a handle to the element you just acted on. Pipe it into the next command
to keep operating on the same element without repeating the bundle ID and query:
invoke element perform com.app.id '[{"role": "button"}]' press | invoke element get value
invoke element set com.app.id '[{"role": "stepper"}]' value "0" | invoke element perform increment
When stdin is a pipe carrying a descriptor, omit the bundle ID and query — the piped
descriptor supplies them and any positional args become the attribute/action.
perform only allows actions the element actually offers; ask first with
element actions. Common actions: press, increment, decrement, showMenu,
pick, confirm, cancel, raise (full list in references/vocabulary.md).
AX manipulation vs. simulated input — prefer AX
key and scroll simulate raw HID events:
invoke key press cmd+shift+e --app com.app.id
invoke key down cmd --app com.app.id
invoke scroll y -3 --app com.app.id
With --app, the event is delivered to that app's process; without it, it's a
system-wide HID event. Combos use +: modifiers are cmd/command, ctrl/control,
opt/option/alt, shift.
Treat key as a last resort — manipulate elements (perform/set) first.
Simulated keys are the fragile path on every axis that matters:
- They need the app focused — background automation breaks, and foreground use
steals focus from whatever the user is doing mid-run.
- Shortcuts are localized and layout-remapped —
cmd+z isn't undo on every
keyboard, menu shortcuts differ per language. A press on the right element is
exact, works in the background, and is locale-independent.
- A key press is aimed at whatever happens to have focus — if focus isn't where
you assumed, you just typed into the wrong place. An element action can't miss.
Before reaching for a shortcut, check the menu bar: nearly every shortcut has a
menu item, and pressing a menu item via AX works even while the app is backgrounded
(see references/native-apps.md for menu mining). Legitimate key/scroll uses are
the cases with no AX surface at all: canvas interactions, typing into terminals,
app shortcuts with no menu equivalent.
Packs: capturing operations as deterministic functions
Once you've worked out an operation in the explore phase, capture it. A pack is a
TypeScript module (index.ts) under a publisher domain that imports the invoke
runtime, names operations as exported async functions, and uses the same query
language you just prototyped — but as a fluent API (.$(...), .press()) instead of
JSON strings on the command line. The CLI exploration and the pack code map almost
one-to-one, so porting a working CLI sequence into a pack function is mechanical.
Packs run inside the orchestrator daemon, which is normally already running
(managed by the Invoke app or a one-time setup the user did). You don't start or
manage it — just use pack commands. pack run and pack list <name> auto-mount the
pack first (idempotent; prints mounting… to stderr only when it actually mounts).
If a pack command fails to reach the daemon (a connection/socket error), stop and
tell the user — e.g. "the Invoke orchestrator doesn't seem to be running; start
Invoke (or run invoke service start) and I'll retry." Do not try to repair it
yourself: invoke service install/start and any launchctl commands are one-time
user setup that rewrites the user's login agent — never an agent's recovery step.
Note that invoke service status reports only the launchd agent, so it can say
"not running" even when packs work fine (the daemon may be hosted another way); don't
act on that status — just try a pack command and surface a real connection error if one
occurs.
invoke pack init mypack -n "My App"
invoke pack list
invoke pack list --full
invoke pack list mypack
invoke pack run mypack doThing
invoke pack run mypack doThing '"some json payload"'
invoke pack path mypack
invoke pack remount mypack
invoke logs mypack
Shorthand: invoke <pack> lists its functions, invoke <pack> <fn> [payload] runs one
— pack run/pack list are just the explicit spellings.
Writing a pack
The capture loop: pack init → fill in index.ts → pack run to verify it actually
works end-to-end. Edits are picked up automatically — the next pack run remounts a
changed pack before running. Don't consider a pack done until you've
run its function and seen the operation happen — determinism is the whole value, so
prove it.
Three authoritative references — lean on these instead of guessing the API:
- The seed
pack init writes — a heavily-commented index.ts showing the common
API (app(), .$({...}) with Role/Subrole, .press(), .key.press(), the
menubar() helper, Vars). Read it first.
references/pack-api.md (this skill) — the distilled, verified API: querying with
.$(...), the has: filter (match a parent by a child, like CSS :has()), element actions, reading/writing attributes,
events, and Vars.
node_modules/invoke/invoke.d.ts at the packs root (what import … from "invoke"
resolves to) — the full, version-exact type defs for anything beyond the above. Also
read an installed pack like getinvoke.com/abletonlive for real patterns.
The shape mirrors the CLI exactly:
import { app, menubar, Role, Subrole, Vars } from "invoke";
export const finder = await app("com.apple.finder");
const button = finder.$({ role: Role.BUTTON, identifier: "TrackView.Device*" });
export async function doThing() {
await button.press();
await finder.key.press("cmd+n");
}
Design packs as the app's operations, not caller mechanics: waveformZoom(amount),
not up/down/delta. The function names the user-level concept; the ugly AX/HID work
hides inside. Vars are named booleans a pack exposes (e.g. windowFocused) that a
caller can gate on — useful for context-sensitive shortcuts.
Pack gotchas
The pack runtime throws where the CLI stayed quiet — walk() rejects on no match, reading an absent attribute throws — so robust packs wrap reads in .catch(). Other traps: rows often can't be pick/press'd (select via the selected attribute), pack run double-encodes its output, packs are sandboxed (invoke sandbox log shows denials), and module state persists between calls. The full list with workarounds is in references/pack-api.md — read it before authoring.
Reference
references/vocabulary.md — the full camelCase vocabulary: roles, subroles, the
attribute names usable in queries and get/set, and the actions perform accepts.
Read it when a query isn't matching or you're unsure of an exact name.
references/reading.md — patterns for pulling on-screen data: scraping web pages in
the tree, and reading big/virtualized lists without hanging. Read it when extracting
data from a UI.
references/web.md — driving browsers and web-engine apps (Electron/CEF): waking
dormant accessibility trees, per-browser navigation, web controls that ignore writes,
engine quirks. Read it before automating anything web-rendered.
references/native-apps.md — what each native framework's tree looks like (SwiftUI,
Catalyst, terminals, PDFs, hybrids, menu-bar apps) and the menu-mining recipe. Read it
when an app's tree behaves unexpectedly or before deep exploration.
references/pack-api.md — the pack-authoring TypeScript API (the invoke module):
querying, actions, attributes, events, waiting on notifications, the pack gotchas, and
patterns proven in shipped packs. Read it before writing or editing a pack.
Error codes you'll see
NoRunningApp — bundle ID isn't running (or wrong ID — check app list).
NoElement / Walk — the query matched nothing; walk the parent to see what's
actually there, and check role/subrole spelling against the vocabulary.
ActionUnavailable — the element doesn't offer that action; the error lists what it
does offer. Run element actions first.
UnknownAttribute / BadQuery / BadFilter — malformed query JSON or a misspelled
attribute/role name.
Raw AXError numbers also surface; the ones worth knowing:
-25204 — app isn't responding to AX (usually App Nap). Not broken: activate it (open -a) and retry.
-25201 — stale element / tree mutated mid-walk (page loading, UI animating). Retry the same command.
-25205 — the element doesn't support that attribute.
-25208 — attribute not settable… except the web-engine wake-up pokes, which error this way and still work (see references/web.md).