RFL — the Reflow Language
Between your plain-language steps and the browser sits RFL (the Reflow Language): a small, human-readable, deterministic step language. Agents write it; humans review it in diffs. It exists so a flow can run without a model in the loop when nothing changed, and so any step can be regenerated against a live page when something did.
The rule that makes it work
Section titled “The rule that makes it work”If a line parses as RFL, it executes deterministically — fast, no model. If it doesn’t parse, it is evaluated as free text by the agent.
RFL is a fast path, not a gate. An agent (or a human in a hurry) can always
write dismiss whatever cookie popup appears as a step line; the run still
works — the agent interprets it against the live page, at model speed and
model cost. Over time agents converge free-text lines into RFL so runs get
faster and cheaper. The run trace marks which lines ran on which path.
Why a language at all
Section titled “Why a language at all”Natural language is the right interface for intent, but it isn’t deterministic. Raw Playwright is deterministic but brittle and hostile to review. RFL is the contract between the two:
- Deterministic — one RFL program resolves to one Playwright execution. Same page, same result.
- Covers Playwright’s interaction surface — every kind of interaction Playwright supports has an RFL construct (not every argument: the 20% of surface that covers ~all real flows).
- Regenerable — when an anchor stops matching, the agent re-reads the plain-language step and emits new RFL for just that step.
- Readable & diff-friendly — verbs read like English, one action per line, stable formatting. PR deltas stay reviewable.
Where it lives
Section titled “Where it lives”In fenced blocks under each plain-language step, maintained by the agent — committed, versioned, diffed:
## Steps1. Add the first product on the page to the cart
```rfl click role=listitem nth=0 >> role=button name="Add to cart" | click testid=product-grid >> text="Add to cart" expect testid=cart-badge text="1" ```The language
Section titled “The language”One statement per line: verb target [value] [modifiers]. A # outside
quotes starts a comment (full-line or trailing). Anything that doesn’t parse
is a free-text step (see the rule above).
Navigate
Section titled “Navigate”open /checkout # path, relative to the run's target urlopen https://status.example.com # absolute urls allowed for cross-origin stepsreloadgo backgo forwardPointer
Section titled “Pointer”click role=button name="Pay now"click role=row name~="Invoice 42" button=right # context menuclick testid=canvas at=120,80 # position within elementdblclick testid=row-42hover role=menuitem name="Account"drag testid=card-3 to testid=column-donescroll role=main to=bottom # top|bottom|into-viewtap role=button name="Menu" # touch devicesKeyboard & focus
Section titled “Keyboard & focus”fill label="Email" $inbox # clears, then enters valuetype role=textbox name="Search" "wireless headphones" # appends, key by keypress role=textbox name="Search" Enterpress page Control+K # page-level chordfocus label="Promo code"blur label="Promo code"Forms & files
Section titled “Forms & files”select label="Country" "United Kingdom"check label="Save this card"uncheck label="Subscribe to updates"upload label="Attachment" fixtures/receipt.pdfFrames, tabs, dialogs, downloads
Section titled “Frames, tabs, dialogs, downloads”# iframes join the target chain like any other scopefill frame title~="Payment" >> label="Card number" "4242 4242 4242 4242"
# native dialogs: handling applies to the next action onlyon dialog acceptclick role=button name="Delete account"
# a click that opens a new tabclick role=link name="Open invoice" opens=tabswitch tab last # subsequent lines target that tabswitch tab main
# downloadsclick role=button name="Export CSV" opens=downloadexpect download filename~=".csv"Assertions
Section titled “Assertions”expect <target> <condition> — or wait for as a synonym when the
condition is expected to become true:
expect role=status name="cart count" text="1"expect role=alert hiddenexpect testid=price-total value="£42.00"expect role=listitem count=3expect label="Email" attr.aria-invalid="false"expect label="Promo code" focusedexpect role=textbox name="Notes" emptyexpect page url~="/order/"expect page title="Order confirmed"wait for role=progressbar hidden timeout=30sElement conditions: visible (default) · hidden · attached · enabled ·
disabled · editable · checked · unchecked · focused · empty ·
text="…" (exact) · contains="…" · value="…" · count=N ·
class~="…" · attr.<name>="…".
Page conditions: url= · url~= · title= · title~=.
Targets
Section titled “Targets”A target is a chain of anchors joined by >> (outer scope → inner):
click role=dialog name="Checkout" >> role=button name="Pay now"| Anchor | Playwright equivalent |
|---|---|
role=button name="Pay" | getByRole('button', { name: 'Pay' }) |
label="Email" | getByLabel('Email') |
placeholder="Search…" | getByPlaceholder('Search…') |
text="Add to cart" | getByText('Add to cart') |
alt="Logo" | getByAltText('Logo') |
title="Close" | getByTitle('Close') |
testid=cart-badge | getByTestId('cart-badge') |
frame title~="Payment" | frameLocator(…) |
css=.legacy-grid | locator('.legacy-grid') — last resort |
Modifiers: nth=N · name~="…" / text~="…" (substring) · exact ·
timeout=10s · optional (no-op if the target never appears — cookie
banners) · button=left|right|middle · at=x,y · opens=tab|download.
Fallback chains
Section titled “Fallback chains”Continuation lines starting | are ordered alternatives. The compiler tries
each whole statement until one resolves; if none do, the step deviates
and the agent regenerates it from the plain-language step:
click role=button name="Add to cart" | click role=menuitem name="Add to cart" | click testid=product-grid >> text="Add to cart"Heals are cheap because the agent usually prepends the new reality and keeps old anchors as history.
Variables
Section titled “Variables”$NAME resolves from flow variables: (set per-team in the dashboard,
encrypted, never logged). Substitution applies anywhere a value appears —
fill values, anchor values (label=$FIELD_NAME), paths. Builtins: $inbox
(this run’s email address), $run.id, $target (the run’s base URL).
Small print
Section titled “Small print”- Values are a quoted string (
"…",\"escapes), a$variable, or a single bare token with no spaces and no=(paths, key names). To clear a field:fill label="Promo code" "". - Multiple conditions on one
expectline are AND-ed. - A bare
expect <target>assertsvisible. timeout=takes30sor500ms.at=x,yis relative to the element’s top-left.switch tabtakesmain,last, or a 1-based index.- A
#outside quotes starts a comment, full-line or trailing.
Grammar
Section titled “Grammar”The reference parser lives at packages/rfl in the Reflow repo, and the
documentation is executable: every RFL example on this page is parsed in CI
against that parser. If the docs and the language disagree, the build fails.
block = [ "rfl" INT ] line*line = comment | statement | "|" statement (* "|" = fallback *)
statement = nav | pointer | keys | forms | meta | assertionnav = "open" value | "reload" | "go back" | "go forward"pointer = ("click"|"dblclick"|"hover"|"tap") target | "drag" target "to" target | "scroll" targetkeys = ("fill"|"type") target value | "press" ("page"|target) value | ("focus"|"blur") targetforms = "select" target value | ("check"|"uncheck") target | "upload" target valuemeta = "on dialog" ("accept"|"dismiss") [ "text~=" STRING ] | "switch tab" ("main"|"last"|INT)assertion = ("expect"|"wait for") ("page"|"download"|target) condition*
target = anchor ( ">>" anchor )*anchor = KIND ["~"] "=" value param* | "frame" param*KIND = "role"|"label"|"placeholder"|"text"|"alt"|"title"|"testid"|"css"param = ("name"|"name~"|"title"|"title~"|"url~") "=" value | "nth=" INT | "exact"
condition = FLAG | CONDKEY ["~"] "=" value | "attr." NAME "=" valueFLAG = "visible"|"hidden"|"attached"|"enabled"|"disabled"|"editable" | "checked"|"unchecked"|"focused"|"empty"CONDKEY = "text"|"contains"|"value"|"count"|"class~" (* element *) | "url"|"title" (* page *) | "filename" (* download *)
value = STRING | "$" NAME | BAREWORDmodifiers = ("timeout"|"button"|"at"|"opens"|"to") "=" value | "optional"Statement modifiers may appear anywhere after the verb; anchor params bind to the anchor they follow; anything else ends the deterministic parse and the line becomes free text.
Determinism contract
Section titled “Determinism contract”- Strict resolution. An anchor chain must match exactly one element.
Zero or many (without
nth=) is a deviation — never a silent pick. - Ordered fallbacks. First uniquely-resolving alternative wins; the chosen branch is recorded in the trace.
- Playwright actionability. Actions inherit Playwright’s auto-waiting. There are no sleeps in RFL.
- Pure compilation. RFL → Playwright involves no model and no network. Models are only involved in writing RFL (from your plain language) and in executing free-text lines.
- Parse or free text. A line either parses and runs deterministically, or it is a free-text step executed semantically. Nothing in between, and the trace says which happened.
- Versioned. A block may open with
rfl 1. Grammar changes bump the version; old blocks keep compiling.
Lifecycle
Section titled “Lifecycle”plain-language step (intent — yours or your agent's) │ model, once ▼RFL block (agent-maintained, committed, diffable) │ pure function, every run ▼Playwright execution (real browser, no model in the loop)
free-text line ──────────────► agent executes semantically (slower path), then proposes RFL for next timeA fully cached green run costs one model call — the final intent check.