# Dynamic variables (/features/variables)



A replacement is a fixed string unless it holds a name in `{{$…}}`, which is
filled in afresh every time it fires:

| Trigger     | Replacement                    | Comes out as                           |
| ----------- | ------------------------------ | -------------------------------------- |
| `;id`       | `{{$randomUUID}}`              | `611c2e81-2ccb-42d8-9ddc-2d0bfa65c1b4` |
| `;today`    | `{{$date}}`                    | `2026-09-07`                           |
| `;ticket`   | `PT-{{$randomInt}}`            | `PT-482`                               |
| `;nextweek` | `{{$date("%d/%m/%Y", "+7d")}}` | `14/09/2026`                           |

The same grammar works in four places: a **snippet replacement**, and the
[shortcut](/features/shortcuts) actions **Type text**, **Show alert** and **Open
URL**. In a URL the substituted values are percent-encoded and the template's
own `?`, `&` and `:` are left alone, so a variable can safely supply a query
value — or the whole address.

Every one of those fields carries an **Insert variable** button, and typing
`{{` brings the same menu to the caret. It groups the names, completes the one
you are typing and says what it stands for; once the parentheses are open it
shows the signature of the call you are in with the argument you are typing lit,
and offers values for that argument — every date format with the date it would
produce, every offset with the day it lands on. It is the same menu the
[flow](/features/flows) editor uses over node params.

<Screenshot name="insert-variable" alt="Typing {{ in a shortcut's URL field: the completion menu offers $selectedText under a Selection heading, with a line saying what it stands for" />

## The catalogue [#the-catalogue]

Thirty-two generated names, in six groups. Each occurrence is evaluated on its
own, so two `{{$randomUUID}}` in one replacement are two different UUIDs.

### Identifiers [#identifiers]

| Name              | Is                                                 |
| ----------------- | -------------------------------------------------- |
| `{{$randomUUID}}` | A UUID, version 4                                  |
| `{{$ksuid}}`      | 27 characters that sort by the time they were made |
| `{{$nanoId}}`     | 21 characters, safe in a URL                       |

### Date and time [#date-and-time]

| Name                | Is                                 |
| ------------------- | ---------------------------------- |
| `{{$timestamp}}`    | Seconds since 1970                 |
| `{{$isoTimestamp}}` | Now, ISO 8601, UTC                 |
| `{{$date}}`         | Today, as `2026-09-07`, local time |
| `{{$time}}`         | Now, as `14:05`, local time        |

### Text and numbers [#text-and-numbers]

| Name                       | Is                            |
| -------------------------- | ----------------------------- |
| `{{$randomInt}}`           | A whole number from 0 to 1000 |
| `{{$randomAlphaNumeric}}`  | One letter or digit           |
| `{{$randomBoolean}}`       | `true` or `false`             |
| `{{$randomWord}}`          | A single word                 |
| `{{$randomWords}}`         | A handful of words            |
| `{{$randomLoremSentence}}` | A sentence of filler          |
| `{{$randomHexColor}}`      | A colour as `#1E90FF`         |
| `{{$randomColor}}`         | A colour by name              |

### People [#people]

| Name                     | Is                 |
| ------------------------ | ------------------ |
| `{{$randomFirstName}}`   | A given name       |
| `{{$randomLastName}}`    | A family name      |
| `{{$randomFullName}}`    | Both, together     |
| `{{$randomUserName}}`    | A username         |
| `{{$randomJobTitle}}`    | A job title        |
| `{{$randomPhoneNumber}}` | A telephone number |

### Internet [#internet]

| Name                    | Is                          |
| ----------------------- | --------------------------- |
| `{{$randomEmail}}`      | An address at example.com   |
| `{{$randomUrl}}`        | An https address            |
| `{{$randomDomainName}}` | A domain                    |
| `{{$randomIP}}`         | An IPv4 address             |
| `{{$randomIPV6}}`       | An IPv6 address             |
| `{{$randomUserAgent}}`  | A browser user agent string |
| `{{$randomPassword}}`   | A throwaway password        |

### Places and business [#places-and-business]

| Name                       | Is                  |
| -------------------------- | ------------------- |
| `{{$randomCity}}`          | A city              |
| `{{$randomCountry}}`       | A country           |
| `{{$randomStreetAddress}}` | A street and number |
| `{{$randomCompanyName}}`   | A company           |

## Arguments [#arguments]

Seven names take arguments, in parentheses after the name. The parentheses are
optional — a name written without them means what it always meant.

| Written                        | Comes out as                    | Without arguments |
| ------------------------------ | ------------------------------- | ----------------- |
| `{{$randomInt(10,100)}}`       | `73`                            | 0 to 1000         |
| `{{$date("%d/%m/%Y")}}`        | `07/09/2026`                    | `%Y-%m-%d`        |
| `{{$time("%H:%M:%S")}}`        | `14:05:32`                      | `%H:%M`           |
| `{{$date("%Y-%m-%d", "+7d")}}` | `14/09/2026`                    | today             |
| `{{$randomAlphaNumeric(12)}}`  | `k2Bq9zXm4Wtd`                  | one character     |
| `{{$randomWords(5)}}`          | `tenetur enim aut aut sapiente` | three to five     |
| `{{$randomLoremSentence(6)}}`  | six words and a full stop       | four to eleven    |
| `{{$randomPassword(24)}}`      | 24 characters                   | 12 to 16          |

Numbers are written bare, text in double quotes. A date or time format is
[strftime](https://docs.rs/chrono/latest/chrono/format/strftime/index.html).
Counts are capped — 512 characters, 128 words — because whatever comes out is
pasted straight into your document.

An **offset** is a sign, a number and a unit: `+7d`, `-1w`, `+3mo`, `-1y`,
`+2h`, `+30min`, `-45s`. The sign is required. A bare `m` is **not** a unit —
minutes are `min` and months are `mo`. An argument cannot contain `}}`.

A name Lumi does not know is left exactly as written rather than dropped,
because it is a typo far more often than a request — as are arguments it cannot
read. `{{$randomInt(100,10)}}` and `{{$date("%Q")}}` come out just like
that. Falling back to a default would be worse: ask for a number between 10 and
100, get one between 0 and 1000, with no way to tell.

## The two that read the machine [#the-two-that-read-the-machine]

Every name above makes something up. Two more read state, and they are handled
differently:

### `{{$clipboardText}}` [#clipboardtext]

What is on the pasteboard, **as plain text** — a copied image, a copied file or
an empty clipboard all come out as nothing at all. Past 16,384 characters it
declines and the token comes out as you wrote it.

An expansion that used it is **never written to History**, whatever the
pasteboard says about itself, because that is where one-time codes and session
URLs end up. It also stands in a [shortcut's Name field](/features/shortcuts#naming-a-row).

### `{{$selectedText}}` [#selectedtext]

What is selected in the front application. It stands in **Type text**, **Show
alert**, **Open URL** and a shortcut's **Name** — and **not** in a snippet
replacement, which is not an omission: a snippet fires mid-word, when nothing is
selected.

It is read through Accessibility where the app exposes it (native apps, Safari),
and through a synthesised <kbd>⌘</kbd><kbd>C</kbd> where it does not (Chrome,
Electron, terminals). The copy is worth knowing about: &#x2A;*a clipboard manager
will record it.** Lumi's own History never does, but a third-party one has no
way to know the copy was Lumi's.

In a URL, the selected value is percent-encoded unless the field holds
*nothing but* the variable — then it is the URL, taken as it stands, and
narrowed to `https`, `http` and `mailto` because the scheme now comes from
another app's window.

Nothing selected comes out as nothing, and the shortcut still fires. A
**missing Accessibility permission** is different: there the press is refused
and says which permission it needs.

## The Playground [#the-playground]

**Snippets → Playground** renders any of the above in place, by the same code
that runs everywhere else — the fastest way to see what an argument does.

<Screenshot name="variable-playground" alt="The Playground" />
