Debugging
How to find out why a macro did not run, why it failed and what it would do, using Activity, traces, dry runs and the command line.
Keyboard Maestro: Debugger, Macro Trace, Engine Log
When a macro misbehaves, first find out whether it ran at all, then what happened at each step. Switchboard keeps both answers in the Activity window and on the command line.
Activity #
Open Activity from the View menu or with ⌘2. It lists, newest first:
every run, with its macro, what started it, when, and how long it took;
every event that fired nothing: a hotkey you pressed or an app that launched, where Switchboard knew about it and chose not to run a macro.
The segmented control at the top filters the list: All, Failed, Not Fired and By Agent (runs started by an agent over MCP). The Macro picker in the toolbar narrows it to one macro, and ⌘F searches it.
Select a run to see its trace. Right-click a row for Open in Editor, Run Again, Show Only This Macro and Copy as JSON (⌥⇧⌘C). Open in Editor is also ⌥⌘E.
Reading a trace #
A trace lists each step that ran, in order, with its status and how long it took:
| Status | Meaning |
|---|---|
| Done | The step ran. |
| Failed | The step ran and failed. |
| Skipped | A branch was not taken. |
| Simulated | A dry run described the step instead of doing it. |
| Timed out | The step took longer than its timeout. |
| Not supported | No handler exists for this step in this build. |
Each step has a note when there is something to say, such as "low confidence (78%)" or "dry run: not executed". Show in and out reveals the step's input after {{ }} expressions were filled in, and its output. Below the steps, Variables after the run lists what every variable held at the end.
Right-click a step for Open in Editor at This Step, or to copy its input or output as JSON.
The failure block #
When a run fails, the trace opens with a red block. It answers the question "what did you try?":
At is the step number and what the step does.
Looked for is what the step was trying to find, such as
role=AXButton title="Send".Found is what was actually there.
Nearest is the closest candidate and how well it scored.
Try is a suggestion in plain words.
For a missing button the block might read: Looked for role=AXButton title="Send". Nearest: AXButton "Send Later" 64%. Re-record it if the app changed. How these scores work is in Finding things on screen.
The block has Fix with AI, Open in Editor (jumps to the failing step) and Run Again.
Fixing a macro #
Fix with AI (on the failed run in Activity) sends the macro and its failed trace to Claude and shows what it would change. It is also on a failed step in the debugger and in the menu bar glyph's menu, under the failure.
Switchboard opens the macro at the failed step and shows Reading the trace… proposing a fix… for a few seconds.
The sheet shows one sentence saying what changed, such as Changed step 4 to click Send (identifier sendButton) and added a 2 s wait for it., then the changed steps, the model, and what the fix cost.
Choose:
| Button | What it does |
|---|---|
| Apply (⌘⏎) | Saves the change as one Undo, and as a version in History with the sentence as its note. |
| Dry run first | Dry-runs the proposed macro without saving it and lists each step's result under the change. Apply stays available. |
| Discard (Esc) | Leaves the macro as it was. |
Nothing changes until you press Apply. A dry run can't check elements on screen, so run the macro once for real after a locator fix. If a fix turns out wrong later, Restore the earlier version in History.
The kinds of fix it tries, and the rule each one keeps:
The element wasn't found: it points the step at one of the elements listed under Nearest, preferring one with an identifier, and keeps the old description as a fallback. It never makes up an element the trace didn't show.
The element wasn't there yet: it adds a wait before the step or raises the step's timeout, up to 10 seconds.
A value was missing: it declares an input or gives a default. It doesn't copy a value from the trace into the macro.
A menu item moved: it changes the menu path only to one the trace found.
The step fails when something is absent: it wraps the step in an If.
Some failures can't be fixed by changing the macro: a missing extension, a permission that is off, an app that isn't installed. Then the sheet says why and offers the button that helps, such as Open Permissions.
What is sent: the macro, the failed step, the three steps before it and every other step's note. Variable values and the clipboard are not sent. The toggle Include the text on the app's window (off until you turn it on) also sends the window's text, read on this Mac. Fix with AI uses the same key or relay as Make with AI, with Claude Sonnet by default, and appears only for failed runs.
Agents connected over MCP repair the same way: get_trace, then update_macro with a note that History shows.
Not fired #
To see why nothing ran, select the event in Activity, or filter by Not Fired. Each macro that could have run says why it did not:
| Reason | What it means |
|---|---|
| Group is off | The macro's group is turned off. |
| Scope | The group is not active in the app that was in front. |
| Conflict | Two macros want the same keys. Neither runs until one changes. |
| Secure input | A password field or another app has secure input on, so hotkeys and typed triggers are held back. |
| Condition false | The macro's Only if list was false. The row says which condition and the values it compared. |
| Paused | Switchboard was paused. |
| Turned off | The macro itself is disabled. |
| Late | A webhook request held while the Mac was away waited longer than the trigger's maxDelay. |
| Duplicate | A held webhook request had already been handled before the connection dropped, so it didn't run twice. |
If no macro listens for the event at all, Activity says "No enabled macro has a trigger that matches this." Check the trigger and the group's scope.
Secure input is named with a hedge, for example "usually Safari" or "a background app", because macOS credits the app in front rather than the one that really switched it on. See Troubleshooting permissions.
How long traces last #
Settings › Advanced › Traces sets how many days to keep traces (30 by default) and the most runs to keep (20,000 by default).
Dry runs #
A dry run shows what a macro would do without doing it.
Run (⌘R) in the editor does the work for real; Dry Run (⌥⌘R) types and clicks nothing.
From the command line:
switchboard run "Name" --dry-run.An agent can ask for one with
dry_run, and the iPhone's long-press menu has a Dry run item.
In a dry run, steps that only organise data really run. Everything else is simulated: it appears in the trace with the status Simulated, the input it would have used, and no side effects. The steps that run for real are if, repeat, group, variable, return, cancel, break, continue, hud, a prompt (it answers with its defaults) and a clipboard read.
Simulated steps include ones that only read, such as Read field value and Wait. A simulated step that has an into variable sets it to the step's sample value, or an empty string, so later steps still have something to work with. Conditions that look at the world (script, imageFound) answer from their sample, which defaults to true.
Safe in a dry run #
If a step is harmless and you want to see its real result in a dry run, turn on Safe in a dry run in the step's inspector. The JSON key is dryRunSafe.
- Run shell script “date +%H:%M”
- Show “It is {{ now }}”
This one runs the shell command date for real, even in a dry run, because it is marked safe.
Run again #
In Activity, Run Again (⌘R) repeats the selected run's macro. It is dry if the original was dry. The button's tooltip says which.
Step-by-step debugging #
Turn on Debug Mode (⇧⌘D, or Debug in the editor's toolbar) and the editor becomes a debugger for the open macro.
Press Run (⌘R, now Debug Run). It is a dry run unless Live is selected in the toolbar. The header under the macro's name says who started the run and where it is.
Each step shows what it did in the status column on its right: a glyph and the time. The right pane lists Variables, This Step (what went in and what came out) and the Element it looked for.
Click in the gutter beside a step to set a breakpoint (or select it and press ⌘\). The run pauses before that step. Clear All Breakpoints is in the Macro > Debug menu.
While paused: Continue (⌘⏎), Step Over (F6), Step Into (F7) to go inside a called macro, Stop (⌘.).
Run from Here starts again at the selected step with the variables as they are, so you can retry one step without redoing the ones before it.
A failed step shows the same failure block as Activity, with Re-record step to fix a missing element.
The Debugger action step lets a macro pause itself when run under the debugger.
Seeing a long run, and stopping it #
A macro that takes twenty seconds can say so. Give a Repeat a progress label and it shows "Exporting · 3 of 7" with a bar and a Cancel button. When the macro ends the same message turns into a check, or into the failure (for example "Export photos failed · Photos wasn't running") for a few seconds. Cancel stops the run, and Activity reads "Stopped from the progress HUD". A step can set the words, or the bar, by hand with a Show message step that has the key run. There is no keyboard focus on the message, so the keyboard way to stop the run is the menu bar item Stop (⌘. while the menu is open).
- Repeat 8 times, showing “Exporting”
- Wait 1 s
On the command line #
The switchboard command talks to the running engine.
switchboard run "Paste ticket as title" --dry-run --wait
switchboard trace last
switchboard trace <runId>
switchboard not-fired --since 2h
runprints the run id. With--waitit prints each step as it finishes and exits non-zero if the run failed.traceprints a run's steps and, if it failed, where and why.lastmeans the most recent run.not-firedlists events that fired nothing, each with the macros it considered and the reason.--sincetakes values like10m,2hor1d; the default is ten minutes.
If the engine is not running, the command says so and exits with status 69.
A checklist #
Open Activity (⌘2). Is there a run?
No run: filter by Not Fired. The reason is listed there. If nothing is, the trigger never saw the event: check permissions and that the engine is running.
A run that failed: read the failure block, then Open in Editor.
A run that succeeded but did the wrong thing: open the steps, show in and out, and check the inputs after expressions were filled in.
Rehearse with ⌥⌘R before changing anything for real.