@Nige_S
Thank you for the review — all three points were well made and all three are now implemented and tested. Reporting back in case it's useful to anyone else.
1. The two AppleScript methods
You were right that this would do anyone's head in, and the environment limit turned out to be less remote than "slim chance" suggests. Both callers handle document content and one handles OCR output, so the 100K aggregate is genuinely reachable — at which point a 17-character window title gets excluded through no fault of its own, the subroutine searches for a window titled empty string, reports every position free, and every window stacks. That is bit-for-bit the original bug, which had already cost me an evening.
Standardised on getvariable with the instance parameter throughout. Only KMINSTANCE still comes from the environment.
2. Building the grid with a loop
Done, and made a subroutine as you suggested — Module_Create Grid Positions, taking an original, a step and a count. With two callers now sharing the pattern it earns its place immediately: changing a cascade is one parameter instead of six literal lines across two macros.
For Each over a number range 1 to Count, with the multiplier as (Local_i - 1) so the arithmetic stays visible in the value field rather than hiding in the range field.
3. Moving the polling loop inside the AppleScript
The strongest of the three, and adopted with two changes.
I kept the Engine's own dictionary rather than System Events. Testing here, System Events returned an empty window list for the Engine process while the Engine dictionary returned everything — probably a permissions thing on my machine, but the Engine route is proven and skips the accessibility layer.
I also added an exit: your loop has no upper bound, so a window that never appears would hold the semaphore indefinitely. It now self-limits at 150 checks (15s) and returns 0, with a 20s action timeout as a backstop that should never fire.
On "is 1" versus the regex — you were right. It went in at the same time as the getvariable fix and I never isolated which mattered. The question is moot now, since there's no condition test left.
One thing I decided against
Separately I considered passing a list of window titles so the two grids could see each other's windows. Rejected it: the subroutine is meant to serve many callers, and a rule requiring every caller to know every other caller's title gets violated silently the first time someone adds one. Non-overlapping grids are documented as a precondition instead.
One thing I decided to do
Also turned off the window title bar on the prompts, which removes the drag handle entirely — so a window can't be moved off its position, and the whole class of "someone nudged it and the slot silently freed" goes away.
Macros attached if anyone wants them.
Thanks once again!
Subroutine that creates the grid
Module_Create Grid Positions.kmmacros (17.2 KB)
Subroutine that determines the free grid position
Module_Determine Grid Position.kmmacros (52.2 KB)
Sample macro that calls the subroutine and grid inputs
Process Downloads.kmmacros (99.8 KB)
PS: Although this was enjoyable and worth the effort -- I think it looks fantastic and will be used repeatedly -- it was a ton of work to build!