# Flow JSON reference

> Every field and step of an attention.run flow: selectors, targets, conditions, params, outputs and handlers, generated from the schema the hub validates with.

A flow is a JSON document: a slug, optional params and outputs, optional handlers, and a list of steps. Steps name elements of a library by their slugs; the runner finds them on the screen by their pixels. The hub, the `attn` CLI and the runner check flows against the same schema, which this page is generated from.

Add `"$schema": "https://attention.run/schema/flow-v1.json"` to a flow file and editors that read JSON Schema (VS Code among them) check it as you type.

```json
{
  "$schema": "https://attention.run/schema/flow-v1.json",
  "flow": "acme/export-invoices",
  "name": "Export invoices",
  "library": "acme",
  "params": { "month": { "type": "string", "default": "2026-09" } },
  "outputs": { "exported": { "type": "number" } },
  "defaults": { "timeout": 10000 },
  "handlers": [
    {
      "id": "cookies",
      "when": { "visible": "global/cookie-accept" },
      "do": [{ "click": "global/cookie-accept" }],
      "max": 1
    }
  ],
  "steps": [
    { "call": "acme/sign-in" },
    { "click": "nav/invoices" },
    { "type": "{{params.month}}", "into": "invoices/month", "clear": true },
    { "scroll": { "dy": 400, "until": { "visible": "invoices/export" }, "max": 10 } },
    { "find": "invoices/row", "as": "rows", "all": true },
    { "set": { "exported": "{{vars.rows.count}}" } },
    { "if": { "var": "rows.count", "eq": 0 }, "then": [{ "pass": "nothing to export" }] },
    { "click": "invoices/export", "id": "export" },
    { "waitFor": { "visible": "toast/export-started" }, "timeout": 5000, "optional": true },
    { "assert": { "gone": "invoices/spinner" }, "timeout": 60000 }
  ]
}
```

## The flow

| Field | Type | Required | Limits | What it is |
| --- | --- | --- | --- | --- |
| `$schema` | string |  |  | The schema's URL, https://attention.run/schema/flow-v1.json, so editors check the file as you type. Ignored by the hub. |
| `flow` | string | yes | up to 120 characters | The flow’s slug, such as `fixture/sign-in`: lowercase words and `/`. Pushing a source with a slug that exists saves a new version of that flow. |
| `name` | string |  | up to 200 characters | A readable name. |
| `description` | string |  | up to 5000 characters | What the flow does, for people and for agents choosing a flow. |
| `library` | string |  | up to 60 characters | The library its element slugs resolve in (a slug or an alias). Default: the `default` library. |
| `params` | { [name]: Param } |  |  | Inputs, by name: given with each run (`attn run --param`, the API, a trigger, a `call`). See "Params and outputs". |
| `outputs` | { [name]: Output } |  |  | What a run returns, by name: the var of the same name when the flow ends. See "Params and outputs". |
| `defaults` | { timeout? } |  |  | `timeout`: the timeout of every step that doesn't set its own, in ms (default 10000). |
| `handlers` | Handler[] |  |  | Steps that run whenever a condition becomes true during the run, such as a cookie banner or a "session expired" dialog. See "Handlers". |
| `steps` | Step[] | yes | at least 1 item | The steps, in order. At least one. |

## Steps

Each step is an object with one action key (`click`, `if`, …) and that action’s fields, plus any of the fields every step takes.

| Group | Steps |
| --- | --- |
| Actions | `click`, `doubleClick`, `rightClick`, `hover`, `drag`, `type`, `press`, `navigate`, `scroll` |
| Waits and checks | `waitFor`, `waitStable`, `wait`, `assert` |
| Values | `find`, `read`, `set` |
| Control | `if`, `switch`, `repeat`, `forEach`, `try`, `call`, `pass`, `fail` |

### Fields of every step

| Field | Type | Required | Limits | What it is |
| --- | --- | --- | --- | --- |
| `id` | string |  |  | A stable name for reports, breakpoints and `runs get`; else the step is named by its path (`steps[3]`). Unique in the flow. |
| `name` | string |  | up to 200 characters | A label shown in the editor and the report. |
| `timeout` | integer |  | 0–600000 | How long the step may wait, in ms: for its element, its condition, or the clipboard. Default: the flow's `defaults.timeout`, else 10000. `0` decides on the current frame without waiting. |
| `optional` | boolean |  |  | A failure is recorded but doesn’t fail the run. |
| `retry` | { times, backoffMs? } |  |  | `{ times, backoffMs }`: run the step again after a failure, up to `times` more times, `backoffMs` apart (default 500). |
| `comment` | string |  |  | A note for people; ignored when the flow runs. |
| `disabled` | boolean |  |  | Skip the step. |

## Actions

### click

Waits for the target, then clicks its click point (the element’s, else the centre of the match).

| Field | Type | Required | Limits | What it is |
| --- | --- | --- | --- | --- |
| `click` | Target | yes |  | What to click. |
| `button` | "left" \| "right" \| "middle" |  |  | The mouse button (default `left`). |

```json
{ "click": "login/sign-in" }
```

### doubleClick

Waits for the target, then double-clicks it.

| Field | Type | Required | Limits | What it is |
| --- | --- | --- | --- | --- |
| `doubleClick` | Target | yes |  | What to double-click. |

```json
{ "doubleClick": "invoice/number" }
```

### rightClick

Waits for the target, then right-clicks it.

| Field | Type | Required | Limits | What it is |
| --- | --- | --- | --- | --- |
| `rightClick` | Target | yes |  | What to right-click. |

```json
{ "rightClick": "files/report" }
```

### hover

Waits for the target, then moves the pointer onto it.

| Field | Type | Required | Limits | What it is |
| --- | --- | --- | --- | --- |
| `hover` | Target | yes |  | Where to move the pointer. |

```json
{ "hover": "nav/settings" }
```

### drag

Presses the left button at `from`, moves to `to` and releases.

| Field | Type | Required | Limits | What it is |
| --- | --- | --- | --- | --- |
| `drag` | { from, to } | yes |  | `from` and `to`: where the drag starts and ends. |
| `durationMs` | integer |  | 0–600000 | How long the move takes, in ms. |

```json
{ "drag": { "from": "board/card", "to": "board/done-column" } }
```

### type

Types text. With `into`, clicks the target first. `\n` presses Enter.

| Field | Type | Required | Limits | What it is |
| --- | --- | --- | --- | --- |
| `type` | Text | yes |  | The text (with `{{params.x}}` and `{{vars.x}}` placeholders), or `{ "param": "name" }` for a flow param. |
| `into` | Target |  |  | Click this first, to focus it. |
| `clear` | boolean |  |  | Select all and delete what the field holds before typing. |

```json
{ "type": { "param": "user" }, "into": "login/username" }
```

### press

Presses a key or a chord.

| Field | Type | Required | Limits | What it is |
| --- | --- | --- | --- | --- |
| `press` | string | yes |  | A key or chord: `Enter`, `Tab`, `Control+A`, `Meta+Shift+P`. `ControlOrMeta` is Command on macOS targets and Control elsewhere. |

```json
{ "press": "Enter" }
```

### navigate

Opens a URL. Chrome targets only.

| Field | Type | Required | Limits | What it is |
| --- | --- | --- | --- | --- |
| `navigate` | string | yes |  | The URL, with placeholders. |

```json
{ "navigate": "https://example.com/login" }
```

### scroll

Scrolls by `dx` / `dy` pixels; with `until`, again and again until the condition holds.

| Field | Type | Required | Limits | What it is |
| --- | --- | --- | --- | --- |
| `scroll` | { at?, dx?, dy?, until?, max? } | yes |  | `at`: where the wheel turns (default: the centre of the screen); `dx`, `dy`: pixels per scroll; `until`: a condition that ends it; `max`: the most scrolls with `until`. |

```json
{ "scroll": { "dy": 400, "until": { "visible": "invoices/export" }, "max": 10 } }
```

## Waits and checks

### waitFor

Waits until a condition holds, on frames from after the last action. Times out with TIMEOUT.

| Field | Type | Required | Limits | What it is |
| --- | --- | --- | --- | --- |
| `waitFor` | Condition | yes |  | The condition. |
| `stableFor` | integer |  | 0–60000 | The condition must keep holding for this long, in ms, before the wait ends. |

```json
{ "waitFor": { "visible": "welcome/heading" }, "timeout": 5000 }
```

### waitStable

Waits until nothing changes in `region` (default: the whole screen) for `forMs`.

| Field | Type | Required | Limits | What it is |
| --- | --- | --- | --- | --- |
| `waitStable` | { region?, forMs } | yes |  | `region` (a rect, optional) and `forMs`. |

```json
{ "waitStable": { "forMs": 500 } }
```

### wait

Pauses for a fixed time, in ms. The validator warns about it: wait for what you expect to see instead.

| Field | Type | Required | Limits | What it is |
| --- | --- | --- | --- | --- |
| `wait` | integer | yes | 0–600000 | Milliseconds. |

```json
{ "wait": 1000 }
```

### assert

Like `waitFor`, but a timeout fails the step with ASSERTION and the message.

| Field | Type | Required | Limits | What it is |
| --- | --- | --- | --- | --- |
| `assert` | Condition | yes |  | The condition. |
| `message` | string |  |  | What the failure says. |

```json
{ "assert": { "gone": "login/sign-in" }, "message": "still on the sign-in page" }
```

## Values

### find

Waits for an element and stores what it found in a var: its position, size, score and state (the variant’s label). With `all`, every instance in reading order.

| Field | Type | Required | Limits | What it is |
| --- | --- | --- | --- | --- |
| `find` | Selector | yes |  | The element. |
| `as` | string | yes |  | The var to store it in. |
| `all` | boolean |  |  | Store every instance: `{ found, element, count, items }`. |

```json
{ "find": { "element": "settings/toggle", "index": 0 }, "as": "toggle" }
```

### read

Reads text into a var: the screen’s text in an area, by OCR on the runner (`{ "text": area }`), or the target’s clipboard (`clipboard`: the text copied after the last action started, so never what was there before the copy). The area is an element (the part its mask ignores, else all of it), `{ "element", "rect" }` (a rect from its top-left; x and y may be negative), `{ "var", "rect"? }` (a found item’s box, or a rect from it) or `{ "rect" }` (screen pixels). A screen read waits until the area holds still.

| Field | Type | Required | Limits | What it is |
| --- | --- | --- | --- | --- |
| `read` | "clipboard" \| { text } | yes |  | The source: `{ "text": area }` or `clipboard`. |
| `as` | string | yes |  | The var to store the text in. |
| `trim` | boolean |  |  | Clipboard only: strip whitespace at both ends. |
| `matches` | string |  |  | Screen text only: a regular expression the text must match. The read waits for it, and with a group the var is the group’s text. |

```json
{
  "read": {
    "text": { "element": "invoice/number-label", "rect": { "x": 90, "y": 0, "width": 160, "height": 20 } }
  },
  "as": "invoiceNo",
  "matches": "INV-(\\d+)"
}
```

### set

Sets vars. A value that is exactly one placeholder copies that value with its type; anything else becomes text.

| Field | Type | Required | Limits | What it is |
| --- | --- | --- | --- | --- |
| `set` | { [name]: string } | yes |  | Var names and their values. |

```json
{ "set": { "greeting": "Hello {{params.user}}", "first": "{{vars.rows.items.0}}" } }
```

## Control

### if

Checks the condition once, now, and runs `then` or `else`.

| Field | Type | Required | Limits | What it is |
| --- | --- | --- | --- | --- |
| `if` | Condition | yes |  | The condition. |
| `then` | Step[] | yes |  | Steps when it holds. |
| `else` | Step[] |  |  | Steps when it doesn’t. |

```json
{ "if": { "visible": "login/error" }, "then": [{ "fail": "Sign-in was refused" }] }
```

### switch

Waits for the first case whose condition holds (in order, on the same frame) and runs its `do`. At the timeout, runs `else`, or fails with TIMEOUT without one.

| Field | Type | Required | Limits | What it is |
| --- | --- | --- | --- | --- |
| `switch` | { when, do }[] | yes | at least 1 item | The cases: `{ when, do }`. |
| `else` | Step[] |  |  | Steps when no case held by the timeout. |

```json
{
  "switch": [
    { "when": { "visible": "welcome/heading" }, "do": [{ "pass": "signed in" }] },
    { "when": { "visible": "verify/code" }, "do": [{ "call": "fixture/verify" }] }
  ]
}
```

### repeat

Runs `do` a number of `times`, `until` a condition holds (checked after each pass) or `while` it holds (checked before). `max` caps `until` and `while` (default 100); going over fails the step.

| Field | Type | Required | Limits | What it is |
| --- | --- | --- | --- | --- |
| `repeat` | { times?, until?, while?, max?, as? } | yes |  | One of `times`, `until`, `while`; `max`; `as`, a var with the pass number (0, 1, …). |
| `do` | Step[] | yes |  | The steps to repeat. |

```json
{
  "repeat": { "until": { "gone": "list/spinner" }, "max": 20 },
  "do": [{ "scroll": { "dy": 300 } }]
}
```

### forEach

Runs `do` once for each item of a list var, such as `find … all`’s items.

| Field | Type | Required | Limits | What it is |
| --- | --- | --- | --- | --- |
| `forEach` | { in, as } | yes |  | `in`: the var holding the list; `as`: the var each item is in. |
| `do` | Step[] | yes |  | The steps. |

```json
{
  "forEach": { "in": "rows", "as": "row" },
  "do": [{ "click": { "element": "row/edit", "within": "row" } }]
}
```

### try

Runs `try`; a step failure there runs `catch` (with `vars.error`: its code, message and path) instead of failing the run. `finally` runs after both.

| Field | Type | Required | Limits | What it is |
| --- | --- | --- | --- | --- |
| `try` | Step[] | yes |  | The steps to try. |
| `catch` | Step[] |  |  | Steps after a failure. |
| `finally` | Step[] |  |  | Steps that always run after. |

```json
{ "try": [{ "click": "dialog/close", "timeout": 2000 }], "catch": [{ "press": "Escape" }] }
```

### call

Runs another flow inline, on the same screen, with its own params, vars and handlers. Its `pass` returns here; its failure fails this step. At most 16 calls deep.

| Field | Type | Required | Limits | What it is |
| --- | --- | --- | --- | --- |
| `call` | string | yes | up to 120 characters | The called flow’s slug. |
| `version` | integer |  | at least 1 | A version of it (default: its latest version when the run started). |
| `params` | { [name]: Value } |  |  | Its params. Strings take placeholders; exactly one placeholder keeps the value’s type. |
| `as` | string |  |  | A var for its result: `{ status, outputs, message?, error? }`. |

```json
{ "call": "fixture/sign-in", "params": { "user": "{{params.user}}" }, "as": "signin" }
```

### pass

Ends the run as passed, with this message.

| Field | Type | Required | Limits | What it is |
| --- | --- | --- | --- | --- |
| `pass` | string | yes |  | The run’s message. |

```json
{ "pass": "no invoices to export" }
```

### fail

Fails the step with this message (FAILED), which fails the run unless something absorbs it (`optional`, `try`).

| Field | Type | Required | Limits | What it is |
| --- | --- | --- | --- | --- |
| `fail` | string | yes |  | The error message. |

```json
{ "fail": "Sign-in was refused" }
```

## Waiting and matching

Every step that acts on an element waits for it first, up to the step’s timeout. A wait after an action only looks at frames from after that action, so a dialog that was already on screen doesn’t end the wait for the next one.

An element that matches in more than one place fails with AMBIGUOUS, and the error lists the places: pick one with `index`, `within` or `region`, or cut a larger element.

## Selectors

A selector names an element: its slug as a string, or an object with the slug and narrowing fields.

| Field | Type | Required | Limits | What it is |
| --- | --- | --- | --- | --- |
| `element` | string | yes |  | The element’s slug or one of its aliases. |
| `variant` | string |  |  | Only this variant, by its label (a state such as `on`, `checked`) or id. |
| `index` | integer |  | at least 0 | The nth instance in reading order, from 0, for elements that appear several times. |
| `within` | string |  |  | Only instances inside a found item in the vars (`row`, `rows.items.2`), such as the Edit icon in this row. |
| `region` | Rect |  |  | Only instances inside this screen rect: `{ x, y, width, height }`. |

## Targets

Actions take a target: where the pointer goes.

| Form | What it is |
| --- | --- |
| `"login/sign-in"` | An element (or a selector object): its click point. |
| `{ "element": "login/username", "offset": { "dx": 120, "dy": 30 } }` | A point at an offset from the element’s top-left corner. |
| `{ "x": 640, "y": 360 }` | Screen coordinates. No waiting: use them for what has no element. |
| `{ "var": "row", "offset": { "dx": 8, "dy": 8 } }` | A found item in the vars: its centre, or an offset from its top-left. No waiting either. |

## Conditions

`waitFor`, `assert`, `if`, `switch`, `repeat`, `scroll.until` and handlers take a condition. The value tests of `var` and `param` are `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `matches`, `exists` (at most one).

| Condition | What it means | Example |
| --- | --- | --- |
| `visible` | The element is on screen. | `{ "visible": "welcome/heading" }` |
| `gone` | The element is not on screen. | `{ "gone": "list/spinner" }` |
| `screen` | The library’s screen is up: every element its signature says shows is on screen, and none it says doesn’t. | `{ "screen": "checkout/payment" }` |
| `any` | At least one holds; `waitFor` reports which (`which`). | `{ "any": [{ "visible": "welcome/heading" }, { "visible": "login/error" }] }` |
| `all` | Every one holds on the same frame. | `{ "all": [{ "visible": "a" }, { "gone": "b" }] }` |
| `not` | The condition doesn’t hold. | `{ "not": { "visible": "toast/error" } }` |
| `stable` | Nothing changed in `region` (default: the screen) for `forMs`. | `{ "stable": { "forMs": 300 } }` |
| `changed` | Some pixel changed in `region` (default: the screen) after the last action. | `{ "changed": {} }` |
| `var` | A var (a dotted path) compared with one test, or truthy without one. Decided from the run’s data, without looking at the screen. | `{ "var": "toggle.state", "eq": "on" }` |
| `param` | A flow param, compared or truthy, like `var`. | `{ "param": "retry", "eq": true }` |

## Placeholders

Strings in steps can name values with placeholders: `{{params.user}}` (a param), `{{vars.row.cx}}` (a var, with dotted paths into objects and lists such as `{{vars.rows.items.0.state}}`), and `{{run.id}}`, `{{run.flow}}`, `{{run.version}}`, `{{run.target}}`. Objects interpolate as JSON. There is no expression language: arithmetic and parsing belong in a script.

The validator checks that each placeholder names a declared param, a var some step sets, or a run field.

## Params and outputs

Params are inputs: `{ "type": …, "default"?: …, "description"?: … }`. Types are `string`, `number`, `boolean`, `json` (any JSON value). Text spells the other types, so `--param count=3` fills a number param: `"42"` and `"-1.5"` are numbers, `"true"` and `"false"` booleans. A param without a default is required; a run with an unknown, missing or unconvertible param is refused.

Outputs are what a run returns: `{ "type"?: …, "default"?: …, "description"?: … }` (no type: any JSON). An output is the var of the same name when the flow ends, so `set`, `find`, `read` and `call … as` write them. A run that passes must have every declared output, of its type; failed runs keep the ones that were set. At most 64 KB as JSON. Runs show them in `attn run --wait`, `GET /runs/:id`, webhooks and triggers (`{{event.run.outputs.name}}`).

## Handlers

Handlers deal with what can appear at any time: `{ "id", "when", "do", "max"? }`. Whenever `when` becomes true during the run, the next safe point (a wait, or between steps; never in the middle of an action) runs `do`, then the interrupted wait goes on. `max` is how many times it may run (default 3). A `fail` in a handler fails the run.

## Results and errors

A run ends `passed`, `failed` (a step failed, or `fail`), `error` (the target or the setup broke) or `cancelled`. A failed step’s error has a code (TIMEOUT, AMBIGUOUS, NOT_FOUND, CAPABILITY, CONNECTOR, CANCELLED, ASSERTION, FAILED, INVALID), a message, the frame at the failure and the best candidate the matcher saw, such as the element at 0.71 against a threshold of 0.86.

## Validation

Before a flow is saved (and before `attn flows push` sends it) it is checked: the schema, with an error per step and its path (`steps[3].click`); element slugs against the library (unknown ones are errors, proposed ones warnings); placeholders; params and outputs; calls (the flow exists, takes the params given, and no call leads back to the caller). Warnings: a `wait`, a `type` with no `into` before any click, steps after `pass` or `fail`.

The JSON Schema: [https://attention.run/schema/flow-v1.json](https://attention.run/schema/flow-v1.json). How flows run: [Concepts](https://attention.run/docs/concepts#flows).

---

Canonical: https://attention.run/docs/flow-json · Updated 2026-10-15
