The flow format
A flow is one markdown file in .reflow/flows/. It combines YAML
frontmatter, prose intent, and named executable steps. Markdown outside the
step fences remains useful context for people and agents, but does not execute
by itself.
Anatomy
Section titled “Anatomy”The canonical form is a ### step heading followed by a bare rfl fence:
---name: Checkout — happy pathurl: /checkoutbrowser: chromiumtags: [smoke, billing]invariants: - 'expect page url~="/confirmation"'budgets: tokens: 50000 minutes: 10---
A signed-in user buys the first product and reaches order confirmation.
### Open checkout
```rflopen /checkout```
### Add the first product to the cart
```rflclick role=listitem nth=0 >> role=button name="Add to cart"expect testid=cart-badge text="1"```
### Complete checkout
```rflclick role=button name="Checkout"expect page url~="/confirmation"snapshot order-confirmation```The parser reads three things:
- Frontmatter —
nameand path-onlyurlare required. The runner composesurlwith the run target’s base URL. - Prose intent — all markdown outside step fences is retained as context.
- Named steps — step names must be unique and run in document order.
You can put the name on the fence when a heading is not convenient:
```rfl step="Open checkout"open /checkout```Step bodies
Section titled “Step bodies”An rfl fence contains deterministic RFL statements.
Every executable line must parse; a malformed RFL line is a flow error rather
than an instruction for an agent.
The earlier ts step="…" fence is still fully supported:
```ts step="the checkout heading is visible"await page.goto(URL);await expect(page.getByRole("heading", { name: "Checkout" })).toBeVisible();```Its body selects one of three legacy execution paths:
- RFL — if every non-empty, non-comment line parses as RFL, the lines run deterministically.
- Playwright TypeScript — otherwise the complete body is compiled as
TypeScript with
page,expect, and the composedURLin scope. - Agent-driven — an empty body asks the agent to satisfy the intent in the
step="…"name and requires a configured provider key.
Do not mix a free-text instruction into a non-empty legacy body. It selects Playwright TypeScript for the whole body; it does not turn only that line into an agent step.
Frontmatter
Section titled “Frontmatter”These are the complete flow frontmatter fields consumed by the batch runner:
| Field | Contract |
|---|---|
name | Required display name. |
url | Required path such as / or /checkout; absolute URLs are not flow URLs. |
browser | chromium, firefox, or webkit. |
budgets.minutes | Wall-clock ceiling for flow work. |
budgets.tokens | Token ceiling for model-assisted work. |
invariants | Deterministic RFL assertions or semantic end-state claims. |
tags | Labels for organizing a suite. |
self_heal | Set to false to opt out of autoheal. |
validate | Set to true to force or false to suppress end-of-run semantic validation. |
needs | Upstream flow slugs for dependency-ordered DAG runs. |
covers | Repository globs used to select affected flows in a DAG run. |
Team environment variables are configured in dashboard settings, not in flow
frontmatter. Reference them as ${VAR} inside executable lines. See
variables for substitution and redaction.
Paths, not origins
Section titled “Paths, not origins”url is always a path. Reflow composes it with the run target, so the same flow
can run against a pull-request preview, staging, or a browser self-target
without edits. An open line may still use an absolute URL when a step
intentionally crosses origins.
Execution targets
Section titled “Execution targets”Run creation accepts exactly one target shape:
target_url: "https://preview.example.com"— run against an application or preview origin.target: { kind: "browser", base_url?: "http://127.0.0.1:3000" }— run on the browser image with no separate application container.base_urldefaults tohttp://127.0.0.1:3000.
Browser targets can use exec "…" in browser and bash "…" in browser.
Read browser self-targets for the complete command and
trust boundary.