Cross-reference Macro

This screenshot shows my approach to cross-referencing a list of "Product" names with their corresponding "Template" names. The goal is to automatically open the right Illustrator template based on the current Product variable. The "TemplatePDF" variable is then used to name the resulting PDF file when saved.

The macro works great, but with over 60 products in the list (I've only included 3 in the image to show the pattern), the "Switch" action is tedious to maintain. Is there a better approach to this kind of "Cross-Reference" macro (I'm guessing Script)?

Thanks for any suggestions.


I'd suggest setting up an array that associates the product name with the Illustrator template. Nothing fancy, editable with a text editor.

Then your macro could simple look up the product name and store the associated template to the Keyboard Maestro variable.

Should you end up with 120 or more templates, the same code would work fine and you could easily expand the associations to many more.

1 Like

Thank you, mrpasini. I'll read up on arrays.

Here is an approach which looks up the product in a MultiMarkdown table.

The table doesn't have to be neat, as long as:

  1. There is an MMD table 'ruler' in the second line,
  2. and a pipe character between each field, and
  3. the product name is spelled exactly as it will be searched for.

(spaces to left and right of values will be ignored,
as will any pipe character at start and end of of each row );

You could also, of course, keep the MMD table in a text file, and get KM to read it in.

Lookup MMD table and set variables by product key.kmmacros (23.8 KB)

Javascript source:

(() => {
    'use strict';

    const main = () => {
            kme = Application('Keyboard Maestro Engine'),
            strTable = kme.getvariable('mmdTable'),
            strProduct = kme.getvariable('someProduct'),

            dctMatch = dictFromMMDTable(strTable)[strProduct];

        return dctMatch !== undefined ? (
                k => kme.setvariable(k, {
                    to: dctMatch[k]
            'Variables set: ' + showJSON(dctMatch, null)
        ) : 'Product not found in table as spelled: ' + strProduct;


    // dictFromMMDTable :: String -> Dict
    const dictFromMMDTable = strTable => {
            rgxRuler = /^[\s\|\:\-\.]+$/,
            notRuler = x => !rgxRuler.test(x),
            notEmpty = x => x.trim().length > 0,

            // Any pipe(s) at start or end of row are ignored.
            values = s => map(
                filter(notEmpty, s.split(/\s*\|\s*/))

            xs = filter(notEmpty, lines(strTable)),
            colNames = values(takeWhile(notRuler, xs)[0]),
            rows = map(values, takeWhileR(notRuler, xs));

        return foldl(
            (a, vs) => {
                const kvs = zip(colNames, vs);
                return Object.assign(
                    a, {
                        // The value in the first column of a row
                        //  is used in the top-level table dict
                        //  as a key to a new row dict.
                        [kvs[0][1]]: foldl((subDict, tpl) =>

                            // Each cell is added under a colName key
                            // to the dict representing its row.
                                subDict, {
                                    [tpl[0]]: tpl[1]
                            ), {}, kvs.slice(1))
            }, {},

    // GENERIC FUNCTIONS --------------------------------------

    // Tuple (,) :: a -> b -> (a, b)
    const Tuple = (a, b) => ({
        type: 'Tuple',
        '0': a,
        '1': b,
        length: 2

    // filter :: (a -> Bool) -> [a] -> [a]
    const filter = (f, xs) => xs.filter(f);

    // foldl :: (a -> b -> a) -> a -> [b] -> a
    const foldl = (f, a, xs) => xs.reduce(f, a);

    // lines :: String -> [String]
    const lines = s => s.split(/[\r\n]/);

    // map :: (a -> b) -> [a] -> [b]
    const map = (f, xs) =>;

    // showJSON :: a -> String
    const showJSON = x => JSON.stringify(x, null, 2);

    // strip :: String -> String
    const strip = s => s.trim();

    // takeWhile :: (a -> Bool) -> [a] -> [a]
    const takeWhile = (p, xs) => {
        let i = 0,
            lng = xs.length;
        while ((i < lng) && p(xs[i])) {
            i = i + 1;
        return xs.slice(0, i);

    // takeWhileR :: (a -> Bool) -> [a] -> [a]
    const takeWhileR = (p, xs) => {
        let i = xs.length;
        while (i-- && p(xs[i])) {}
        return xs.slice(i + 1);

    // zip :: [a] -> [b] -> [(a, b)]
    const zip = (xs, ys) =>
        xs.slice(0, Math.min(xs.length, ys.length))
        .map((x, i) => Tuple(x, ys[i]));

    // MAIN ---
    return main();

This is so cool. I so appreciate all the help I get on this forum. This program is really making a difference in our Production Art department and it will only get better!

1 Like

And here's a very simple illustration of retrieving data from an array.

The array is just a set line with two items separated by a colon, in this case a Manufacturer and a Model.

To find the Model (or Illustator template in your example), you search the Manufacturer (Product name). That gives you the associated model, stored in a local variable.

The array can be stored in a text file easily editable by anyone, one item per line, delimited by that colon (any character not used in the names).

Keyboard Maestro 8.2.1 “Array Example” Macro

Array Example.kmmacros (4.0 KB)

1 Like

I use a very simple approach, similar to what @mrpasini was suggesting, but using RegEx instead of arrays.

I have a macro which moves the mouse pointer to a preset position whenever the window changes. It looks up the position using RegEx and the App Name.

Here's a sampling of my list, which I maintain in a KM Variable.
Could be just as easily maintained in a File.

Google Chrome,TL,93,121
Keyboard Maestro,TL,801,187
Microsoft Outlook,TL,316,220

Here's the RegEx, which uses the KM Variable as the source, but could use a file:


This would be very easy to adapt to a Product/Template list.



I'm in over my head, as usual. I realize I did not consider a "RegEx" solution @JMichaelTX showed me, here, that I am currently using in my "Case" action approach. It allows several similar products to open the same template. Can this be uses as part of one of the approaches shared in this thread?

Hope that makes sense.

I'm not sure I understand your last post, but based on your first post:

I think the below Macro should work fine to get you started.
MACRO: Get Fields for Key Item in List [Example]

Questions or Issues?

Hey Ray,

Comma-delimited data for this sort of thing tends to be unpleasant to look at and overly difficult to maintain, so I tend to use a tab-delimited text file and set up a table with columns I'm comfortable with.

Like so:

Bookmark Classic Brass, Bookmark Classic Antique Brass	Bookmark_BRASS_Rofin2.ait	Bookmark
Cigar Cutter - Black, Cigar Cutter - White				CigarCutter_ALUM.ait		CCutter
Cufflink A, Cufflink B, Cufflink C						Cufflinks_STEEL_Rofin.ait	Clinks

This sort of table is very easy to maintain using a text editor like BBEdit.

In table above I've associated more than one product with each Template and Template_PDF, and a little bit of RegEx magic makes it easy to find them.

Extracting Fields from a Structured Data Table v1.01.kmmacros (4.6 KB)

As you can see the part of the regular expression you have to deal with in local_RegEx is simple, literal text, and the macro builds the more complex final regular expression for you.

The important thing to remember about the table is that each column must be separated by 1 or more tabs.



Thanks, Chris. I like this approach. Fortunately my current "Switch Action" macro is working. But the ideas everyone has share will help tremendously down the line (as I wrap my head around it all).

Not surprisingly, I'm confused.
Sticking with your example for now; I can only get "Cufflink A" and "Cufflink B" to work without error. Inputing other "Products" present in the txt file produce this error:

This is how my txt file appears in BBEdit:

If I understand the concept "local_RegEx" is the current "Product". I've tweaked the macro to help me understand what's happening:

I am trying to learn about RegEx so I can make sense of the "magic" but it's slow to sink in. Maybe I'm overlooking something simple.


Cufflink C is at the end of the product name followed by the Tab delimiter, rcraighead. But the regex looks for the product name followed by at least one character before the tab.

You can change the .+ after the variable name in the regex to .* to make following characters optional.

(BTW, with such long product names, you might want to construct a pick list to select from so users don't have to type them in.)

1 Like

Thanks so much, Mike.
Unfortunately I'm still getting the error after making the change.

Regarding Product name length: In our workflow the "Product" variable is captured directly from a webpage using KM so the user does not select it manually.

I updated the .txt file and still find only the last line of the tab-delimited file works as expected.


Did you try the Macro posted here:
MACRO: Get Fields for Key Item in List [Example]


Hey Ray,

Pfft! I was sure I'd tested more thoroughly than that...

Sorry, this was my fault.

I've changed the macro above to v1.01 and updated the regular expression.

Oh, I've also changed RegEx from a local to a global variable, so it shows up in the search action.


Thanks, Chris. JMichaelTX had a very similar solution and I was able to combine the two. RegEx is high on my list for study. I realize it is a critical part of editing and maintaining KM Macros.