Browse the docs

Variables and expressions

Use {{ }} templates, variables, operators and built-in functions to make a macro's steps depend on data.

Keyboard Maestro: Variables, Tokens, Calculations, Local_, Instance_, Global

A variable holds a value for the steps that come after. An expression works something out from variables and other values. You write an expression between double braces, {{ like this }}, in almost any field of a step.

Templates #

Any text field in a step can contain {{ }}. Switchboard works out each expression just before the step runs and puts the result in.

"Hello, " + name + "!"
→ Hello, Ada!
Sample variableValue
nameAda

Two rules decide what you get back.

  • A field that is only one {{ expression }} keeps the value's type. "{{ n + 1 }}" gives the number 6, not the text "6".

  • A field with other text around the braces is always text. "Total: {{ n + 1 }}" gives "Total: 6".

Write \{{ to put a literal {{ in the text. A {{ with no closing }} stays as it is. Braces inside quoted text in an expression do not end it.

If an expression is wrong, the step fails before it runs and the trace names the field and the position: unknown variable 'x' at 3.

A variable that does not exist is an error, not empty text. Set it first, give it an input default, or use a step's into.

Types #

TypeWritten asNotes
Text"abc" or 'abc'\n, \t and \r are line break, tab and return.
Number42, 3.5, 1e3Whole numbers show without .0.
True or falsetrue, false
NothingnullWhat a missing field gives.
List[1, 2, 3]Counted from 0. list[-1] is the last item.
Recordfrom a step, or json(...)Fields by name: record.name or record["name"]. You cannot write one directly in an expression.

Text that looks like a number acts like one in arithmetic and comparisons. This matters because values from the command line and from web requests arrive as text. Two texts are compared as text.

"5" * 2
→ 10
number("12") + 1
→ 13

Read inside text, a number shows without a trailing .0, true and false show as words, nothing shows as empty, and a list or record shows as compact JSON.

Operators #

From the loosest binding to the tightest:

OperatorsMeaning
a ? b : cIf a is true, b, otherwise c.
||Or. Gives the first value that is true, else the last.
&&And. Gives the first value that is false, else the last.
== !=Equal, not equal.
< <= > >=Order numbers, or two texts.
+ -Add and subtract. + joins text, and joins two lists.
* / %Multiply, divide, remainder.
! -Not, and negative.
. [ ] ( )A field, an item, a function call.
n > 3 && n < 10 ? "in range" : "out of range"
→ in range
Sample variableValue
n7
[1, 2] + [3]
→ [ 1, 2, 3 ]
nickname || name
→ Ada
Sample variableValue
nameAda
nickname

Comparisons work like this. A number equals text that is exactly that number: 5 == "5" is true. Two texts must match exactly, so "1" == "1.0" is false. Lists and records compare by their contents. Ordering something that cannot be ordered, such as a list against a number, is an error.

Division by zero is an error, not infinity.

Use ==, never a single =. Use && and ||, never a single & or |. Switchboard says so if you slip.

Truthiness #

Anywhere a yes or no is needed, such as !, &&, ? : and the If step, a value counts as false if it is:

  • false, null, 0

  • the empty text ""

  • an empty list or an empty record

Everything else is true. That includes the text "0" and the text "false", because they are not empty. Convert first if you need them to mean no: number("0") is 0.

[] || "nothing yet"
→ nothing yet
"0" ? "true" : "false"
→ true

Variables #

A variable is a name that holds a value. Create or change one with the Set variable step, or give another step an into field.

Variables in a few steps
Do
  1. Set price to 12.5
  2. Set qty to 3
  3. Set total to “{{ price * qty }}”
  4. Show “Total: {{ total }}” for 2 s
Open in Switchboard to try it

The Variable step has five operations: set, append, increment, unset and setPath (write inside a record or list). See Set variable.

Scope #

There is one rule. A variable lives only for the current run, unless its name starts with shared.

  • Run-local (total, n, text). Created in the run, gone when it ends. Two runs of the same macro never see each other's.

  • Shared (shared.counter). Kept on this Mac between runs and across macros. Any macro can read and write it. It is not synced to other devices.

A dry run does not save shared variables.

A counter that survives between runs
Do
  1. Add 1 to shared.runs
  2. Show “This macro has run {{ shared.runs }} times”
Open in Switchboard to try it

Inputs #

A macro can declare inputs: named values it expects when something starts it, such as the command line, a URL, a webhook or another macro. Each input has a name, a type (string, number, boolean, choice or file) and an optional default. Inside the macro an input is an ordinary variable with that name.

A macro with an input
Do
  1. Show “Hello, {{ who }}”
Open in Switchboard to try it

Values that arrive as text are converted to the declared type. A number input that cannot be read as a number, or a missing input with no default, fails the run before any step runs and says which input. Values that were passed but not declared are available too.

Keeping a step's result with into #

Many steps give a result, and the ones with an into field store it in the variable you name: Clipboard, Prompt, Shell, AppleScript, JXA, JavaScript, Swift, Python, Run macro, Run shortcut, Read value, HTTP request and others. Later steps read it by that name.

Keep a result with into
Do
  1. Read the clipboard into text
  2. Show “{{ upper(trim(text)) }}”
Open in Switchboard to try it

A step that is only simulated in a dry run still sets its into variable, to its sample or to empty text, so the steps after it can be tried.

A repeat step also sets variables: the one named in forEach for each item, and the one in indexVar for the pass number, counting from 1.

Typing in the Inspector #

A text field in the Inspector is an editor for templates. Type {{ and a list of the values this step can read appears at the caret: inputs, earlier results, the loop's item inside Repeat, the trigger's value and the system tokens. Keep typing to narrow it, press ⏎ or ⇥ to take one, and ⎋ to leave exactly what you typed. Type . after a value to see its fields. Accepting never doubles }}, and only the word at the caret is replaced.

  • Insert (above the field) searches the same values and every function, and writes a whole expression: choose Format date and its format is selected for you to type over.

  • Values that are not safe yet. A result created later in the macro, or only on one branch, is listed under Other values, with the reason it is not offered first. A loop item outside its loop is explained, not offered.

  • Readable and Source. Readable shows each simple {{name}} as a token. Click one (or select it and press ⏎ or F2) to see where it comes from, its type, its properties and an example, then Reveal Source to jump to the step that makes it, Replace it with another value, Transform it (Upper case, Trim... wrap it in the function) or Edit Expression. Each is one change you can undo. Source shows the exact text, which is what the macro stores. The choice is remembered. Tokens never change what is saved, and copying a token gives its {{ }} text.

  • Wrap with Expression. Select some plain text and choose Edit > Wrap with Expression: it becomes a quoted text expression, {{"your text"}}, with the same result, ready to turn into something dynamic. Nothing guesses that the text is a variable.

  • Example result (under the field) works the text out with sample values you type, the last run's values or an input's default, and says which it used. It never runs the step, sends anything, runs a function that has an effect, or reads the clipboard until you press Use clipboard. A value with no sample is named, not guessed.

  • Expand (the arrows at the field's corner) opens the same editor in a larger sheet; ⌘F finds in it.

  • A value that is a whole field, such as {{customer}} alone, keeps its type (a record stays a record). Inside other text it becomes text.

  • A typo or a value that is not available here shows a message under the field with the repair, such as Choose Value. The message says what happens when the macro runs.

Tokens #

A token is a name that reads something from the Mac or the clock, like a variable you never set.

TokenGives
clipboardThe text on the clipboard.
frontAppThe bundle id of the front app.
frontAppNameThe name of the front app.
frontWindowTitleThe title of the front window. Empty without Accessibility.
dateToday's date as yyyy-MM-dd. date("HH:mm", "-1h") also takes a format and an offset.
nowThe current time as seconds since 1970.
uuidA new random id each time.
triggerValueWhat the trigger handed over.

If you set a variable with a token's name, your variable wins.

The trigger value #

{{ triggerValue }} is the most useful piece of the event that started the run. It is the first of these that exists: an explicit triggerValue, value, the file path, the typed string, the window title, the hotkey keys, or the URL or webhook route. A run with nothing to hand over gives empty text.

Say what changed
When
  1. A file is created in ~/Downloads
Do
  1. Show “New file: {{ triggerValue }}”
Open in Switchboard to try it

Functions #

A function is called by name with arguments in brackets: upper("abc"). Names are case-sensitive. A wrong number of arguments or an unknown name is an error, with a suggestion where one is close.

FunctionDoes
upper, lower, trimChange case; remove spaces at the ends.
lengthCharacters in text, items in a list, fields in a record.
replace, matchRegular-expression replace and first match.
split, joinText to list and back.
contains, startsWith, endsWithTest text, a list or a record.
urlEncode, base64, base64Decode, sha256Encode and hash text.
number, stringConvert to a number or to text.
json, jsonTextRead JSON text into a value, and write a value as JSON text.
round, floor, ceil, absRound and take absolute value.
min, maxThe smallest or largest of some numbers, or of a list.
envAn environment variable, or nothing.

The date token above also works as a function with a format and an offset like "+1d", "-2h" or "1w".

To read a date, length of time, number, colour or link out of text, see parseDate, parseDuration, parseNumber, parseColor and parseURL; formatDate, formatDuration and formatColor write them back. find and findFirst (and the Find in text step) pull every date, link, email, number and more out of a block of text.

upper(trim("  hello  ")) + "!"
→ HELLO!
split("a,b,c", ",")[1]
→ b
join(["a", "b", "c"], " - ")
→ a - b - c
replace("2026-10-24", "([0-9]+)-([0-9]+)-([0-9]+)", "$3/$2/$1")
→ 24/10/2026
round(total * 1.2, 2)
→ 23.99
Sample variableValue
total19.99
length(items) > 0 ? items[0] : "empty"
→ first
Sample variableValue
items["first", "second"]
json('{"name": "Ada", "tags": ["a", "b"]}').tags[1]
→ b
max([3, 9, 4]) - min(3, 9, 4)
→ 6

Where expressions work #

  • In the fields of almost every step, as a template.

  • In Repeat, the list in in is a bare expression: items, with no braces, or a {{ }} template. A text is split into lines, and a record yields key and value pairs.

  • Control-flow steps (If, Repeat, Group) leave their inner steps alone until those steps run, so a variable set earlier in the loop is visible to a later step.

Loop over a list
Do
  1. Set names to “{{ split('Ada,Grace,Linus', ',') }}”
  2. Set count to 0
  3. Repeat for each name in names
  4. Add 1 to count
  5. Show “{{ i }}. {{ upper(name) }}” for 0.5 s
  6. Return “{{ count }} names”
Open in Switchboard to try it

A repeat is limited to 10 000 passes. At the limit it stops with an error.

Next steps #