Skip to content

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.

The canonical form is a ### step heading followed by a bare rfl fence:

---
name: Checkout — happy path
url: /checkout
browser: chromium
tags: [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
```rfl
open /checkout
```
### Add the first product to the cart
```rfl
click role=listitem nth=0 >> role=button name="Add to cart"
expect testid=cart-badge text="1"
```
### Complete checkout
```rfl
click role=button name="Checkout"
expect page url~="/confirmation"
snapshot order-confirmation
```

The parser reads three things:

  • Frontmattername and path-only url are required. The runner composes url with 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
```

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 composed URL in 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.

These are the complete flow frontmatter fields consumed by the batch runner:

FieldContract
nameRequired display name.
urlRequired path such as / or /checkout; absolute URLs are not flow URLs.
browserchromium, firefox, or webkit.
budgets.minutesWall-clock ceiling for flow work.
budgets.tokensToken ceiling for model-assisted work.
invariantsDeterministic RFL assertions or semantic end-state claims.
tagsLabels for organizing a suite.
self_healSet to false to opt out of autoheal.
validateSet to true to force or false to suppress end-of-run semantic validation.
needsUpstream flow slugs for dependency-ordered DAG runs.
coversRepository 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.

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.

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_url defaults to http://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.