Returning a value when triggering a macro via a script

You're able to trigger macros with script triggers, and even pass a parameter to them.

Is it possible to a have a value returned to the script from the macro if the macro ends with a 'Return Result' action? The script trigger wiki page makes no mention if this is possible or not.

If not, is this a viable feature request? You could utilise this to treat a macro like a function within scripts.

Thanks!

Edit:

It turns out that yes you can! I've used python via shell script in the example macros below:

Return Macro Value Via Script [Macros].kmmacros (11.9 KB)

This is the first macro which contains the script that 'targets' a macro, passing on a parameter, and captures any results:

And this is the example 'target' macro which checks to see if it has been passed on a parameter, and returns a result depending on the value of that parameter:

Below is the Shell Script / Python code below for anyone to copy/paste:

#!/Library/Frameworks/Python.framework/Versions/3.12/bin/python3

import subprocess

# The UUID or name of the Keyboard Maestro macro
macro_name= "[TEST] PythonReturn - Target Macro"
parameter = "apple"

# Use subprocess to run the osascript command and pass the parameter to the macro
result = subprocess.run(
    ['osascript', '-e', f'tell application "Keyboard Maestro Engine" to do script "{macro_name}" with parameter "{parameter}"'],
    capture_output=True,
    text=True
)

# Print only the result returned from the macro (without any extra text)
print(result.stdout.strip())

You can return a value that way when triggering a macro via JXA:

WOW! When did this happen? Triggering a Macro from JXA can return a result!.

I would imagine it works with AppleScript also.

Ah thank you for your response! You caught me in the middle of writing my own little tutorial on how to do this via the execute shell script action / python.

It would be great if the official wiki was updated on documentation for this :slight_smile: I'd happily do it but I know editor permissions are whitelisted.

1 Like

Yep, it's in the Engine's AppleScript Dictionary:

do script v : Execute a macro or action.
do script text : The name of macro, UID of macro, or plist of action to execute.
[with parameter text] : A parameter to the macro (in the %TriggerValue% token)
→ text : the result from a Return action.
1 Like

Sorry to resurrect a dead thread, but I can't get this to work in a custom HTML prompt.

The following javascript from a HTML prompt DOES trigger the target macro and pass on a parameter, but DOES NOT capture a result returned from the macro:

      // Trigger macro (Javascript)
      function run_km_macro(macroIdentifier, param) {
        //log inputs
        log_message("Macro to execute: " + macroIdentifier);
        log_message("Parameter to use: " + param);

        // Possibly re-stringify if param is JSON:
        let preparedParameter;
        try {
          preparedParameter = JSON.stringify(JSON.parse(param));
        } catch (err) {
          preparedParameter = param.replace(/\n/g, "\\n").replace(/\r/g, "\\r");
        }

        // KeyboardMaestro.Trigger with returning the macro's Return value
        const macroResult = window.KeyboardMaestro.Trigger(macroIdentifier, preparedParameter);
        console.log("Macro returned:", macroResult);
        return macroResult;
      }

the documentation doesn't explictly say that "window.KeyboardMaestro.Trigger" can return a value, but does suggest that AppleScript can:
"...executes the AppleScript and returns the result"

But I cannot get this to trigger a macro in this environment:

        // Trigger Macro (Applescript)
        function run_km_macro(macroUUID, parameter) {
          let preparedParameter = parameter;

          // If 'parameter' is already JSON, re-stringify it.
          try {
            const parsed = JSON.parse(parameter);
            preparedParameter = JSON.stringify(parsed);
          } catch (e) {
            // If not JSON, do some light cleanup of newlines
            preparedParameter = parameter.replace(/\n/g, "\\n").replace(/\r/g, "\\r");
          }

          // Now escape quotes and backslashes to make it safe in AppleScript
          const escapedParam = preparedParameter
            .replace(/\\/g, "\\\\") // escape backslashes
            .replace(/"/g, '\\"'); // escape double-quotes

          // Build the AppleScript
          const applescript = `
              tell app "Keyboard Maestro Engine"
                  set theResult to do script "${macroUUID}" with parameter "${escapedParam}"
                  return theResult
              end tell
          `;

          let scriptResult = "";
          try {
            // Applescript to trigger a KM Macro
            scriptResult = window.KeyboardMaestro.ProcessAppleScript(applescript);
            log_message("AppleScript run complete. scriptResult: " + scriptResult);
          } catch (err) {
            log_message("Error in run_km_macro AppleScript: " + err);
            throw new Error("run_km_macro failed: " + err);
          }

          return scriptResult;
        }

The following error occurs when trying to execute the macro like this:

[Log] [CompanyDataEditor] AppleScript run complete. scriptResult: KMERROR:Internal AppleScript Execute Error Keyboard Maestro Engine got an error: "***UUID hidden for privacy***" doesn’t understand the “do script” message.

This might be due to the double-quotes surrounding the UUID being escaped with backslashes and then consequently interpreted incorrectly as a different string altogether. If so, I do not know how I should programmatically configure AppleScript to be executed in this function.

I tried hard-coding the AppleScript as a single line with the correct UUID and a test parameter, but receive the same errors.

Yep, it's in the Engine's AppleScript Dictionary:

I did some googling and I cannot find what you're referencing anywhere. Actually, the only thing that comes up across the web is this comment.

The offical wiki documentation doesn't make any mention of how to return a value from AppleScript.

I appreciate any insight here. Thank you.

Unfortunately, a Custom HTML Prompt can't trigger a macro and receive a response - not even via AppleScript.

HOWEVER, I have a reasonably easy solution, which I'm writing up right now. But here's the gist:

I'll post a complete example later this morning, which includes an asynchronous method so you can "await" the response.

1 Like

Posted.

1 Like

In Script Editor (or Script Debugger, or whatever AS editor you use) use File->Open Dictionary... and open the "Keyboard Maestro Engine" dictionary. You'll find it in there.

An AppleScript can certainly return a result. Try this in a prompt:

<!DOCTYPE html>
<html>
<body>
<h1>Test</h1>
<script>
   let myVar = window.KeyboardMaestro.ProcessAppleScript( 'tell application "Keyboard Maestro" to return count of windows');
   document.write(`<p>Count of KM windows: ${myVar}</p>`);
</script>
</body>
</html>

What I don't think it can do is trigger a macro from a Custom HTML action -- I think you've done everything right, but it's a sandboxing issue.

You return a value from the macro to the AS using a "return" action in the macro. You can explicitly return a value from your AS with a return statement, otherwise the result of the final statement will be returned. But I don't think either of those help here -- you need to use the Trigger function, you can't get a return from that, and you'll have to use @DanThomas's fix.

1 Like

It's been a while, but I'm pretty sure this is true:

You can't return a result from AppleScript called from a Custom HTML Prompt, even though you think it should work.

I've had at least one long discussion with Peter about this before. I'll see if I can track it down.

Right. Here it is:

1 Like

Dan, you are a wizard. Sincerely, thank you very much for going to the effort to share a custom macro to solve this issue; what you provided is incredibly helpful.

Since you originally posted the solution, I've spent the time since deconstructing it (and the previous series of macros you shared that are attached to it) and trying to piece it all back together myself just to understand how it works. I'm a programming newbie, and KM is my conduit for learning, so being able to study tricks you're using, like dynamically creating a KM action in memory and executing it, is fascinating.

I've taken what you've done and distilled it into a solution that works for me, specifically for triggering KM Macros from a Custom HTML Prompt and returning a value to the prompt.

The solution consists of the following:

  • A module of helper javascript functions to be used in a Custom HTML Prompt, designed to trigger a KM Macro with the function "window.KeyboardMaestro.Trigger".
  • These then wait for the response from the KM macro detailed below, which is responsible for returning the response to the HTML prompt.

Javascript helper functions:

      /* Custom HTML Prompt Helper Function Module: 
   Extended Functions for async Macro Trigger + Response
   ------------------------------------------------------
   These functions allow you to trigger a Keyboard Maestro macro asynchronously
   and then await a response (injected from KM) via a custom event.

   1) run_km_macro_htmlprompt() - triggers the macro and awaits a response
   2) getResponse() - awaits a custom event "kmResponse"
   3) insertResponse() - dispatches that custom event

   Ensure that:
     - "run_km_macro_htmlprompt()" includes the window.KeyboardMaestro.Trigger call
     - The macro you trigger eventually calls the `Inject Response Into Custom HTML Prompt`
       subroutine with the correct code to invoke "insertResponse(...)".
    */

      /**
       * Triggers a Keyboard Maestro (KM) macro asynchronously and awaits its response.
       *
       * @async
       * @function run_km_macro_htmlprompt
       * @param {string} macroIdentifier - The identifier of the KM macro to trigger.
       * @param {Object|string} param - The parameter to pass to the KM macro. Can be an object or a string.
       * @returns {Promise<any|null>} - Resolves with the macro's response data or `null` if an error occurs.
       *
       * @throws {Error} Throws an error if awaiting the macro's response fails or times out.
       *
       * @example
       * const response = await run_km_macro_htmlprompt('MacroID', { key: 'value' });
       * console.log(response);
       */
      async function run_km_macro_htmlprompt(macroIdentifier, param) {
        try {
          // Log macro details
          log_message("Macro to execute: " + macroIdentifier);
          log_message("Parameter to use: " + JSON.stringify(param));

          // Trigger the KM macro with the provided identifier and parameter
          window.KeyboardMaestro.Trigger(macroIdentifier, param);

          // Await the macro's response with a 6-second timeout
          const macroResult = await getResponse(6000);
          log_message("Macro returned: " + macroResult);
          return macroResult;
        } catch (err) {
          log_message("Error in run_km_macro_htmlprompt: " + err.message);
          return null;
        }
      }

      /**
       * Waits for a custom "kmResponse" event and resolves with its data.
       *
       * @function getResponse
       * @param {number} timeoutMs - The maximum time to wait for the response, in milliseconds.
       * @returns {Promise<any>} - Resolves with the event's detail data or rejects if timed out.
       *
       * @throws {Error} Throws an error if the response times out.
       *
       * @example
       * getResponse(5000)
       *   .then(data => console.log('Received:', data))
       *   .catch(err => console.error(err));
       */
      function getResponse(timeoutMs) {
        // Creates a Promise that resolves or rejects based on a custom event or timeout.
        return new Promise((resolve, reject) => {
          const timer = setTimeout(() => {
            document.removeEventListener("kmResponse", handleResponse);
            log_message("Response timed out");
            reject(new Error("Timed out waiting for macro response"));
          }, timeoutMs);

          /**
           * Handles the "kmResponse" event by resolving the Promise with its data.
           *
           * @param {CustomEvent} evt - The custom event containing the response data.
           */
          function handleResponse(evt) {
            clearTimeout(timer);
            document.removeEventListener("kmResponse", handleResponse);
            log_message("Received 'kmResponse' event with data:", evt.detail);
            resolve(evt.detail);
          }

          document.addEventListener("kmResponse", handleResponse);
        });
      }

      /**
       * Dispatches a custom "kmResponse" event with the provided response data.
       *
       * This function should be called by the KM macro's `[Utility] Inject Response Into Custom HTML Prompt`
       * subroutine to send data back to the awaiting function.
       *
       * @function insertResponse
       * @param {any} response - The response data to dispatch with the event.
       *
       * @example
       * insertResponse({ success: true, data: {...} });
       */
      function insertResponse(response) {
        // This is called by the subroutine macro's
        // "ExecuteJavaScriptForCustomPrompt" injection.
        log_message("insertResponse called with:", response);

        // Dispatch an event that getResponse() is waiting for.
        // 'detail' can contain any data you'd like.
        document.dispatchEvent(new CustomEvent("kmResponse", { detail: response }));
      }

The KM Macro "Inject Response Into Custom HTML Prompt"

This is a subroutine macro that accepts two inputs: "InjectJavascript" (i.e. the value to inject into an HTML prompt) and "InjectWindowID" (the ID of the HTML Prompt). The script within is a distilled version of yours but only works to inject back into a prompt rather than handling additional potential contexts (e.g. Safari and Chrome).

The only real additions I have made are logging and additional escaping. Many of my macros return JSON Objects, for which this additional escaping function compensates. I also unintentionally butchered your KM variable reading of local & instance variables, which I'll come back and fix once I figure that out for JS (I was very tired at that point and just needed to ensure I had a solution that worked, hence why the attached macro is using global variables. Please feel free to correct my code there if you know what's wrong?)

The javascript for the "Inject Response Into Custom HTML Prompt" macro:

/**
 * Keyboard Maestro Variables Management Module
 *
 * This module provides functions to interact with Keyboard Maestro (KM) variables,
 * manage macro executions, and handle responses within custom HTML prompts.
 */

// Define global variables
const kme = Application("Keyboard Maestro Engine");
const currentApp = Application.currentApplication();
currentApp.includeStandardAdditions = true;
const kmInstance = currentApp.systemAttribute("KMINSTANCE");

/**
 * Determines if a variable name is an instance or local variable.
 *
 * @function isInstanceVariableName
 * @param {string} name - The name of the KM variable to check.
 * @returns {boolean} - Returns `true` if the variable is an instance or local variable, otherwise `false`.
 *
 * @example
 * isInstanceVariableName("InstanceVariable"); // true
 * isInstanceVariableName("GlobalVariable");    // false
 */
function isInstanceVariableName(name) {
  return name.match(/^Instance|^Local/) != null;
}

/**
 * Retrieves the value of a Keyboard Maestro (KM) variable.
 *
 * @function getKMVariable
 * @param {string} name - The name of the KM variable to retrieve.
 * @returns {string|undefined} - The value of the KM variable, or `undefined` if not found.
 *
 * @example
 * const value = getKMVariable("GlobalVar");
 * console.log(value);
 */
function getKMVariable(name) {
  if (isInstanceVariableName(name)) {
    result = kme.getvariable(name, { instance: kmInstance });
  } else {
    result = kme.getvariable(name);
  }
  return result;
}

/**
 * Sets the value of a Keyboard Maestro (KM) variable.
 *
 * @function setKMVariable
 * @param {string} name - The name of the KM variable to set.
 * @param {string} value - The value to assign to the KM variable.
 *
 * @example
 * setKMVariable("GlobalVar", "New Value");
 */
function setKMVariable(name, value) {
  if (isInstanceVariableName(name)) {
    kme.setvariable(name, { to: value, instance: kmInstance });
  } else {
    kme.setvariable(name, { to: value });
  }
}

/**
 * Escapes characters that would break a JavaScript string literal.
 *
 * @function escapeForJSString
 * @param {string} str - The string to escape.
 * @returns {string} - The escaped string safe for JavaScript string literals.
 *
 * @example
 * const safeStr = escapeForJSString('He said, "Hello\nWorld!"');
 * console.log(safeStr); // He said, \"Hello\nWorld!\"
 */
function escapeForJSString(str) {
  // Escapes characters that would break a JavaScript string literal
  return str
    .replace(/\\/g, "\\\\")   // backslashes
    .replace(/"/g, "\\\"")    // double quotes
    .replace(/\r/g, "\\r")    // carriage returns
    .replace(/\n/g, "\\n"); // newlines
}

/**
 * Escapes special XML characters to ensure valid XML construction.
 *
 * @function escapeForXml
 * @param {string} str - The string to escape.
 * @returns {string} - The escaped string safe for XML.
 *
 * @example
 * const safeXml = escapeForXml('<tag>Value & "Quotes"</tag>');
 * console.log(safeXml); // &lt;tag&gt;Value &amp; &quot;Quotes&quot;&lt;/tag&gt;
 */
function escapeForXml(str) {
  // Escapes special XML characters to ensure valid XML construction
  return str.replace(/[<>&'"]/g, function (c) {
    switch (c) {
        case '<': return '&lt;';
        case '>': return '&gt;';
        case '&': return '&amp;';
        case '\'': return '&apos;';
        case '"': return '&quot;';
    }
  });
}

/**
 * Appends a message to the InjectJavascriptLog variable if logging is enabled.
 *
 * @function logMessage
 * @param {string} message - The message to log.
 *
 * @example
 * logMessage("Injection started.");
 */
function logMessage(message) {
  if (isLoggingEnabled) {
    const currentLog = getKMVariable("InjectJavascriptLog") || "";
    setKMVariable("InjectJavascriptLog", currentLog + message + "\n");
  }
}

// Check if logging is enabled
const isLoggingEnabled = getKMVariable("InjectJavascriptLogWrite") === "true";

/**
 * Injects JavaScript code into a Custom HTML Prompt window and retrieves the result.
 *
 * This function constructs an ephemeral KM action to execute the JavaScript code,
 * dispatches the code to the prompt, and retrieves the result from KM variables.
 *
 * @function injectResponse
 * @returns {string} - The result of the JavaScript injection, or a JSON string containing an error message.
 *
 * @example
 * const result = injectResponse();
 * console.log(result);
 */
function injectResponse() {
  try {
    logMessage("Starting injectResponse function.");

    // Retrieve the JavaScript code to inject from KM variable
    const jsToInject = getKMVariable("InjectJavascript");
    logMessage(`Retrieved jsToInject: ${jsToInject}`);

    if (!jsToInject) {
      throw new Error("No code found in InjectJavascript");
    }

    // Take the javascript to inject and escape it
    const safeJS = 'insertResponse("' + escapeForJSString(jsToInject) + '")';

    // Retrieve the Custom HTML Prompt window ID from KM variable
    const windowID = getKMVariable("InjectWindowID");
    logMessage(`Retrieved windowID: ${windowID}`);

    // Define the variable name to return results to
    const resultVariableName = "InjectionResult";

    // Define the variable name to return errors to
    const errorVariableName = "InjectionError";

    if (!windowID) {
      throw new Error("No Custom HTML Prompt window ID found in InjectWindowID");
    }

    // Construct the ExecuteJavaScriptForCustomPrompt action XML
    const ephemeralActionXML = `
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" 
   "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>MacroActionType</key>
  <string>ExecuteJavaScriptForCustomPrompt</string>
  <key>Text</key>
  <string>${escapeForXml(safeJS)}</string>
  <key>WindowID</key>
  <string>${escapeForXml(windowID)}</string>
  <key>Variable</key>
  <string>${resultVariableName}</string>
  <key>UseText</key>
  <true/>
  <key>TimeOutAbortsMacro</key>
  <true/>
</dict>
</plist>
    `.trim();

    logMessage("Constructed ephemeralActionXML:\n" + ephemeralActionXML);

    // Execute ephemeral action
    kme.doScript(ephemeralActionXML);
    logMessage("Executed ephemeral action via doScript.");

    // Give KM a tiny delay to finish writing the result variable
    delay(0.2);

    const injectionResult = getKMVariable("InjectionResult");
    logMessage("injectionResult => " + injectionResult);

    return injectionResult;
  } catch (err) {
    logMessage("Error in injectResponseEphemeral: " + err.message);
    return JSON.stringify({ error: err.message });
  }
}

// Main script execution
try {
  return injectResponse();
} catch (err) {
  logMessage(`Error in script execution: ${err.message}`);
  // Return error message if injection fails
  return JSON.stringify({ error: err.message });
}

All credit for the methodologies used in the above go to Dan.

The macro

Inject Response Into Custom HTML Prompt copy Macro (v11.0.3)

Inject Response Into Custom HTML Prompt copy.kmmacros (19 KB)

I use this subroutine as an alternative 'Return' action. As a contextual example in my own workflow, I have a macro that programmatically edits data in a spreadsheet. The macro expects a JSON object to be used as the %TriggerValue%. I parse that object for various points of information. When I intend to execute this macro from a HTML prompt, I add a key-value pair to the JSON object to send to KM labelled "WindowID" to both indicate the intended use case and also provide the ID of the Custom HTML Prompt:

const param_obj = {
  EntityTag: entity_tag || "",
  ConfigurationPreset: configuration_preset,
  TargetUUID: "UUID hidden for privacy",
  WindowID: "CSVEditorWindow",
};

In my spreadsheet macro, at the end of the script, I have an "If All Conditions Met Execute Actions" look for the presence of this key value in the variable I've set to capture the %TriggerValue%. If it detects it, it provides the python's result and WindowID value to the injection macro. Otherwise, it uses the 'Return' action, sufficient for other non-HTML-environment scripts and macros. The filter for JSON Compact reduces the complexity of the escaping operation needed in the injection macro.

Note: I've shared this "If" action below. This forum's formatting makes it appear as a macro rather than as just an action.

Using the injection macro as an alternative 'return' action

Select Return Method Based On Execution Context Macro (v11.0.3)

Select Return Method Based On Execution Context.kmmacros (3.2 KB)

There is something to note - I had issues with your injection script. I would appreciate your insight on the following:

In your "Execute Dynamic Javascript in Browser" macro's javascript, you defined the in-memory KM actions by using Action UID's:

<dict>
    <key>ActionUID</key>
    <integer>199489</integer>    <--- the Action UID
    <key>DisplayKind</key>
    <string>Variable</string>
    <key>MacroActionType</key>
    <string>ExecuteJavaScriptForCustomPrompt</string>
    <key>Path</key>
    <string></string>
    <key>Text</key>
    <string>${escapeForXml(javascript)}</string>
    <key>TimeOutAbortsMacro</key>
    <true/>
    <key>UseText</key>
    <true/>
    <key>Variable</key>
    <string>${resultVariableName}</string>
    <key>WindowID</key>
    <string>${escapeForXml(kmwindowid)}</string>
</dict>

This gave me a lot of issues. I originally thought that Action UIDs in this context were an ID for the 'generic' action type. That is to say, I thought "199489" would always refer to the action "Execute a JavaScript in all Custom Prompts". But it's actually a unique identifier that relates to a specific instance of that action. If you create three new actions of the same type in the KM editor, they'll all have different UIDs.

I thought you might be referencing some 'hidden' action or action held in a sort-of 'bank' macro that contained blank versions to then programmatically configure. I couldn't find anything of the sort.

I tried making my own, looking through the XML, taking the UIDs, and using those in the script, to no avail.

After a lot of trial and error, I realised you could use generic names for these actions, and the execution began to work:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" 
   "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>MacroActionType</key>
  <string>ExecuteJavaScriptForCustomPrompt</string>
  <key>Text</key>
  <string>${escapeForXml(safeJS)}</string>
  <key>WindowID</key>
  <string>${escapeForXml(windowID)}</string>
  <key>Variable</key>
  <string>${resultVariableName}</string>
  <key>UseText</key>
  <true/>
  <key>TimeOutAbortsMacro</key>
  <true/>
</dict>
</plist>

Using this fixed the issue. But I'm genuinely curious how UIDs work in your scenario, given that seemingly trying the same approach to generating and using my own UIDs didn't help.

Wow, it looks like you're learning things really quickly. Good job.

The ActionUID is just left over from the XML I copied it from. You don't need the ActionUID at all - it works fine without it. KM s smart enough to not let duplicate UIDs cause any problems. I just forgot to delete it.

I'm glad you asked, and that you took the initiative to figure it out. With that said, a lot of times I post code with some parts of it that either aren't relevant, or are left over from other things. So while I do encourage you to try and figure out stuff, don't be afraid to ask if the code is relevant.

One thing I noticed throughout your macros was that you have a tendency of using global variables. I highly recommend you start using Local or Instance variables, because their values go away when they're not needed anymore. Global variables just pollute the KM environment.

A few other comments: If you use Local and Instance variables, you never have to clear them out at the start - they default to being empty.

Also, there's two KM actions that are really helpful when working with JSON. One is "Set Variables to JSON". It lets you take all JSON fields and move them to KM variables. So, for example:

image

gives you this:

image

And you can do the opposite - create a JSON variable from variables with a specific prefix:

image

gives you this:

image

I hope you're using VS Code to design your HTML pages. If so, I have some macros you might want that help me a lot.

I usually put my HTML, CSS, and JS in separate files, because it's much easier to develop that way. But if I need to combine them all into one HTML file to distribute it somewhere, I have a macro that combines those 3 files into one HTML file.

And that's just the tip of the iceberg for helper macros I have. So feel free to ask more.

2 Likes