Docs
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.
Last updated 15 October 2026
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.
{
"$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). |
{ "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. |
{ "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. |
{ "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. |
{ "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. |
{ "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. |
{ "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. |
{ "press": "Enter" }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. |
{ "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. |
{ "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. |
{ "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. |
{ "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. |
{ "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 }. |
{ "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. |
{
"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. |
{ "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. |
{ "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. |
{
"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. |
{
"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. |
{
"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. |
{ "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? }. |
{ "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. |
{ "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. |
{ "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. How flows run: Concepts.