How Are HTML Button Result Values Populated

As a start, here is the HTML Code that spawned this discussion (errrrr, rabbit hole) which was written by Claude Code.

To keep things unambiguous, I'll use these definitions:

  • The macro — the sequence of actions.

  • The prompt action — the Custom HTML Prompt action inside the macro.

  • The window — the Custom HTML Prompt window on screen.

  • KM Engine — the Keyboard Maestro engine working in the background.

  • You — the person picking an item from the prompt.

  • HTML Result Button — the global variable (it appears in my global variables list) that holds the button / list item picked. (I gather %HTMLResult% is the modern replacement and is documented as scoped to the execution instance — see the last question.)

  • JS Handler — the JavaScript in the prompt listening for key presses.

  • KM Handler — the KM Engine listening for Return and Esc, separate from the JS Handler.

The one distinction that matters: the prompt action "finishing" (the macro moves to the next action) is a separate event from the window "closing" (the window leaves the screen).

ITEM 1: Synchronous prompt (the prompt action finishes at Submit())

  1. The macro reaches the prompt action.

  2. The prompt action clears / deletes HTML Result Button.

  3. The prompt action spawns the window and waits (the prompt action has not finished). The macro is parked, waiting for the prompt action to finish.

  4. You pick a button / row. Your pick calls Submit(choice).

  5. Submit(choice):
    a. Tells the KM Engine you submitted the value choice.
    b. The KM Engine finishes the prompt action, which copies choice into HTML Result Button.
    c. Closes the window.

  6. HTML Result Button holds choice.

  7. The macro moves to the next action.

Question: Is the above correct and, if not, please correct it?

ITEM 2: Asynchronous prompt (the prompt action finishes at launch)

  1. The macro reaches the prompt action.

  2. The prompt action clears / deletes HTML Result Button.

  3. The prompt action spawns the window but does not wait. The prompt action finishes immediately, because it has completed its only job — spawning the window.

  4. There are now two independent streams:

a. The macro
i. At the finish of the prompt action, the KM Engine has no value for choice (you haven't chosen yet), so it copies nothing into HTML Result Button (the variable is never created — it doesn't appear in KM → Settings → Variables).

ii. The macro walks straight on to the next action, while the window sits open on its own.

b. The window
i. After the prompt action has finished, you pick a button / row, which calls Submit(choice)

ii. Submit(choice) closes the window.

iii. The prompt action has already finished, so there is no trigger to tell the KM Engine to copy choice into HTML Result Button.

iv. HTML Result Button is never created (doesn't appear in KM → Settings → Variables).

Question: Help me understand how / why the window is independent from the macro. How / why does the KM Engine treat it as a separate entity?

Question: Is the above correct and, if not, please correct it?

ITEM 3: Return vs double-click (how you select changes how many submitters act)

The above assumed one submitter. But how you pick a button / row changes how many submitters act on it.

ITEM 3A: Does selecting with Return create a race condition?

If you pick using Return, there could be two submitters acting on the same keystroke:

  1. The JS Handler catches Return and calls Submit(choice), where choice is the row value.

  2. The KM Handler treats Return as "press the default button" — which for a window with buttons would be the default button's value, and for a buttonless list would be OK. This calls Submit(OK).

Question: Is the above correct and, if not, please correct it?

Question: Is there actually a race condition here — and does something prevent it, e.g. the preventDefault() I have in my keydown handler? Put differently: does preventDefault() on Return / Esc suppress the KM Handler? If there's no race, please explain why; if there is, please explain how it's resolved.

Question:What value will HTML Result Button hold (running synchronously), and why? Very confused about this one.

ITEM 3B: Selecting with a double-click does not create a race condition.

If you pick using a double-click, the KM Handler is not involved. The JS Handler is the only submitter, and the prompt action / window behave as described above.

Question:Is the above correct and, if not, please correct it?

Question: Is there no KM Handler equivalent that listens for mouse clicks? If not, why not? If there is, would there be a similar race — but with both submitters carrying the same choice value?

One more, on scope:

Note: The wiki describes %HTMLResult% as "the result of the last Custom HTML Prompt action in this execution instance" (instance-scoped), yet the older HTML Result Button shows up in my global variables list. Are these two genuinely different scopes, and is that difference relevant to any of the above?

KM version: [11.0.4] · macOS: [26.5.2]

Apologies for the additional post, I meant to add teh HTML code but it is too long in terms of words, what is the protocol here for such a situation, dropbox link, etc.?

Question -- Why are you using HTML Result Button? It's a "special variable" that was deprecated with the introduction of the %HTMLResult% Token, as per this Wiki page.

Since you shouldn't be using HTML Result Button in any macro written since KM v8, I suspect that most of your questions can remain unanswered!

I actually am not using it.

It is appearing in Keyboard Maestro > Settings > Variables and I am curious as to how it works and, because it is deprecated, why it even appears in the Variables list at all (I would have thought it would be blocked from appearing).

It would be great to get answers as it would give me insight into the back of Keyboard Maestro which will help me understand how things work.

Why? Because you set it to run asynchronously. So it must be a new "thing", separate from the currently-executing instance. How? In a similar way to how "Display text" is done.

Why does it matter?

Much of the rest requires some demo HTML code -- you should be able to cut yours down to an MVP inside a macro that you can upload. But remember -- if it's too long to post, it's likely too long for anyone to read :wink:

Deprecated. As in "is still there, for backwards compatibility, but you shouldn't be using it any more". Remove it and you could break a lot of pre-v8 macros...

Appreciated and great idea, it was a ton of code and other items not needed to get to the point.

Code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<style>
  body{font:14px -apple-system,sans-serif;margin:0;padding:8px;}
  #search{width:100%;box-sizing:border-box;padding:6px;font-size:14px;margin-bottom:6px;}
  #list{list-style:none;margin:0;padding:0;max-height:300px;overflow:auto;}
  #list li{padding:6px 8px;cursor:default;}
  #list li.sel{background:#0a84ff;color:#fff;}
</style>
</head>
<body>
<input id="search" type="text" placeholder="Type to filter, Return to select, Esc to cancel">
<ul id="list"></ul>
<script>
function kmGet(n){try{return window.KeyboardMaestro.GetVariable(n)||'';}catch(e){return '';}}
function kmSet(n,v){try{window.KeyboardMaestro.SetVariable(n,v);}catch(e){}}

function parseItems(text){
  var out=[];
  String(text).split(/\r?\n/).forEach(function(line){
    var t=line.trim(); if(!t) return;
    var i=t.indexOf('__');
    if(i>0) out.push({value:t.slice(0,i),display:t.slice(i+2)});
    else out.push({value:t,display:t});
  });
  return out;
}

var ITEMS=parseItems(kmGet('Local_PromptListItems'));
if(!ITEMS.length) ITEMS=parseItems("Alpha\nbeta__Beta Item\nGamma");
var VISIBLE=[], SEL=0;

function render(){
  var q=document.getElementById('search').value.trim().toLowerCase();
  VISIBLE=ITEMS.filter(function(it){return !q||it.display.toLowerCase().indexOf(q)!==-1;});
  if(SEL>=VISIBLE.length) SEL=Math.max(0,VISIBLE.length-1);
  document.getElementById('list').innerHTML=VISIBLE.map(function(it,i){
    return '<li class="'+(i===SEL?'sel':'')+'" data-i="'+i+'">'+it.display+'</li>';
  }).join('');
}

function submit(){
  if(!VISIBLE.length) return;
  var choice=VISIBLE[SEL].value;
  kmSet('Local_PromptListChoice',choice);
  try{window.KeyboardMaestro.Submit(choice);}catch(e){}
}
function cancel(){
  kmSet('Local_PromptListChoice','');
  try{window.KeyboardMaestro.Cancel('Cancel');}catch(e){}
}

document.addEventListener('keydown',function(ev){
  if(ev.key==='Escape'){ev.preventDefault();cancel();}
  else if(ev.key==='Enter'){ev.preventDefault();submit();}
  else if(ev.key==='ArrowDown'){ev.preventDefault();if(VISIBLE.length){SEL=(SEL+1)%VISIBLE.length;render();}}
  else if(ev.key==='ArrowUp'){ev.preventDefault();if(VISIBLE.length){SEL=(SEL-1+VISIBLE.length)%VISIBLE.length;render();}}
});
document.getElementById('search').addEventListener('input',function(){SEL=0;render();});
document.getElementById('list').addEventListener('click',function(ev){
  var li=ev.target.closest('li'); if(!li) return;
  var prev=document.querySelector('#list li.sel'); if(prev) prev.classList.remove('sel');
  SEL=parseInt(li.getAttribute('data-i'),10)||0; li.classList.add('sel');
});
document.getElementById('list').addEventListener('dblclick',function(ev){
  var li=ev.target.closest('li'); if(!li) return;
  SEL=parseInt(li.getAttribute('data-i'),10)||0; submit();
});

render();
document.getElementById('search').focus();
</script>
</body>
</html>

I will that I am not entirely sure teh code is needed as it is more a question of how the Keyboard Maestro operates.

Thank you.

But part of how KM operates is the inputs you give it and how you provide them...

Easy bit first -- don't use HTML Result Button! Not just because it's deprecated (though that should be reason enough) but because it's a Global variable, with all the "macros trampling over each other" problems that implies, and that also gets deleted every time you spawn a "Custom HTML Prompt".

Since the first thing you'll need to do after any Prompt submission is copy the value in HTML Result Button to a Local variable, that same value that is already available via the %HTMLResult% instance Token, you're just giving yourself more work.

When you run the Prompt asynchronously it is "outside" your macro's executing instance and, as usual, instance variables and instance Tokens -- %HTMLResult% -- aren't available to it. HTML Result Button is populated from the %HTMLResult% instance Token, which won't evaluate, which is why the special Global isn't created after an async Prompt.

It's the same as with an async AppleScript -- and the solution's the same. Post your data to a KM Global variable, or trigger a macro to handle the data (sent in the trigger value).

Optional extra -- include the executing instance UUID in the HTML for the async Prompt, then use the ProcessAppleScript function to

tell application "Keyboard Maestro Engine" to setvariable "foo" instance theUUID

...but you'll need to be sure the instance is still executing!

I'm not sure what you mean by this. KM has nothing to do with the "operation" of the Prompt, it's all WebKit -- WebKit sees Return as "press the default button", WebKit handles the EventListener that also spots a Return key press.

I'm not going down the rabbit-hole of "Event Capturing" and "Event Bubbling" (I know they happen, and that's enough for me!), but I'm pretty sure the answer's in your code:

document.addEventListener('keydown',function(ev){
  if(ev.key==='Escape'){ev.preventDefault();cancel();}
  else if(ev.key==='Enter'){ev.preventDefault();submit();}
...

"If the Enter key was pressed, prevent the default event from happening (the "press the default button" action) and run the submit() function."

Not really, because even if both submissions happen they will be in a known, consistent, order because your DOM hierarchy is static (Capturing and Bubbling again). But you only get one because of the preventDefault().

Why not run it and find out? And then forget about HTML Result Button entirely :wink:

I think the rest is sorted if you remember that much of what you are asking is actually nothing to do with KM and everything to do with WebKit. If you haven't already, enable the Web Inspector:

defaults write com.stairways.keyboardmaestro.engine WebKitDeveloperExtras TRUE 

...and then you can right-click and "Inspect Element" on the Prompt and start poking around in the JS console.

If nothing else, the above should keep you busy until someone who actually knows some JS comes along :wink:

I am not using it. I am writing to my local variables. I avoid using global variables unless absolutely necessary. :slight_smile:

Appreciated, I will simplify the code.

Question: I thought you could make them available by reframe the instance, is this not correct?

Agreed and happily consistent with what I wrote!

Agreed, as I believe this is consistent with my instance reference marked Question above.

Appreciated, I will dig into this.

Appreciated.

Appreciated, so a race condition cannot occur!

I have run it -- before I started teh thread and I know the answer is the selected button / list item but now I know why from your above two answer. This is exactly what I was hoping for.

As i) button / list selection cannot create a race and ii) preventDefault prevent the default key (i.e., RETURN) means that HTML Button Response holds the button / list value!!

Excellent and will do!

Appreciated and, as always, very helpful! Huge thank you!!

Instance variables aren't available to the async Prompt. They're available to an AppleScript that you run from the Prompt, assuming that a) you can pass the instance UUID to the Prompt somehow, and b) the instance is still executing. That's OK for "getters" but setting an instance (or local) variable from outside its scope should be a last resort as you're deliberately breaking the "protection" that scoping gives you. Instead, use a Global -- then it's obvious that "this variable may be messed with from outside this instance" and you can proceed accordingly.

%HTMLResult% for this Prompt isn't available at all -- it can't be explicitly set (it's an evaluated Token, not a variable), and doesn't have a value for this Prompt until submission (at which point the Prompt will close so there's nothing to "get" the value).

(You could, of course, evaluate %HTMLResult% for an immediately previous Prompt in the same instance -- perhaps to change this Prompt dependent on answers to the last. Though I have to think that would be easier done in the macro than in the Prompt itself!)

All of this begs the question -- why worry about how to return data from an async Prompt to the macro that spawned it? You generally only run something asynchronously when the "parent" does not care about the result, otherwise you are just adding complications onto complications.

Appreciate the added information.

I had assumed -- something I should not do -- that a similar capability existed in Java Script as that in AppleScript.

Agreed and understood; entirely consistent with my mental model.

Agreed and understood.

I am curiou say nature and I like to understand how things work. I was seeing something different behaviours in my testing and wanted to confirm / correct my mental model.

Browser JavaScript is sandboxed, thank goodness. Imagine the nastiness that could ensue! So you have to interact via one of the window.KeyboardMaestro functions and either use AS (I'm assuming you can't just pass JXA in to that function) or trigger a macro with appropriate parameters.

1 Like

@peternlewis or anyone who knows.

I have two related questions about where the result value actually gets written that I would greatly appreciate response to.

What the Wiki says. The wiki describes window.KeyboardMaestro.Submit( buttonName ) as "submit the form and write the Keyboard Maestro Variables", and says %HTMLResult% "is set to the parameter in the Submit() function". You have also posted that the Result Button is "cleared at the start of the … action, and then set if any button is pressed".

What I observe. When the Custom HTML Prompt action runs asynchronously, Submit() is definitely called and the window definitely closes, but HTML Result Button is never created.

My reading, which I would like confirmed or corrected: Submit() sends the value, but the thing that records it is the prompt action, which must still be executing to receive it. Run asynchronously, the action finished the moment the window appeared, so when the value arrives there is nothing left to write it down.

Q1. Is the above correct? And does %HTMLResult% behave the same way — never set for an asynchronous prompt?

Q2. For a synchronous prompt, what is the order of events when Submit(value) is called? Is the value written to HTML Result Button / %HTMLResult% before the window closes and the action completes, or is the window closed first and the value written as the action finishes?

I ask Q2 because it decides whether anything still running in the window immediately after Submit() could ever read the value back — and, more generally, whether "the window closed" and "the value is recorded" are one event or two.

Much thanks is advance.

Correct.

If the action is run asynchronously, so the action is no longer executing when the prompt finishes, and it is the action that sets the result.

However, (rightly or wrongly) the custom html prompt still runs under that instance in the sense that you can use instance variables in the forms, and when the Submit is clicked those instance variables will be set.

Why does it matter? How would you even detect the difference from the Keyboard Maestro side?

On the web side, things happen after the Submit (they have to, to read the variable values), but not for long.

The value is recorded must, by necessity, happen before the window is closed.

@peternlewis

As always, greatly appreciated.

I do have two quick follow ups and one unrelated question:

  1. In a synchronous prompt, which happens first: the window closing, or the action writing HTML Result Button [NOTE: I understand your point that it does not matter but a asking out of curiosity]?

  2. In an asynchronous prompt, does %HTMLResult% follow the same rule as HTML Result Button in that it is written by the action and therefore never gets written?

  3. I have built a series of themed Custom HTML Prompt that mimic macOS (which I will share shortly). One thing I that I cannot produce is the translucency where the desktop shows through the panel behind the content.

From CSS I can only style within the web view; backdrop-filter blurs the page's own background, not the window over the desktop. The prompt window always paints an opaque background that the HTML can't reach.

Is there any way — an action setting, a plist key, a window.KeyboardMaestro call, anything — to make the Custom HTML Prompt window's background itself transparent or vibrant, so the desktop is visible behind it like Spotlight or Control Center? Or is the window background always opaque by design?

Thank you.

macOS Tahoe 26.5.2, Keyboard Maestro 11.0.4.

Since %HTMLResult% is an instance Token and an async "Custom HTML Prompt" is in its own executing instance (assuming it even is an executing instance), if it was set you'd still not be able to access it from the calling macro.

But that's simple enough to test. Have you?

There's a "Transparent" option in the Action's settings. Perhaps you could combine that with some CSS background and opacity/blur tricks if the default is a little too transparent.

Or you could leave it opaque so people can appreciate your lovely design and not be distracted by the blurry stuff behind your fields and buttons...

Good point, and no — I'd been reasoning instead of testing. So I ran it.

Async prompt with a keep-alive after it, a button calling Submit('hello'), then Display Text showing both %HTMLResult% and the HTML Result Button variable: both come back empty. Run the same thing synchronously and both come back "hello".

Combined with Peter's note that it's the action that sets the result, that answers it: async isn't a case of "set but unreachable across instances" — the action has already finished, so the result is simply never written, neither the old variable nor the token. The scope question doesn't even arise, because there's nothing there to reach for.

Appreciated and that worked, that said, I looked at the two and decided to leave it opaque so people can appreciate the design and not be distracted by the blurry stuff behind it.

Thank you for teh comment that it I lovely!

The bit about testing was -- the rest was total b*ll*cks, I'm afraid...

I should have read Peter's previous answer more closely, particularly:

In an attempt to add something useful (and, hopefully this time, correct) it appears that the %HTMLResult% Token is cleared (like HTML Result Button) when a second HTML Prompt is spawned within an instance -- sync or async.

Other random observations are as you might expect...

%HTMLResult% is "inherited" by subroutines and sync sub-macros, and if they set a value it's passed back up to the parent macro -- including the empty string if the subroutine/sub-macro's Prompt is cancelled or run async.

It isn't inherited by async subs, and the parent's Token retains its original value even if the async sub changes it.

Appreciated and --having got off my lazy horse -- confirmed through testing!

So much to learn!

I don't know and you should not depend on it.

Neither the button nor the token is written. Probably the token should be written, given the variables, including instance variables, are written.

Not that I know of.

@peternlewis , greatly appreciated, thank you!