Demo: Scripting nested actions

Scripting nested actions is tricky.

A recent thread full of helpful and very kind posts resulted in a pair of handlers for for making the task easier.

Link

[Feature Request] To include the Macro Group of the target macro besides its name, in the "Execute Macro" macro - #14 by tiffle

The macro at the bottom of the post demos primarily the handler that the flattens actions and secondarily the helper handler that queries the flat actions.

The main macro enables the user to change the colors of actions and groups of existing colored actions of the currently selected macro.

Changing actions colors a few at a time is likely what works for most people.

This macro may be helpful occasionally for changing colors on multiple actions on different levels, or according to kind, or on groups of colored actions.

Its main methods could be adapted to go to any action (not just top-level actions) from a prompt list or query/set other action properties such and note, xml or name.

Usage:

  1. Enable the macro group and the macro.

  2. Run the macro from a trigger of your choosing.

  3. From the prompt list that appears, select one or more of the actions and existing groups of colored actions of the currently selected macro.

  4. Choose a color to apply to the selection from a macro palette of available colors.

  5. The macro will change the items selected to the color choice.

A group of existing colored actions looks like this: Green (2). When selected, it adds the 2 green actions to the items selected.

Macro Screenshot

Configuration and optimization

For better performance, suspend Edit mode.

Watching KM show the live AppleScript updates is fun, but asks too much of a UI meant for hand editing when the number of a macros's actions runs into the hundreds.

In such cases, turning off the Edit mode can speed up processing.

Similarly loading actions into the prompt can take time.

There are 3 settings related to performance and processing feedback:

  1. localSuspendEditModeWhenMacroActionsExceed = 100

By setting the value of this variable, you can tell the macro to toggle Edit mode off during updates and on again when the loaded number of a macro's actions exceed a specific number. It is preset to 100.

The prompt title will show the total number of actions in parentheses, followed by the name of the macro being edited.

  1. localShouldDisplayProgressBar = true

By default a progress window shows the number of updates as they happen. You can turn this off by setting the value of this variable to false.

  1. localEditLimitWarning = 100
    By default a display dialog warns when over 100 actions will be colored. Set the value of localEditLimitWarning to a higher number to keep it from displaying.

:eight_spoked_asterisk: Escape cancels the macro at any time.

Actions already colored will remain colored. Those not colored will be unaffected by the cancel.

:eight_spoked_asterisk: To undo changes, press command + z

Explanation

Scripting nested actions is tricky.

The AppleScript, "actions of macros", retrieves only the top level actions.

One strategy to access all actions is to flatten the top level actions.

The first "Execute an AppleScript" demos use of a handler that flattens top level actions to fill a prompt with list with the names of all actions.

To filter the flat actions, a simple but slower, AppleScript repeat loop could have been used.

Instead, the second "Execute an AppleScript" demos use of a filter-by-predicate to tweak performance.

Its syntax is verbose and fiddly-looking:

plistObject's filteredArrayUsingPr`edicate:(NSPredicate's predicateWithFormat:"ActionUID ==" & quoted form of varActionID

That is too bad.

It is actually the same thing as the AppleScript plural object filter.

For Example:

"actions whose name is ..."

However, it can filter in ways impossible in AppleScript:

actions whose id is IN {"15927027","15452518","15927030"}-->won't compile

Plus, the predicate filter method is available on ANY array of actions (action plist).

Plus, regex comparisons work...

These 2 methods are most useful for searching property lists with shallow nesting, such as flat lists of macros or actions.

  • actionDicts's valueForKeyPath:name == "name of actions"
  • actionDict's filteredArrayUsingPredicate == "actions whose..."

Because KM uses property listβ„’ xml to represent groups, macros and and actions as data, predicate/plist filtering, verbosity aside, is especially well-suited to searching these KM objects.

"Key-value coding is a fundamental concept that underlies many other Cocoa technologies, such as key-value observing, Cocoa bindings, Core Data, and AppleScript-ability (emphasis mine). Key-value coding can also help to simplify your code in some cases."

Key-Value Coding Programming Guide: About Key-Value Coding

Color Multiple Actions Macros.kmmacros (65.9 KB)
Color Multiple Actions + Action Kind Groups Macros.kmmacros (68.1 KB)

2 Likes

This is awesome and will save a lot of time in coloring action types to a specific color. Then I can change as needed. Thank you for sharing this and I look forward to learning from it and implementing it.

Thank you for all the detail in your explanation as well.

1 Like

Thank you for trying it out! I am thrilled that it may of use.

Ask anything. :slightly_smiling_face:

It took me a second to figure out that there are settings for the width and that you had it set to Narrow instead of Automatic.

Things were getting truncated for me.
image

At some future date I might use what you have here to set colors for macros based on their action type.

Hi Skillet,

Thanks for noticing the narrow width setting. I forgot to change it back to normal width after screenshotting it.

I've changed the upload so that the "Prompt with List" displays at normal width.

The modified macro with entries of groups of types of actions is posted below the original macro.

Never mind the blather below.

Hidden

This is a modification that fits well with the filtering method and the keys of the action xml.

Swapping a "ActionColor =='Green'" filter for an "MacroActionType =='ExecuteMacro'" should be straightforward, if verbose :see_no_evil:.

Another example : "MacroActionType =='IfThenElse'"

I'll get an example out at some point of how to get a list of your MacroActionType keys.

There are a lot of actions so the list will be pretty huge.

Then, put out an example of how to use (some of) them with the "Prompt with List".

For now, the next best thing will be to type in the "Prompt with List" the key words of the default names of the actions.

Predicate filtering on MacroActionType

Details

In AppleScript, we filter the "plural actions" like this:

set greenList to actions of macro "my macro" whose color is "Green"

We collect values of a property of "plural actions" like this:

set actionIdList to id of actions of macro "my macro"

In xml the "ActionColor" key equals the "color" property.
In xml the "ActionUID" key equals the "id" property.

So here are the equivalent statements using a predicate filter and collection query:

Filter:
set greenArray to actionsArray's filteredArrayUsingPredicate:(current application's NSPredicate's predicateWithFormat:"ActionColor =='Green'")

Collect:
set greenIdsArray to greenActionArray's valueForKeyPath:"ActionUID"

instead of "ActionColor", we can query on the "MacroActionType" key.

Filter:
set IfThenElseArray to actionsArray's filteredArrayUsingPredicate:(current application's NSPredicate's predicateWithFormat:"MacroActionType =='IfThenElse'")

Collect:
set IfThenElsIdsArray to IfThenElseArray's valueForKeyPath:"ActionUID"

If you squint, you might be able to see in this verbosity the original AppleScript filter. :exploding_head:

Like this kind of make sense, right?

"Colorless green ideas sleep furiously"
--Noam Chomsky

:grimacing::roll_eyes:

You can lead a horse to water...

I will work on tweaking this so it just always colors all of the actions to my default colors and then I can change anything as needed like red if I need to work on change the color of just one action using Jim's macros.

It'll take me some time to process it all but I will get there. Thank you for the explanation of how it works.

Color Coding

Keyboard Maestro Action Color Coding

:heartpulse: Magenta – Flow Interruption
Used when macro execution pauses or awaits user input, stopping normal flow. Actions:
β€’ Pause
β€’ Pause Until
β€’ Prompt for User Input
β€’ Display Dialog
β€’ Display Alert
β€’ Prompt With List Rationale: Magenta = intentional halt in automation for user interaction or timing.

:yellow_circle: Yellow – Flow Control
Used for logic branching, macro structure, and conditional flow. Actions:
β€’ If Then Else
β€’ Switch/Case
β€’ While
β€’ Repeat
β€’ For Each
β€’ Break From Loop
β€’ Continue Loop
β€’ Cancel Macro
β€’ Return from Macro Rationale: Yellow = forks in logic, conditionals, loop controllers.

:green_circle: Green – Flow Continuation
Used for UI automation or direct progression through tasks. Actions:
β€’ Type Text
β€’ Type Keystroke
β€’ Press Button
β€’ Click at Found Image
β€’ Move and Click
β€’ Scroll Wheel
β€’ Select or Choose Menu Item
β€’ Drag
β€’ Insert Text by Pasting Rationale: Green = linear steps with no interruptions or logic changes.

:purple_circle: Purple – Data Management
Used for transforming, storing, and managing variables and data structures. Actions:
β€’ Set Variable to Text
β€’ Set Variable to Calculation
β€’ Set Variable to JSON
β€’ Set Variable to Clipboard
β€’ Delete Variable
β€’ Filter Variable
β€’ Process Tokens in Text
β€’ Search and Replace
β€’ Dictionary Actions (Set/Get/Delete)
β€’ Math Functions Rationale: Purple = underlying data logic and transformations.

:large_blue_circle: Aqua – Organizational Structure
Used to visually organize and group macro logic for readability. Actions:
β€’ Group (action container for organizing)
β€’ Submacro Rationale: Aqua = structural layout and readability (not execution flow).

:blue_square: Teal – Documentation & Annotation
Used strictly for internal communication within the macro. Actions:
β€’ Comment
β€’ Notification (used as inline reminders or flags)
β€’ Display Text (when purely for developer reference) Rationale: Teal = non-functional info to help the macro builder.


The following two colors Red and Orange I would manually change on my own as needed after standard color coding has been applied.

Change Manually or Individually

:red_circle: Red – Work in Progress / Needs Attention
Used to mark incomplete, buggy, experimental, or temporary actions. :white_check_mark: You can apply this to any action needing review or in active development. Examples:
β€’ Temporary Set Variable
β€’ Placeholder Execute Macro
β€’ Incomplete logic sections Rationale: Red = needs attention, debugging, or validation.

:orange_circle: Orange – User-Configurable Elements
Used for actions likely to require user edits or maintenance over time. Actions:
β€’ Execute Macro (if macro name could change)
β€’ Set Variable (user-editable content)
β€’ Open URL
β€’ Find Image
β€’ Type Text (customizable content)
β€’ Create New Folder (in Finder – user-defined paths) Rationale: Orange = things the user may modify often.

Okay this is beyond me, I have been messing with this for a little bit and haven't figured it out. If you have time could you possibly get me started by how I would modify this macro to just start color coding specific macro actions to my default color scheme (no prompt just color the selected macro with the defaults I listed.)?

This might be a massive undertaking to have to pull out each action type I have listed and not really feasible.

I think it is really quite amazing that you have made it so it shows them all in the same group with numbers for the macros that are the same type.

Hi Skillet,

I didn't expect this kind of interest. :smiley:

I am thrilled that the handlers and explanations have made enough sense for you to keep reading and thinking on this.

The handlers push up against the boundaries of KM's intended use, automating other things. It is a niche goal with a fair amount of--what shall we say?--assembly required. :construction:

I need some time to mull over--what shall we call it?--user defined color categories? But at a glance it looks entirely possible--famous last words. :zipper_mouth_face:

So, tentatively, I've made of project folder named "Color by user defined color categories"

Thank you for the clear outline. It will help a ton.

Medium-tedium to small, but not as massive as you might imagine.

As you have probably noticed by now even though the value of the MacroActionType key in action xml is different it still resembles the action name pretty well.

That is what you are seeing in the second macro's action type groups.

Just for fun, here is an AppleScript macro that reads all the unique values of MacroActionTypes in all macros in the Keyboard Maestro Macros.plist file and displays them in a window.

The plist deserializes to a dictionary.

My personal favorite collection query,valueForKeyPath, (like "name of actions" in AppleScript) drills down to the actions with this incantation:


set actionDicts to kmPlistDict's valueForKeyPath:"MacroGroups.@unionOfArrays.Macros.@unionOfArrays.Actions"
Link

Key-Value Coding Programming Guide: Using Collection Operators

A conversion back to xml tees up an xpath to jump straight to the MacroActionTypes:

"//dict[key='ActionUID']/key[.='MacroActionType']/following-sibling::string[1]/text()"

Not to downplay the fair amount of teeing up the data for the queries, that really is all there is at the core of it.

(A cheat sheet (NSpredicate + cheatsheet websearch search word) I mentioned earlier lists collection queries nicely.)

Anyway...let's give it a go. (Caveat: My speed these days: :construction::turtle:)

AppleScript

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

set fn to "Keyboard Maestro Macros.plist"
set posixPath to ((current application's NSString's stringWithString:(POSIX path of (path to application support from user domain)))'s stringByAppendingPathComponent:"Keyboard Maestro")'s stringByAppendingPathComponent:fn
--Deserialize to an dictionary
set kmPlistDict to current application's NSPropertyListSerialization's propertyListWithData:(current application's NSData's dataWithContentsOfFile:posixPath) options:0 format:(missing value) |error|:(missing value)
if kmPlistDict is missing value then error (theError's localizedDescription() as text) number -10000
set actionDicts to kmPlistDict's valueForKeyPath:"MacroGroups.@unionOfArrays.Macros.@unionOfArrays.Actions"

set {theData, theError} to current application's NSPropertyListSerialization's dataWithPropertyList:actionDicts format:(current application's NSPropertyListXMLFormat_v1_0) options:0 |error|:(reference) -- don't use binary format
if theData is missing value then error (theError's localizedDescription() as text) number -10000
set {doc, theError} to current application's NSXMLDocument's alloc()'s initWithData:theData options:0 |error|:(reference)
if doc = missing value then error (theError's localizedDescription() as text)
set flatActionsAll to doc's nodesForXPath:"//dict[key='ActionUID']/key[.='MacroActionType']/following-sibling::string[1]/text()" |error|:(missing value)
set macroActionTypesUsed to (flatActionsAll's valueForKeyPath:"stringValue")
set macroActionTypesUsed to (current application's NSSet's setWithArray:macroActionTypesUsed)'s allObjects()
set macroActionTypesUsed to (macroActionTypesUsed's sortedArrayUsingSelector:"localizedStandardCompare:") as list
set {TID, text item delimiters} to {text item delimiters, linefeed}
return macroActionTypesUsed as text
ScreenShot

List values of all MacroActionTypes in Keyboard Maestro Macros_plist.kmmacros (3.5 KB)

Hi Skillet,

Here is macro that shows what I propose using for a WYSIWYG way of setting color schemes.

You would simply enter the actions you want to color into in a "color schemes" macro and then color them accordingly.

The colorize script would get its coloring instructions from macro's colored actions's xml.

For fun and instruction, this macro reports the MacroActionTypes actions of the selected macro by their colors.

Screenshot

Knowing how the filtering works will help better specify what actions get colored and how.

The sample actions in the macro roughly corresponds to the categories you uploaded.

How to read the report

The lines with a single entry shows an action's main type.

The lines with a comma show up on actions that have options that make the action perform quite differently despite it having the same main type. For now, I'm calling it a subtype.

The first item is the main type, the second item is the "subtype"

Anyway, the results should give you a better idea of what we can filter on and therefore what we can color by scripting.

Anything with a clear type and subtype we should be able to filter and script.

There may be other ways to filter and color but this is what I have so far.

Have a play with it to get clearer idea of what to expect.

If you find other possible filter keys and patterns in the xml, please let me know.

This is what two filters might look like:

MacroActionType == 'Filter'
MacroActionType == 'Filter' AND Action == 'ProcessTokens'

We would use the first filter to color ALL filter actions.
The second if we only wanted to color filter actions that performed token processing.

There may be other ways to filter but this is what I have so far.

Have a play with it to get clearer idea of what to expect.

If you find other possible filter keys and patterns in the xml, please let me know.

MacroActionType and subtypes of Current Macro.kmmacros (20.2 KB)

Thank you very much for all your work with this. I have some more time in a couple days and will plan on working through some of this. This is pretty amazing and certainly something I would not have been able to do on my own. Thank you very much for all your time and effort in putting this together and sharing you knowledge and expertiese!

Haha, I hear you there, life gets busy.

I ran it and got an error and will have to look more into it when I have some more time.

set actionDicts to kmPlistDict's

Hi Skillet,

Sorry for the error and thank you for posting the details. It looks like you may be running an earlier upload. I think the current upload should be not contain: "set actionDicts to kmPlistDict's".

A first draft that runs is nearing completion. There is room for optimizations in a second version.

Don't worry about weeding through the last two vague posts.

The working version should make things clearer. :crossed_fingers:

I am certain I would not taken this script this far without your interest, suggestions and kind enthusiasm.

I am lookng forward to running the first version by you.

It's been such a surprising and enjoyable project! Thank you!

1 Like

Thank you, you are very kind and generous. I am stoked to try it out and no doubt will use it several times a week.

1 Like

Here is a draft of the color code actions macro. It colors actions in a selected macro according to user-defined sets of colored actions contained in a macro named "Action Color Scheme"

The main difference between this macro and first posted macro is a lack of a prompt list and filtering in the script added for type of color-coded actions.

The script handlers that flatten nested actions and filter them with a queryable index are the same.

USAGE:

  1. Assign a trigger of your choice to the "Color Code Actions" macro.

  2. Go to the "Action Color Scheme" macro.

  3. Delete/modify its sample actions.

  4. Add the kind of actions you want to color code and set them to the color you want them to have.

  5. Go to a macro you want the color settings applied to.

  6. Trigger the macro. It will change the color of the actions according to the actions in "Action Color Scheme"

  7. To undo the coloring press command + z.

Press the escape key at an time to abort the macro.

To reset the color of all actions in a macro to None, select the macro and run the macro named "Set all actions of currently selected macro to None"

Other settings

These variables allow customization.

  1. localColorSchemeMacroName, default is "Action Color Scheme".
    Specifies the name of the macro containing the color scheme.
  2. localSuspendEditModeWhenMacroActionsExceed (true/false)
    Suspends Edit mode for better performance.
  3. localShouldDisplayProgressWindow (true/false)
    Enables/disables color progress bar.
  4. localEditLimitWarning a number
    Enables/disables a dialog that allows cancelling the macro when the number of actions to color exceeds a number.
How to override the 'Action' key query

If a the xml of an action contains an "Action" key, the script will color only actions with that specific Action value.

For example, the "Action" key value of a Cancel action can be:

BreakFromLoop
ContinueLoop
CancelSpecificMacro
CancelAllMacros
CancelAllOtherMacros
CancelJustThisMacro
CancelThisMacro
RetryThisLoop

Each type of Cancel action's color must be set individually.
Each type of Cancel action can be set to a different color.

If you want override this behavior, insert only one of the Cancel actions.
Click the gear icon on the Notes menu item and type "all" (lowercase, no spaces).
Now all Cancel actions will be assigned the color of the Cancel action with the "all" in Notes

Script Explanaton

This macro's script handles nested actions in 2 main steps.

  1. Gets a flat list of actions.

  2. Queries the flat list of actions using a queryable index.

Querying the flat actions list.

A list of actions cannot use the AppleScript method of directly querying plural object.

To overcome this issue, the script uses a queryable index made from a "deserialized" flat list of actions's xml. This index is an array of dictionaries that mirrors the flat list of actions. The script indirectly queries the indices of actions by querying the corresponding dictionaries in the array of dictionaries and getting their identical positions.

Single and two-key action identification.

To query an array of dictionaries by kind, it is often enough to filter on the "MacroActionType" key in action xml.

So a query for an "If then Else" action would be: MacroActionType == 'IfThenElse'

An action that has a dropdown action option has additional an key in its xml that can be used to qualify the "MacroActionType" key--a kind of "type qualifier".

This macro's script handles only one such "type qualifier" key: "Action"

So a query for "Insert By Pasting" would look like this: MacroActionType == 'InsertByText' AND Action == 'ByPasting'"

To build queries for sets of actions, the script reads the values of these two keys in the xml of the actions in the macro named "Action Color Scheme" .

For each action in a set of colored-coded actions, it builds a series of "sub" predicates, as in the previous query statements.

It then joins them with OR into a single query.

(MacroActionType == 'IfThenElse') OR (MacroActionType == 'InsertByText' AND Action == 'ByPasting'")

This query comprises a single user-defined set of color-coded actions.

The complete set of queries comprise the user-defined color coding scheme.

Screenshots

Color Coder Macros.kmmacros (88.9 KB)
View XML of selcted actions or macro.kmmacros (12.4 KB)

1 Like

Thank you for posting this I will spend some time with this and see what I can learn. It woked when I ran it from the macro group it is in but when I renamed the macro group and ran it on another macro I got an error probably because I need to enter the details for the variables.

image

The set them all to none works perfectly. I'll see what I can figure out but just wanted to reply as soon as I could and say thank you.

You're more than welcome, thank you for trying it!!

I'm relieved to hear it worked--at least once. :wink:

(I always worry that once a macro gets off my pre-Jurassic set up it won't work)

I'm sorry that changing the macro group name seems to have caused an AppleScript error in the "Execute AppleScript" script in "Color Code Actions"

Thanks for your description of the error and the image.

The "Action Color Scheme" macro looks to be the offending component.

The "Execute AppleScript" script in "Color Code Actions" macro uses its xml as coloring instructions.

It locates this macro by using the name in "localColorSchemeMacroName" variable.

Is it somehow not finding it?

You could try hard coding the name into the script:

set kminstance to system attribute "KMINSTANCE"
tell application id "com.stairways.keyboardmaestro.engine"
	set colorSchemeMacroName to getvariable "localColorSchemeMacroName" instance kminstance
	set colorSchemeMacroName to "Action Color Scheme" -- ⬅️ ADD THIS LINE
end tell

"Action Color Scheme" is just the default name. Any valid macro name should be OK--unique is best and it should contain actions.

But changing the macro group name shouldn't cause a location issue...so maybe that's not the issue.

I can't think of any other settings that would need adjustment when the macro group name changes.

Please take your time, but ask anything.

(The script isn't as readable as I wanted it to be.)

This is insanely rad, I am so impressed! What is even cooler is if you do have another macro in there that is the same like a cancel macro you can have one be a different color and all the others follow the "all" note so you can just make one have the all with the color you want. The action at the top is the one that wins if there are two that are the same macro type and has the name all in it. Mind blown by these macros! Thank you, thank you, thank you!

How you figured out how to do this is so impressive. The chances of me coming up with something like that is infantesimally close to never.

That was competely my bad, I had changed the name which was silly since I didn't know how everything was referenced. It is now working.


Current Results Below
Red, Orange, and Aqua didn't seem to work for some reason. Orange did work once and then stopped working when I colored multiple actions that color. I thought at first you excluded Red because it was one I would do manually.

Change Manually

Here is a step by step of what I did.

  1. I copied all the default colored actions into another macro without changing anything and just ran them in your group.
  2. Created a new macro to change the colors of actions using the default actions in your "Action Color Sheme" macro and set all those to no color.
No Color

Screenshot 2025-05-07 at 6.23.33 PM

  1. Ran the "Color Code Actions" macro to change the colors and below are the results.
Results

Screenshot 2025-05-07 at 6.24.03 PM

This is what they should have been based on the colors from the settings in the "Action Color Sheme" macro.

Should Have Been

Screenshot 2025-05-07 at 6.23.41 PM

I am absolutely delighted that you've given it try and that the macro seems to run at all.

Thank you so much for your patience and your feedback! If I can match your interest, attention to the details and praise the thing will be brewing coffee soon. :sunglasses:

Bad where? This is exactly what helps. I completely neglected to include any checking whatsoever much less, user feedback. :man_facepalming:t2:

I'm looking forward to the chance to track down what are certainly other goof ups in the script.

When I get a moment to look at it more closely, I'll have a better idea of how long it might take. Hopefully, something between :snail: and :racehorse:.

Thank you again for giving me these leads to go on!

1 Like

You are extreamly kind, I try to see what I can figure out as well.

I certainly know how that goes with everything going on which is why I am so grateful for your time and effort in making this happen!

It is great already and I am enjoying the drink now!

Great news this seems to be working.

Working AppleScript
use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

--┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
--┃ Purpose: Color-code actions in a selected macro
-- |                based on user-defined list of colored actions 
--┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-- Requires input from KM variables:
--     localColorSchemeMacroName
--     localEditLimitWarning
--     localProgressUpdateWindowExists
--     localSuspendEditModeWhenMacroActionsExceed

--Sets KM variables:
--     localTotalColorCodableActions
--     localCountOfActionsColored
--     localTotalActions

--┏━━━━━━━━━━━━━━━━━━━━━━━
--┃ --Build a "Color-Action" data object:"colorByKindDicts"
--┗━━━━━━━━━━━━━━━━━━━━━━━

--This object is an array of dictionaries with the following keys: Color, TypesFilter, ActionIndices.
-- {{Color : "Purple", TypesFilter: "boolean statement"},ActionIndices:{1,3,8}}

---"TypesFilter" defines a color-coded set of actions defined by user.
-- "TypesFilter" structure:
-- The value of "TypesFilter" is a single string of boolean statements joined by "OR".

--Types of filter statements:
--1. MacroActionType (Single key identification)
--"MacroActionType IN {'SetVariableToText', 'SetVariableToCalculation'}"
--(any action matching a single key whose value is one in a list of values)
--2. MacroActionType + Action (Two  key identification)
--(MacroActionType == 'Cancel' AND Action == 'CancelAllMacros')--  (actions matching these 2 key/values pairs) 

--An example filter would be: 
--"MacroActionType IN {'SetVariableToText', 'SetVariableToCalculation'} OR (MacroActionType == 'Cancel' AND Action == 'CancelAllMacros')"



set kminstance to system attribute "KMINSTANCE"
tell application id "com.stairways.keyboardmaestro.engine"
  set colorSchemeMacroName to getvariable "localColorSchemeMacroName" instance kminstance
end tell

if colorSchemeMacroName is "" then set colorSchemeMacroName to "Action Color Scheme"

--Get the user-defined colors and actions as deserialized xml, ie, a flat array of dictionaries
tell application id "com.stairways.keyboardmaestro.editor"
  set {doc, theError} to current application's NSXMLDocument's alloc()'s initWithXMLString:(xml of macro colorSchemeMacroName) options:0 |error|:(specifier)
  if doc = missing value then error (theError's localizedDescription() as text)
end tell
set nodes to doc's nodesForXPath:"//dict[key='ActionUID']" |error|:(missing value)
set flatRulesDicts to nodes's valueForKeyPath:"XMLString.propertyList"

set colorByKindDicts to current application's NSMutableArray's array()

--For each color's associated actions build a types filter from the values of the keys "MacroActionType" and "Action" extracted from each actions's xml.
--.eg (MacroActionType == 'IfThenElse') OR (MacroActionType == 'InsertByText' AND Action == 'ByPasting'")

set schemeColors to getUniqueColorsFromFlatDicts(flatRulesDicts)

repeat with aColor in schemeColors
  set actionDictsPerColor to (actionDictsWhose("ActionColor == " & (quoted form of aColor), flatRulesDicts))
  --Build the subpredicates
  set ORSubPredicates to current application's NSMutableArray's array()
  
  --Make the "IN statement" subpredicate .eg "MacroActionType IN {"For","IfThenElse"}"
  set singleIDActionDicts to actionDictsWhose("Action == nil", actionDictsPerColor)
  set twoIDActionDictsOverRidden to actionDictsWhose("ActionNotes == 'all'", actionDictsPerColor)
  set singleIDActionDicts to (singleIDActionDicts's arrayByAddingObjectsFromArray:twoIDActionDictsOverRidden)
  
  set INArgs to (singleIDActionDicts's valueForKeyPath:"MacroActionType")
  set INArgs to listToLiteral(INArgs)
  
  -- Only add the IN predicate if we have actions that match
  if INArgs is not "{}" then
    set INPred to (current application's NSPredicate's predicateWithFormat:("MacroActionType IN" & INArgs))
    --Add the IN subpredicate
    (ORSubPredicates's addObject:INPred)
  end if
  
  --Add subpredicates that use an "Action" key as qualifier. eg "MacroActionType == 'Cancel' AND Action == 'CancelThisMacro'"
  set twoIDActionDicts to actionDictsWhose("Action != nil", actionDictsPerColor)
  if twoIDActionDicts's |count|() > 0 then
    --Extract the"MacroActionType" and "Action" values
    set tempDict to (twoIDActionDicts's dictionaryWithValuesForKeys:{"MacroActionType", "Action"}) --⬅️ add more identifying xml keys to retrieve here
    set actionTypes to (tempDict's objectForKey:"MacroActionType") as list
    set typeQualifiers to (tempDict's objectForKey:"Action") as list
    
    set i to 1 --one-based processing
    repeat with theType in actionTypes
      set statement1Pred to (current application's NSPredicate's predicateWithFormat:("MacroActionType == " & quoted form of theType))
      set statement2Pred to (current application's NSPredicate's predicateWithFormat:("Action ==  " & quoted form of (item i of typeQualifiers)))
      --"AND" the two statements together into a single predicate
      set anAndPred to (current application's NSCompoundPredicate's andPredicateWithSubpredicates:{statement1Pred, statement2Pred})
      --add the predicate to the array of subpredicates
      (ORSubPredicates's addObject:anAndPred)
      set i to i + 1
    end repeat
    --repeat ...
    -- βž• ADD ADDITIONAL TYPES OF BOOLEAN STATEMENTS πŸ”¦ HERE.
    -- end repeat
  end if
  
  -- Only add this color if we have any predicates to match
  if ORSubPredicates's |count|() > 0 then
    --" OR " the subpredicates together into a single color scheme predicate
    set filterForColorGroup to (current application's NSCompoundPredicate's orPredicateWithSubpredicates:ORSubPredicates)'s predicateFormat() as text
    -- Make a new  color and filter dictionary
    set colorSchemeFilters to (current application's NSMutableDictionary's dictionaryWithObjects:{aColor, filterForColorGroup} forKeys:{"Color", "TypesFilter"})
    --Add the new dictionary
    (colorByKindDicts's addObject:colorSchemeFilters)
  end if
end repeat


--The values for keys, "Color","TypesFilter" of colorByKindDicts is finished.
--βš™οΈβš™οΈGet the settings related to executing the scriptβš™οΈβš™οΈ
--
set kminstance to system attribute "KMINSTANCE"
tell application id "com.stairways.keyboardmaestro.engine"
  set maxEdits to getvariable "localEditLimitWarning" instance kminstance
  set shouldDisplayProgressBar to getvariable "localShouldDisplayProgressWindow" instance kminstance
  
  set suspendEditModeWhenMacroActionsExceed to getvariable "localSuspendEditModeWhenMacroActionsExceed" instance kminstance
  
  try
    if maxEdits is "" then set maxEdits to 50
    set maxEdits to maxEdits as integer
  end try
  try
    if shouldDisplayProgressBar contains "true" or shouldDisplayProgressBar contains "yes" or shouldDisplayProgressBar is "1" then
      set shouldDisplayProgressBar to true
    else
      set shouldDisplayProgressBar to false
    end if
  end try
  try
    if suspendEditModeWhenMacroActionsExceed is "" then set suspendEditModeWhenMacroActionsExceed to 50
    set suspendEditModeWhenMacroActionsExceed to suspendEditModeWhenMacroActionsExceed as integer
  on error
    set suspendEditModeWhenMacroActionsExceed to 50
  end try
  
end tell


--βš™οΈβš™οΈ Apply Settings βš™οΈβš™οΈ
-- Dynamic update step based on how many updates you want
set desiredNumberOfUpdates to 10 -- adjustable
set updateStep to round (100 / desiredNumberOfUpdates) rounding as taught in school
set nextTargetPercent to updateStep

--FLATTEN THE ACTIONS OF THE CURRENTLY SELECTED MACRO
tell application id "com.stairways.keyboardmaestro.editor" to set flatActions to my flattenActions(actions of item 1 of (selected macros as list))

--Query  indices of the actions of each set to an "ActionIndices" key.

--make the queryable flatDicts index, ie. a mirror array of dictionaries of flatActions's xml
set flatDicts to plistDicts(flatActions)

set totalIndices to 0
repeat with aDict in colorByKindDicts
  set theIndices to indexesMatchingPredicate(aDict's objectForKey:"TypesFilter", flatDicts) --Query the flatDicts using the color-grouped action-kind predicates
  (aDict's setObject:theIndices forKey:"ActionIndices")
  set totalIndices to totalIndices + (count of theIndices) --keep a count of total actions to be colored.
end repeat

--Warn that the number of  updates will exceed max limit. 
if (totalIndices) > maxEdits then
  tell application id "com.apple.systemevents" to tell (first process whose frontmost is true)
    display dialog "Change the color of " & (totalIndices) & "?"
  end tell
end if

--Turn Edit Mode off if the number of actions to color exceeds user-defined limit.

set shouldSuspendEditMode to missing value
if (count of flatActions) > suspendEditModeWhenMacroActionsExceed then
  set shouldSuspendEditMode to true
else
  set shouldSuspendEditMode to false
end if
set savedEditMode to missing value
if shouldSuspendEditMode is true then
  tell application id "com.apple.systemevents" to tell application process "Keyboard Maestro"
    tell checkbox "Edit" of window 1
      set savedEditMode to (value of it) as boolean
      if savedEditMode then click it
    end tell
  end tell
end if
--πŸŽ¨πŸ–ŒπŸŽ¨πŸ–ŒπŸŽ¨
--Apply the colors to the actions at the indices of color-grouped actions  
set i to 0
set actionIDs to {}
tell application id "com.stairways.keyboardmaestro.editor"
  repeat with aColorRule in colorByKindDicts
    
    set aColor to (aColorRule's objectForKey:"Color") as text
    set actionIndices to (aColorRule's objectForKey:"ActionIndices") as list
    
    repeat with anIndex in actionIndices
      set color of item anIndex of flatActions to aColor
      set i to i + 1
      if shouldDisplayProgressBar then
        set percentFinished to round ((i / totalIndices) * 100) rounding as taught in school
        if percentFinished β‰₯ nextTargetPercent then
          ignoring application responses
            tell application id "com.stairways.keyboardmaestro.engine" to do script my displayProgressXML(nextTargetPercent, ("Updating " & (i as text) & " of " & (totalIndices) & " actions (" & aColor) & ")")
          end ignoring
          set nextTargetPercent to nextTargetPercent + updateStep
        end if
      end if
    end repeat
  end repeat
end tell

if shouldDisplayProgressBar then
  tell application id "com.stairways.keyboardmaestro.engine" to do script my displayProgressXML(99, ((i as text) & " of " & (totalIndices) & " finished "))
  delay 0.5
  tell application id "com.stairways.keyboardmaestro.engine" to do script my displayProgressXML(100, ("Finished"))
end if
if savedEditMode is not missing value then
  if savedEditMode is true then
    tell application id "com.apple.systemevents" to tell application process "Keyboard Maestro"
      tell checkbox "Edit" of window 1 to click
    end tell
  end if
end if
tell application id "com.stairways.keyboardmaestro.engine"
  setvariable "localTotalColorCodableActions" instance kminstance to totalIndices
  setvariable "localCountOfActionsColored" instance kminstance to i
  setvariable "localTotalActions" instance kminstance to (count of flatActions)
end tell
return i

--=======================HANDLERS=====================

-- Returns actions matching the predicate directly
on actionsMatchingPredicate(predicateString, actionsList)
  -- Convert AppleScript action references into NSDictionary objects via property list deserialization
  set plistXMLList to {}
  tell application id "com.stairways.keyboardmaestro.editor"
    repeat with anAction in actionsList
      set xmlString to (xml of anAction)
      set end of plistXMLList to xmlString
    end repeat
  end tell
  set plistDicts to (current application's NSArray's arrayWithArray:plistXMLList)'s valueForKeyPath:"propertyList"
  
  set dicts to plistDicts
  set plistXMLList to {}
  
  set resultOrError to indexesMatchingPredicate(predicateString, dicts)
  if class of resultOrError is list and (count of resultOrError) > 0 then
    if item 1 of resultOrError is false then
      display dialog (item 2 of resultOrError)
      return {} -- safe fallback
    end if
  end if
  set idxList to resultOrError
  set matchedActions to {}
  repeat with k in idxList
    set end of matchedActions to item k of actionsList
  end repeat
  return matchedActions
end actionsMatchingPredicate

-- Creates a queryable index of actions
-- Helper Handler 1 : queries the index and returns the found indices
on indexesMatchingPredicate(predicateString, dictList)
  -- Takes a list of NSDictionary objects and returns 1-based indexes matching the NSPredicate
  try
    set thePredicate to current application's NSPredicate's predicateWithFormat:predicateString
  on error errMsg number errNum
    return {false, "⚠️ Predicate error: " & errMsg}
  end try
  set nsDictArray to current application's NSArray's arrayWithArray:dictList
  set filteredDicts to nsDictArray's filteredArrayUsingPredicate:thePredicate
  
  set matchIndices to {}
  -- Iterate over matched dictionaries to find their original positions
  set matchCount to filteredDicts's |count|()
  repeat with j from 0 to (matchCount - 1)
    set aDict to (filteredDicts's objectAtIndex:j)
    set zeroIdx to (nsDictArray's indexOfObjectIdenticalTo:aDict) as integer
    -- Convert to AppleScript 1-based index
    set oneBasedIdx to zeroIdx + 1
    set end of matchIndices to oneBasedIdx
  end repeat
  return matchIndices
end indexesMatchingPredicate

on displayProgressXML(thePercent, progressTitle)
  return "<dict>
  <key>MacroActionType</key>
  <string>DisplayProgress</string>
  <key>Progress</key>
  <string>" & (thePercent as text) & "</string>
  <key>Title</key>
  <string>" & progressTitle & "</string>
</dict>"
end displayProgressXML

------------------------------
-- Convenience handlers
------------------------------

on getUniqueColorsFromFlatDicts(flatDicts)
  set kindColors to flatDicts's valueForKeyPath:"ActionColor"
  set uniquedefinedColors to (current application's NSMutableOrderedSet's orderedSetWithArray:kindColors)
  uniquedefinedColors's removeObject:(current application's NSNull's |null|())
  set uniquedefinedColors to uniquedefinedColors's allObjects() as list
  return uniquedefinedColors  -- Added return statement
end getUniqueColorsFromFlatDicts

-- filter an array of dictionaries
on actionDictsWhose(predicateString, dictList)
  try
    set thePredicate to current application's NSPredicate's predicateWithFormat:predicateString
  on error errMsg number errNum
    display dialog "⚠️ Predicate error: " & errMsg
    return current application's NSArray's array() -- safe fallback
  end try
  return dictList's filteredArrayUsingPredicate:thePredicate
end actionDictsWhose

--Convert a list, ie {"a"}, to string, eg. "{\"a\"}" for use in predicate string eg. "name IN {\"Archie\",\"Bill\",\"Bob\"}
on listToLiteral(anAppleScriptList)
  return (current application's NSExpression's expressionWithFormat:"%@" argumentArray:{anAppleScriptList})'s |description|() as text
end listToLiteral

on plistDicts(actionsList)
  -- Convert AppleScript action references into NSDictionary objects via property list deserialization
  set plistXMLList to {}
  tell application id "com.stairways.keyboardmaestro.editor"
    repeat with anAction in actionsList
      set xmlString to (xml of anAction)
      set end of plistXMLList to xmlString
    end repeat
  end tell
  set plistDicts to (current application's NSArray's arrayWithArray:plistXMLList)'s valueForKeyPath:"propertyList"
  return plistDicts
end plistDicts
--===================THE HANDLER THAT FLATTENS THE MACRO'S ACTIONS =====================================
-- Takes any list of actions and recursively returns a flat a list of actions

-- https://github.com/kevin-funderburg/AppleScripts/blob/master/Keyboard-Maestro/Recursively-Get-Every-Action.applescript
-- https://forum.keyboardmaestro.com/t/feature-request-to-include-the-macro-group-of-the-target-macro-besides-its-name-in-the-execute-macro-macro/10885/15

on flattenActions(actionList)
  local actionList, output
  set output to {}
  tell application "Keyboard Maestro"
    get class of actionList
    if (class of actionList = list or class of actionList = action list) and (count of items of actionList) > 0 then
      repeat with act in actionList
        set output to output & my flattenActions(act)
      end repeat
      
    else if class of actionList = case entry then
      if (count of actionList's actions) > 0 then
        set output to output & my flattenActions(actionList's actions)
      end if
      
    else if class of actionList = action then
      set output to {actionList}
      
      -- groups
      try
        if (count of actionList's actions) > 0 then
          set output to output & my flattenActions(actionList's actions)
        end if
      end try
      -- switch statements
      try
        if (count of actionList's case entries) > 0 then
          set output to output & my flattenActions(actionList's case entries)
        end if
      end try
      -- if then actions
      try
        if actionList's thenactions β‰  missing value then
          set output to output & my flattenActions(actionList's thenactions's actions)
        end if
      end try
      -- if else actions
      try
        if actionList's elseactions β‰  missing value then
          set output to output & my flattenActions(actionList's elseactions's actions)
        end if
      end try
      -- try actions
      try
        if actionList's tryactions β‰  missing value then
          set output to output & my flattenActions(actionList's tryactions's actions)
        end if
      end try
      -- catch actions
      try
        if actionList's catchactions β‰  missing value then
          set output to output & my flattenActions(actionList's catchactions's actions)
        end if
      end try
    end if
  end tell
  return output
end flattenActions

Color Coder Macros.kmmacros (91 KB)

1 Like

OMG! You found the errant "end if" :jigsaw:--and fixed it!

I can't believe you combed through this verbosity monstrosity! Thank you!

It caused the script to skip adding the predicate of a color-set in which all actions have just the single identifying key, MacroActionType.

We can certainly condition adding a color-set on at least one subpredicate existing. However, since KM seems to consistently require a MacroActionType of every action, there should always be at least one subpredicate to add per color. I like your fix which takes that assumption out of the equation, but in the revision decided to live dangerously--possible because of KM's consistency--and place the "end if" just after the "two-key" identification block.

The fixed upload has a couple of checks added:

  • Feedback when a "Action Color Scheme" macro name can't be found.
  • Feedback when the macro is run without a target macro selected.

Bonus:

A utility macro is also included to view the xml of selected action(s) or actions of a single selected macro. It also posted as a separate download below the new Color Code macro.

This makes it easier to verify the existence of the "Action" key in an action's xml when deciding to override the script's treatment of the options of an action with a dropdown--which seem generally represented by the value of the Action key-- as separately color codable actions.

  1. Set the macro to a trigger of your choice

  2. Select an action(s) or macro and trigger the macro.

  3. It will display in a window the action or actions as of by default.

It declutters the xml by removing the plist description and ActionUID.

<array>
	<dict>
		<key>MacroActionType</key>
		<string>SetVariableToText</string>
		<key>Text</key>
		<string>bar</string>
		<key>Variable</key>
		<string>localFoo</string>
	</dict>
</array>

Set the variables localShouldRemoveActionUID and localRemovePlistDOCTYPE to "false" to change the default display.

You could also use it to find other identification keys like "Action".

FWIW, displayed xml can be pasted back into the Editor as actions.

If you set the variable "localDisplayAsAppleScriptQuotedXML" to true, the xml can be pasted into directly into Script Editor and used, for example, in KM Engine's "do script" and run as an action(s) from AppleScript.

https://wiki.keyboardmaestro.com/manual/Scripting?s[]=script

(If for some odd reason, you want to use the text to play with NSXMLDocument and xpath, make sure to set localRemovePlistDOCTYPE to "false")

(I like to trigger it with shift+space)

AppleScript

-- https://forum.keyboardmaestro.com/t/demo-scripting-nested-actions/40543/15
use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions
property author : "@CRLF"

--┏━━━━━━━━━━━━━━━━━━━━━━━
--┃ PURPOSE: OUTPUT DECLUTTERED XML OF SELECTED ACTION(S) OR
-- |                ACTIONS OF THE FIRST SELECTED MACRO

--┗━━━━━━━━━━━━━━━━━━━━━━━
-- GETS KM VARIABLE:
--        localShouldRemoveDOCTYPE
--        localShouldRemoveActionUID
-- DEFAULT FORMAT: xml as a bare <array/> of <dict> without ActionUID and plist description
-- USER OPTIONS:
-- Remove ActionUID
-- Remove plist DOCTYPE

--βš™οΈβš™οΈGet user settingβš™οΈβš™οΈ
set kminstance to system attribute "KMINSTANCE"
tell application id "com.stairways.keyboardmaestro.engine"
	set shouldRemoveDOCTYPE to getvariable "localShouldRemoveDOCTYPE" instance kminstance
	set shouldRemoveActionUID to getvariable "localShouldRemoveActionUID" instance kminstance
end tell

--convert setting to boolean values
try
	if shouldRemoveDOCTYPE contains "true" or shouldRemoveDOCTYPE contains "yes" or shouldRemoveDOCTYPE is "1" or shouldRemoveDOCTYPE is "" then
		set shouldRemoveDOCTYPE to true
	else
		set shouldRemoveDOCTYPE to false
	end if
end try

try
	if shouldRemoveActionUID contains "true" or shouldRemoveActionUID contains "yes" or shouldRemoveActionUID is "1" or shouldRemoveActionUID is "" then
		set shouldRemoveActionUID to true
	else
		set shouldRemoveActionUID to false
	end if
end try

--Get an array of the xmls of the selected actions or actions of the first selected macro
tell application id "com.stairways.keyboardmaestro.editor"
	
	set selectedActions to {}
	set classOfSelected1 to class of item 1 of (selection as list)
	if classOfSelected1 is macro then
		set selectedActions to actions of (item 1 of (selection as list))
	else if classOfSelected1 is action then
		set selectedActions to (((selection as list)))
	end if
	set actionXMLs to current application's NSMutableArray's array()
	repeat with anAction in selectedActions
		(actionXMLs's addObject:(xml of anAction))
	end repeat
end tell


--deserialize the array of xml into array of dictionaries
set actionDicts to ((current application's NSMutableArray's arrayWithArray:actionXMLs)'s valueForKeyPath:"propertyList")'s mutableCopy()
--remove action uids for less cluttered xml
if shouldRemoveActionUID is true then
	actionDicts's makeObjectsPerformSelector:"removeObjectsForKeys:" withObject:{"ActionUID"}
	set actionDicts to actionDicts
end if
--To exclude DOCTYPE, convert array of dictionaries NSXMLDocument, 
set actionsDocument to documentFromObject(actionDicts)
if shouldRemoveDOCTYPE then
	--get only the array of dictionaries--no DOCTYPE--for less cluttered xml
	set xmlArrayOfDictionaries to (actionsDocument's rootElement()'s children()'s firstObject())'s XMLString()
else --include DOCTYPE
	set xmlArrayOfDictionaries to actionsDocument's XMLString()
end if
--trim whitespace
set xmlOfSelectedActionsJoinedAndDecluttered to (xmlArrayOfDictionaries's stringByTrimmingCharactersInSet:(current application's NSCharacterSet's whitespaceAndNewlineCharacterSet)) as text

set xmlOfSelectedActionsJoinedAndDecluttered to xmlArrayOfDictionaries as text

-- OUTPUT SERIALIZED XML AS TEXT
xmlOfSelectedActionsJoinedAndDecluttered
--==============UTITLY HANDLER====================
-- convert an AppleScript or AppleScriptObjC object to NSXMLDocument
on documentFromObject(anObject)
	set {theData, theError} to current application's NSPropertyListSerialization's dataWithPropertyList:anObject format:(current application's NSPropertyListXMLFormat_v1_0) options:0 |error|:(reference) -- don't use binary format
	if theData is missing value then error (theError's localizedDescription() as text) number -10000
	set {doc, theError} to current application's NSXMLDocument's alloc()'s initWithData:theData options:(current application's NSXMLNodePreserveAll) |error|:(reference)
	if doc = missing value then error (theError's localizedDescription() as text)
	return doc
end documentFromObject
2 Likes

@CRLF, this is a really amazing and useful macro. Thanks so much for sharing!

I have two enhancement suggestions. I suspect the first might be relatively easy, whereas the second might be complex (or impossible).

  1. It would be great if disabled actions in Action Color Scheme were ignored. That way it would be easy to apply a subset of color specifications (useful at times), but still maintain the full set in the template macro.

  2. I alternate yellow/green for conditional and loop actions. That makes it easier to follow the macro logic when actions are moderately or deeply nested. Would it be possible to check the nesting level of an action type (in Action Color Scheme) and apply the colors per the nested level?

1 Like

I did actually but it was beyond me all the logic in it and you are the mastermind. I worked with Claude.ai back and forth to fix the error.

Thanks for all the details and the updated AppleScript!

1 Like