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 variable | Value |
|---|---|
name | Ada |
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 #
| Type | Written as | Notes |
|---|---|---|
| Text | "abc" or 'abc' | \n, \t and \r are line break, tab and return. |
| Number | 42, 3.5, 1e3 | Whole numbers show without .0. |
| True or false | true, false | |
| Nothing | null | What a missing field gives. |
| List | [1, 2, 3] | Counted from 0. list[-1] is the last item. |
| Record | from 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" * 210number("12") + 113Read 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:
| Operators | Meaning |
|---|---|
a ? b : c | If 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 variable | Value |
|---|---|
n | 7 |
[1, 2] + [3][
1,
2,
3
]nickname || nameAda| Sample variable | Value |
|---|---|
name | Ada |
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,0the 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"trueVariables #
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.
- Set price to 12.5
- Set qty to 3
- Set total to “{{ price * qty }}”
- Show “Total: {{ total }}” for 2 s
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.
- Add 1 to shared.runs
- Show “This macro has run {{ shared.runs }} times”
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.
- Show “Hello, {{ who }}”
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.
- Read the clipboard into text
- Show “{{ upper(trim(text)) }}”
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.
| Token | Gives |
|---|---|
| clipboard | The text on the clipboard. |
| frontApp | The bundle id of the front app. |
| frontAppName | The name of the front app. |
| frontWindowTitle | The title of the front window. Empty without Accessibility. |
| date | Today's date as yyyy-MM-dd. date("HH:mm", "-1h") also takes a format and an offset. |
| now | The current time as seconds since 1970. |
| uuid | A new random id each time. |
| triggerValue | What 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.
- A file is created in ~/Downloads
- Show “New file: {{ triggerValue }}”
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.
| Function | Does |
|---|---|
| upper, lower, trim | Change case; remove spaces at the ends. |
| length | Characters in text, items in a list, fields in a record. |
| replace, match | Regular-expression replace and first match. |
| split, join | Text to list and back. |
| contains, startsWith, endsWith | Test text, a list or a record. |
| urlEncode, base64, base64Decode, sha256 | Encode and hash text. |
| number, string | Convert to a number or to text. |
| json, jsonText | Read JSON text into a value, and write a value as JSON text. |
| round, floor, ceil, abs | Round and take absolute value. |
| min, max | The smallest or largest of some numbers, or of a list. |
| env | An 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]bjoin(["a", "b", "c"], " - ")a - b - creplace("2026-10-24", "([0-9]+)-([0-9]+)-([0-9]+)", "$3/$2/$1")24/10/2026round(total * 1.2, 2)23.99| Sample variable | Value |
|---|---|
total | 19.99 |
length(items) > 0 ? items[0] : "empty"first| Sample variable | Value |
|---|---|
items | ["first", "second"] |
json('{"name": "Ada", "tags": ["a", "b"]}').tags[1]bmax([3, 9, 4]) - min(3, 9, 4)6Where expressions work #
In the fields of almost every step, as a template.
In Repeat, the list in
inis a bare expression:items, with no braces, or a{{ }}template. A text is split into lines, and a record yieldskeyandvaluepairs.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.
- Set names to “{{ split('Ada,Grace,Linus', ',') }}”
- Set count to 0
- Repeat for each name in names
- Add 1 to count
- Show “{{ i }}. {{ upper(name) }}” for 0.5 s
- Return “{{ count }} names”
A repeat is limited to 10 000 passes. At the limit it stops with an error.