# Action Flows (/features/flows)



A shortcut binding does exactly one thing. A **flow** is the answer to wanting
four, in order, with a condition in the middle: a graph of nodes, wired
together, built by dragging in a window of its own.

Open it from &#x2A;*Flow Editor…** in the tray, or bind a combination to it under
**General**. The list of flows, and the switch that arms each one, is under
**Flows** in Settings.

<Screenshot name="flow-editor" alt="The flow editor: the node palette on the left, a graph running from two triggers through an If to two branches, and the selected node's properties on the right" />

## The one rule [#the-one-rule]

The graph is data-flow, the way n8n's is. A node takes the items that reached it
and returns items on one or more output ports, and &#x2A;*a port that comes back
empty ends that branch.** `If` puts an item on one of its two ports and nothing
on the other, and everything downstream of the empty one is never reached.

There is no separate step or control token. That one rule is what makes `If` —
and, later, `Switch` and `Filter` — the same mechanism seen from different
angles. Edges are Pending, Delivered or Dead; a node whose incoming edges all
came back dead is skipped, and its outputs go dead too.

The canvas draws it after a run: a wire that carried items is solid, one that
went dead is dashed, and a node nothing arrived at is badged **not reached** —
which is a different answer from **failed**, and the Console counts the two
separately.

## Expressions [#expressions]

Node params can hold `{{ … }}`, resolved fresh on every run against:

| Expression                                             | Reads                                                            |
| ------------------------------------------------------ | ---------------------------------------------------------------- |
| `{{ $json.url }}`                                      | a field on the item currently on the wire                        |
| `{{ $node["Get selected text"].json.selection.text }}` | another node's output                                            |
| `{{ $trigger.frontApp }}`                              | what was true when the trigger fired                             |
| `{{$randomUUID}}`, `{{$date("%d.%m.%Y")}}`             | the [snippet variables](/features/variables), arguments included |

A param that can be picked from a list carries a two-way toggle: the list icon
picks a value, and the `{}` icon takes typed text — which is where an expression
goes.

All the snippet variables work **except two**: `{{$clipboardText}}` and
`{{$selectedText}}` are not offered and do not resolve. A flow reads the
clipboard through the **Read clipboard** node and the selection through **Get
selected text** — each declares the capabilities the import review lists, and a
variable doing the same thing quietly would go around both.

## The node catalogue [#the-node-catalogue]

A node is one file plus one line in a list — the editor reads its spec and
draws the inspector, so there is no per-node UI.

<Screenshot name="flow-inspector" alt="A node selected, its inspector open: the name, the params drawn from the node's spec, and the behaviour settings under them" />

### Triggers [#triggers]

| Node               | Runs when                                                                                                        |
| ------------------ | ---------------------------------------------------------------------------------------------------------------- |
| **Manual trigger** | something asks for this flow by name — the tray, a shortcut bound elsewhere, another flow. At most one per flow. |
| **Shortcut**       | a key combination is pressed, wherever you are. A flow may hold several, each starting the run at itself.        |

### Apps [#apps]

| Node               | Does                                                                                                               |
| ------------------ | ------------------------------------------------------------------------------------------------------------------ |
| **Open app**       | Launches an application, or brings it forward if it is running.                                                    |
| **Focus app**      | Switches to an application that is already running — it is never launched. Ports: **Focused** and **Not running**. |
| **Toggle app**     | Hides it when it is in front, shows it when it is not. Has a **Not running** port.                                 |
| **Arrange window** | Moves and resizes the frontmost window to a slot on its display.                                                   |
| **Cycle windows**  | Switches to the next or previous window of one application.                                                        |
| **Frontmost app**  | Describes the application in front at this moment.                                                                 |

### Input [#input]

| Node               | Does                                                      |
| ------------------ | --------------------------------------------------------- |
| **Send keystroke** | Presses a key combination into the frontmost application. |
| **Type text**      | Types text at the caret, without touching the clipboard.  |
| **Media control**  | Play/pause, next or previous track, volume, or mute.      |

### Cursor [#cursor]

| Node                 | Does                                                                    |
| -------------------- | ----------------------------------------------------------------------- |
| **Cursor ripple**    | Draws a ring expanding out of the mouse pointer.                        |
| **Cursor spotlight** | Dims the display except for a circle around the pointer.                |
| **Cursor halo**      | Leaves a translucent circle under the pointer. Runs again to remove it. |
| **Move cursor**      | Moves the pointer to a window or a display.                             |

### Data [#data]

| Node                  | Does                                                                                   |
| --------------------- | -------------------------------------------------------------------------------------- |
| **Set**               | Adds or replaces fields on every item passing through.                                 |
| **Read clipboard**    | Takes what was copied — text, HTML, rich text, files or an image — off the pasteboard. |
| **Write clipboard**   | Puts text, HTML, rich text, files or an image on the pasteboard.                       |
| **Get selected text** | Takes what you have highlighted in whatever is in front.                               |
| **Run script**        | Runs JavaScript over the items on the wire and returns what comes back.                |

### Control, System, Notify, Network [#control-system-notify-network]

| Node                     | Does                                                                 |
| ------------------------ | -------------------------------------------------------------------- |
| **If**                   | Sends each item down one of two branches.                            |
| **System command**       | Lock, screen saver, switch user, sleep, log out, restart, shut down. |
| **Open System Settings** | Opens System Settings at one pane.                                   |
| **Show alert**           | Draws a line of text over whatever is frontmost.                     |
| **HTTP Request**         | Calls an HTTP API or webhook and puts the answer on the wire.        |

## Triggers, and starting a flow by name [#triggers-and-starting-a-flow-by-name]

A flow can have more than one trigger, and they are not spares — each claims its
own combination and starts the run at itself, so one flow can hold a quick path
and a long one without being split in two.

The **Manual trigger** is where everything that asks for the flow by name
arrives: the tray, the Run button, another flow, and a **Run flow** shortcut
bound in the [Shortcuts](/features/shortcuts) pane. That last one is the only
way to give a key to a flow whose entry point is the Manual trigger, and the
picker offers it **only** for flows that have one. The binding stores the flow's
id and nothing else, so renaming the flow cannot break it.

A `Run flow` shortcut whose flow can no longer answer — the Manual trigger
deleted, the flow switched off, the flow deleted — does not go quiet. It draws
the reason on screen and pins it to the row.

## Editing, and Run [#editing-and-run]

The toolbar carries the flow's name, &#x2A;*Open…*&#x2A; and **+ New**, an **Arrange**
that lays the graph out for you, the **On** switch that arms this flow, **Run**,
and **Save**. **Run** executes the flow from the first trigger in the file, and
is never subject to the [Pro](/pro) cap — a flow can always be built and tested.
A run's variables live exactly one run and are gone when it ends.

**Console**, at the foot of the window, is where a run reports itself: how long
it took and `N ok · N failed · N not reached`, then a line per node with the
items in, the items out and the time each took, and which trigger the run
started at.

**Test this node**, in the inspector, runs the selected node on its own against
whatever the last run left on the wire above it. It is not a dry run: anything
the node does, it really does.

A node's **Name** is also its address — `{{ $node['…'] }}` in an expression
reaches it by that name, so renaming a node breaks the expressions pointing at
it. Under the params, **If it fails** decides what a failure costs:

| Choice                    | What happens                              |
| ------------------------- | ----------------------------------------- |
| **Stop the run**          | Nothing after this node runs.             |
| **Carry on**              | The next node gets the input unchanged.   |
| **Down the error branch** | The run leaves by this node's error port. |

Closing the editor with unsaved changes puts up **Save / Don't save / Cancel**.
A clean editor closes straight away.

## Flows are files [#flows-are-files]

Each flow is one file in `flows/` beside `config.json`, so exporting is a copy
and importing is a drop — and a flow that will not parse costs you that flow
rather than every shortcut you own.

Four version numbers keep an older Lumi honest about a file from a newer one,
and they are deliberately separate: the file format (`schemaVersion`), the node
catalogue (`requires.sdk`), each node's own params (`typeVersion`), and the
author's semver (`version`). The `requires` block is worked out from the nodes
on save, never written by hand.

| Opening a flow where…                     | What happens                                                                                                                  |
| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| everything is understood                  | it opens; nodes older than this build are brought forward in memory, nothing written back until you save                      |
| a node or a setting is from the future    | it opens **read-only** and cannot be armed — a flow half of whose steps were silently skipped would do something nobody wrote |
| the file format or the SDK major is newer | refused, naming the release that would open it                                                                                |

Whatever this build does not understand — an unknown node type, field or value
— survives being opened and saved. That is the one promise the format makes.

{/*
  TODO: Imported flows — pulled out to revisit.

  What the section said, and what it still needs before it goes back in:

  - Importing never switches anything on, and the review screen lists what the
    flow is allowed to do — open applications, type, read and write the
    clipboard, read files, reach the network — worked out from the nodes
    actually in the file rather than from anything the file claims about
    itself (`compat.rs`, and `sdk.rs::Host` as the capability list).
  - The clipboard is the one a flow can quietly read something private
    through. A run keeps no payloads unless asked (`Summary` logging is the
    default: timings and outcomes only), and anything a password manager
    marked private is logged as its shape rather than its value.
  - Needs a `flow-import-review` screenshot, and the capability wording
    checked against `Capability` in src/flows/types.ts before publishing.
  */}

## How many can be armed [#how-many-can-be-armed]

The free copy runs three armed **ways into a flow** at once; [Pro](/pro&#x29; runs
all of them. Switching on a fourth is refused, in amber rather than red, with
the sentence &#x2A;"The free plan runs up to 3 ways into a flow at once, and they
are all in use. Turn another flow off, or unlock Pro in Settings → About →
License."* A flow that is switched off costs nothing, so the cap never stops
you writing or keeping flows — only running more than three at a time. The
Flows pane shows which are live.

<Screenshot name="flows-pane" alt="The Flows pane: each flow with Run, Export, Edit, Delete and an arm switch, and a warning saying the free plan's three ways in are all in use" />
