Write a flow
A flow is a Markdown document saved in Reflow or in your repository, run with reflow plan and
reflow apply. 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.
Choose where new tests are saved during onboarding or in Team settings. The syntax, dependencies, plugin bindings and execution workflow are the same in both locations. Repository examples use .reflow/flows/; shared tests are created through manage_flows or reflow flows create.
Walk through a checkout flow
Section titled “Walk through a checkout flow”Scroll through the explanation to follow each part of checkout.md. This example
uses a seed-customer dependency and a private Browser connection named site.
Your agent can adapt the journey, setup flow and selectors to your application.
Name the behavior you want to check.
A flow is a Markdown test saved in Reflow or in your repository. Give it a useful name and a starting path. The path is combined with the target you select, so the test can follow your app from localhost to a PR preview.
Lines 1–3---
name: Checkout with a discount
url: /checkout Declare what must happen first.
This checkout depends on seed-customer, a separate flow that prepares its test customer. Reflow puts that dependency in the plan and runs it first. If it cannot complete, checkout is blocked. self_heal and validate are off so this flow uses deterministic checks.
Lines 4–6needs: [seed-customer]
self_heal: false
validate: false Choose the commands your test can use.
The site alias adds Browser commands to this flow. It uses the private connection named site on the machine running the test. Your agent selects the plugin’s exact version, keeping repeat runs on the same command schema.
Lines 7–12providers:
site: Browser
plugins:
- name: Browser
sourceCommit: 5852a91f8ba9f4c73585a5984a96117a159c068f
descriptorDigest: c86d7af94675fed13771b48b5f75f60a4988a2cf4158dd6d211e0f651006e505 Make the values explicit.
promoCode is a string input with a default. Supply a different value through your target’s input file, without rewriting the journey. Private connection settings and credentials stay outside the flow.
Lines 13–15inputs:
promoCode: {type: string, default: QA10}
--- Write steps in your application’s language.
The heading explains the intent. Commands inside the rfl fence execute in order. Here, the browser opens checkout and enters the declared promo code. Replace these selectors with the controls in your app.
Lines 16–22
### Apply the customer’s discount
```rfl
site.open url="/checkout"
site.fill selector="[data-testid=promo]" value="${inputs.promoCode}"
site.click selector="[data-testid=apply-promo]" Assert the outcome. Capture the result.
Check that the customer sees the discount, then capture the screen. The assertion checks a business outcome; the named snapshot gives your agent a visual to compare and bring into PR review.
Lines 23–25site.expect selector="[data-testid=discount]" text="10%"
site.snapshot name="discount-applied"
``` ---name: Checkout with a discounturl: /checkoutneeds: [seed-customer]self_heal: falsevalidate: falseproviders: site: Browserplugins: - name: Browser sourceCommit: 5852a91f8ba9f4c73585a5984a96117a159c068f descriptorDigest: c86d7af94675fed13771b48b5f75f60a4988a2cf4158dd6d211e0f651006e505inputs: promoCode: {type: string, default: QA10}--- ### Apply the customer’s discount ```rflsite.open url="/checkout"site.fill selector="[data-testid=promo]" value="${inputs.promoCode}"site.click selector="[data-testid=apply-promo]"site.expect selector="[data-testid=discount]" text="10%"site.snapshot name="discount-applied"``` Database steps and variables
Section titled “Database steps and variables”Use your business data alongside browser checks. This flow captures a repeatable
selection of orders; orderLimit is a typed input with a default:
---name: Inspect ordersproviders: db: PostgreSQLinputs: orderLimit: {type: integer, default: 10}plugins: - name: PostgreSQL sourceCommit: 5852a91f8ba9f4c73585a5984a96117a159c068f descriptorDigest: 52bdb147746c6fea6fe1f11aa839b05c7ee0eb9a9f97619380ae38a143f72afe---
### Capture recent orders
```rfldb.query sql="SELECT id, status FROM orders ORDER BY id LIMIT ${inputs.orderLimit}" key="id" checkpoint="orders"```db uses the private connection with the same name. key="id" uses a unique, non-null column to give the rows a stable order.
The retained results let your agent inspect which order values changed.
To override the default, save {"orderLimit": 25} in a private JSON input file
and supply it when selecting your target:
reflow target --group orders --bindings /absolute/path/to/connections.json --inputs /absolute/path/to/inputs.jsonreflow planreflow applyUse numeric or boolean inputs for values such as limits and feature switches. Do not concatenate untrusted text into SQL. See variables for typed references and outputs from upstream flows.
Generate values and pass them between flows
Section titled “Generate values and pass them between flows”Use capture to give a value a name. Later statements can read it; a flow that
lists this flow in needs can read its saved outputs. Receipts retain captures as
strings; declared types restore numbers when consumed. Use number(...) to
convert a text capture before arithmetic.
For example, .reflow/flows/seed-order.md generates test data without opening a
browser or connecting to a plugin:
---name: Choose order test datainputs: unitPrice: {type: number, default: 25}---
### Generate the order
```rflcapture suffix = random.string length=12capture quantity = random.integer min=1 max=5capture orderId = "test-${suffix}"capture total = expression "number(${quantity}) * number(${inputs.unitPrice})"```The consumer declares the dependency and the outputs it reads. This
.reflow/flows/check-order.md example prints a summary through LocalBash; replace
the command with your application’s check when adapting it:
---name: Check the generated orderneeds: [seed-order]providers: worker: LocalBashreferences: needs.seed-order.outputs.orderId: {type: string} needs.seed-order.outputs.quantity: {type: string} needs.seed-order.outputs.total: {type: string}plugins: - name: LocalBash sourceCommit: 5852a91f8ba9f4c73585a5984a96117a159c068f descriptorDigest: c00f906e9113dc65cdcc314cfc0cc7464bbaad5780e6a69bcb1872517fb78d98---
### Show the order values
```rflworker.bash command="printf 'Order %s / quantity %s / total %s\\n' '${needs.seed-order.outputs.orderId}' '${needs.seed-order.outputs.quantity}' '${needs.seed-order.outputs.total}'"```The generated values above have a fixed safe format. Shell and SQL interpolation is text substitution, not argument escaping or query parameterization; do not insert arbitrary user text this way. Keep credentials in private bindings and avoid capturing sensitive input values: captures become shared run evidence.
Run the consumer; Reflow includes its dependency:
reflow plan .reflow/flows/check-order.mdreflow apply .reflow/flows/check-order.mdThe generated suffix and quantity belong to this flow’s named test data in the
selected execution group. Re-planning, new applies, retries, browser replay and
reflow taint seed-order preserve them. Changing unitPrice recomputes total
without choosing another quantity. Renaming a generated capture creates a new
identity; changing its generator options requires a reset or a new name.
To choose fresh data for the whole selected group, run reflow reset, then plan
and apply again. Reset also replaces the group’s named mailboxes; it does not
remove application records. Separate setup flows remain useful for expressing
shared dependencies, but are not needed merely to keep random values stable.
The producer must succeed and provide the named output before a consumer can run.
See the variable reference for exact scope and
Mailbox plugin for reusable test accounts.
Configure plugins once per flow
Section titled “Configure plugins once per flow”Aliases belong in frontmatter; executable commands belong in the steps:
providers: db: PostgreSQL shell: LocalBash ui: {use: Browser, binding: site}The short form uses the alias as its private connection name. Use binding only
to select a differently named connection, as ui does above. Declare each plugin’s
exact version in plugins; your agent can select the pins with list_plugins.
Keep connection strings, working directories and credentials in the private JSON
file passed to reflow target --bindings. The flow stays portable: each teammate
supplies their own connections without editing the test.
LocalBash adds shell commands; DockerUbuntu runs commands inside an existing Ubuntu container. PostgreSQL captures database rows and can prepare test data. A custom plugin can add commands and comparisons that express your business rules. See writing a plugin.
Frontmatter reference
Section titled “Frontmatter reference”| Field | Purpose |
|---|---|
name | Display name of the flow. |
providers | Aliases for plugins and their private connection names. |
plugins | Plugin name, source commit and descriptor digest from the installed catalog. |
inputs | Typed parameters with optional defaults. |
references | Types of values read from other flows. |
url | Starting path for a browser flow, such as /checkout. |
needs | Flow slugs that must complete before this flow runs. |
prerequisites | Checks and optional setup before the steps. |
covers | Repository globs used to select flows affected by a change. |
tags | Labels for organizing flows. |
self_heal | Optional model-assisted runner: false disables in-run selector repair. Your coding agent can still repair and rerun. |
validate | Enable or disable model-assisted end-of-run validation. |
invariants | Assertions checked at the end of a run. |
browser | Browser engine; local browser flows use Chromium. |
device | Playwright device preset for browser emulation. |
budgets.minutes | Maximum duration. |
budgets.tokens | Token limit for model-assisted execution. |
Run against different environments
Section titled “Run against different environments”Use a path for url. Reflow combines it with your selected target, so the same
flow can run against a local app, a PR preview or staging. An open command can
use an absolute URL to visit another origin.
Select the target with reflow target, inspect the work with reflow plan, then
execute it with reflow apply or reflow run. Your agent uses the same controller
through MCP’s manage_execution tool. Plugins run on the connected device, so
localhost reaches that device’s application.
HTTP batch targets
Section titled “HTTP batch targets”The HTTP batch API accepts either target_url: "https://preview.example.com"
or target: {kind: "browser", base_url: "http://127.0.0.1:3000"}. The browser
form runs without a separate application container and allows shell commands in
the browser image. See browser self-targets.
TypeScript and agent steps
Section titled “TypeScript and agent steps”The HTTP batch runner also accepts named TypeScript steps:
```ts step="the checkout heading is visible"await page.goto(URL);await expect(page.getByRole("heading", { name: "Checkout" })).toBeVisible();```Within a ts step="…" fence, a body made entirely of valid RFL statements runs
as RFL. Otherwise, the complete body runs as TypeScript with page, expect
and URL in scope. Keep prose outside the fence: it is not valid TypeScript.
An empty ts step="…" fence asks the runner’s agent to carry out the named
step. This requires a configured model provider. Use non-empty rfl fences
for local CLI flows.