A testing language
built for your application.

Plugins let you name application actions, such as shop.refund_order, and use them in a flow. Your agent can call the command without reading its implementation each time.

One command for
a business action.

Your agent defines commands, snapshots and comparisons in a plugin. A flow can then say “refund this order and check the balance” without repeating the browser steps, SQL and worker checks behind that action.

Each plugin defines which evidence it keeps and how to compare results. The agent can inspect a changed balance or missing entitlement alongside the code change.

The provider framework is Apache-2.0 licensed. Public source and SDK releases are pending; custom integrations currently require private source access.

Build your testing language
reflow / Custom Commerce pluginIllustrative example
refund-order.mdshop.refund_order customer="alex"
Customer balance$75.00$100.00$25.00 returned
Inventory9101 item returned
Snapshot deltaBeforeAfter
order.statuspaidrefunded
balance$75.00$100.00
stock910
shop.expect_balance customer="alex" amount=100shop.expect_stock count=102 assertions passed

Build across your whole application.

Browser

Run a user journey, capture the screen and compare it with an earlier snapshot. Your agent can check the visible difference against the change you intended.

See Browser in a flow
reflow / Team invitationsIllustrative example
invite-member.rflLine 5 · selected step
  1. site.open url="/settings/team"

Illustrative flow excerpt and playback. Comments describe retained evidence and review, not extra plugin commands. Select a checkpoint line to inspect its output; preceding lines provide context.

Output · selected step
Change Summary

Update invitation copy

1 changed screen
invite-memberPassed
team-invitation/settings/team
Beforemain
AcmeWorkspace Invite a teammate Add someone to your workspace. Email address sam@acme.test RoleEditor Send invitation
Afterthis change
AcmeWorkspace Invite a teammate Add someone to your workspace. Email address sam@acme.test Role selector removed Send invitation

Snapshot delta · team-invitation Before: role selector present. After: role selector removed.

The test passed. The screen changed.site.snapshot
Agent reviewOutside scope

The role selector disappeared.

This PR changes invitation copy. Owners still need to choose a role before sending an invitation.

team-invitationteam-access.md

Bash

Run your application’s own commands and keep their output for comparison. An exit code tells you the command finished; its result tells you what happened.

See Bash in a flow
Bash · fulfillment workerIllustrative example
fulfill-order.rflLine 4 · selected step

Illustrative flow excerpt and playback. Comments describe retained evidence and review, not extra plugin commands. Select a checkpoint line to inspect its output; preceding lines provide context.

Output · selected step
fulfill-order.mdLocalBash
worker.bash
command="./bin/fulfill order-1042"

Your application command. Its result becomes a snapshot.

Beforeexit 0
stdout.txt order: 1042 status: dispatched tracking_number:
TRK-1042
stderr.txtempty
Afterexit 0
stdout.txt order: 1042 status: dispatched
tracking_numberMissing from output
stderr.txtempty
State and evidence changed
Exit code
0 → 0
Standard output
Changed

Both commands succeeded. The saved output differs.

Agent’s review note

“The worker still reports ‘dispatched’, but no longer returns a tracking number. Can the shipment notification still include a tracking link?”

Evidence: before + after / stdout.txt

SQL

Save query results as snapshots and compare them across runs. Your agent can inspect a changed balance or order status alongside the browser journey.

See SQL in a flow
reflow / PostgreSQL · refund checkIllustrative example
verify-refund.rflLine 4 · selected step

Illustrative flow excerpt and playback. Comments describe retained evidence and review, not extra plugin commands. Select a checkpoint line to inspect its output; preceding lines provide context.

Output · selected step
verify-refund.mddb: PostgreSQL
db.query sql="SELECT id, status, refunded
FROM orders ORDER BY id"
key="id" checkpoint="orders"
orders / rows.jsonEarlier runCurrent run
id keystatusrefunded
1041paid0.00
1042refunded25.0020.00
1043paid0.00
3 rows retained · amounts in USD
Retained before → after1042
refunded25.0020.00
Agent review · examplerefunds.md

Full refunds return the entire $25.00 payment.

The order says refunded. $5.00 is missing. The captured rows show $20.00 returned. Check the refund amount before merging.

Mailbox

Test signups, email codes and magic links with a remote inbox. Save the email body to a named variable, extract a code or link with your regex, then continue in the browser.

See Mailbox in a flow
reflow / Mailbox · email sign-inIllustrative example
email-sign-in.rflLine 6 · selected step
  1. inbox.cursor name="signup" -> before
  2. inbox.extract text=${email_body} pattern="code is ([0-9]{6})" group=1 -> code

Illustrative flow excerpt and playback. Comments describe retained evidence and review, not extra plugin commands. Select a checkpoint line to inspect its output; preceding lines provide context.

Output · selected step
Request a fresh codesign-in.md
inbox.cursor name="signup" -> before
click role=button name="Send code"
New message after the cursorMatched
From
noreply@example.test
Subject
Sign-in code
inbox.capture name="signup" after=${before} from="noreply@" subject="Sign-in code" -> email_body

email_body = "Your code is 831204."

Extract with your regexcode = "831204"
inbox.extract text=${email_body} pattern="code is ([0-9]{6})" group=1 -> code

The first regex group becomes code. The browser uses that value below.

fill label="Verification code" ${code}
email_body · before / afterIllustrative snapshots

Earlier runYour code is 617203.

This runYour code is 831204.

Delta: verification code changed. A fresh message is expected. The cursor keeps the previous run’s email out of this capture; the extracted current code is 831204.

OpenAPI

Call operations from a pinned OpenAPI document, assert their status and JSON fields, and capture public values for later flow steps.

See OpenAPI in a flow
reflow / OpenAPI · test itemIllustrative example
get-item.rfl · excerptLine 4 · selected step

Illustrative playback. Contract digest abbreviated in the request line; expand Complete flow step for exact source. Checkpoint names and comparison comments are presentation labels. Select a checkpoint line to inspect its output; preceding lines provide context.

Output · selected step
getItemPinned contract
sha256:9c491f4050369bc8572eaf86ab1333bad35dabb38f11a890f94b4bd0d2a37444

The synthetic item API from the guide. The test item already exists.

Declared requestapi: OpenAPI
GET /items.get
Query · id
3f2b8c1e-5a4d-4e6f-9b7a-1c2d3e4f5a6b
Header · X-Project
default

The binding supplies the server and private session credential. One request; no automatic retries.

Retained evidence · illustrativeconfirmed
HTTP status
200
JSON assertion
/id · equals · pass

Assertion results omit target and operand values. Raw response bodies are not retained.

Agent review · example

The expected public item id was returned. Check this API result alongside the rest of the journey. A confirmed response can still fail its assertions.

getItem safe evidence · snapshot deltaIllustrative comparison
Retained fieldBeforeAfter
Completionconfirmedconfirmed
HTTP status200200
/id · equalspasspass

Delta: safe status and assertion results unchanged. This comparison does not expose or compare a retained raw response body.

Complete flow step

Use the guide’s exact spec and private binding with an owned test server. This illustration does not send a request.

---
providers:
  api: OpenAPI
---

api.call (
  operationId="getItem"
  spec="sha256:9c491f4050369bc8572eaf86ab1333bad35dabb38f11a890f94b4bd0d2a37444"
  query=json(
    {"id":"3f2b8c1e-5a4d-4e6f-9b7a-1c2d3e4f5a6b"}
  )
  header=json(
    {"X-Project":"default"}
  )
  expect=json(
    {"status":200,"json":[{"pointer":"/id","equals":"3f2b8c1e-5a4d-4e6f-9b7a-1c2d3e4f5a6b"}]}
  )
)

MCP

Connect as one test identity, list the tools your server exposes, call them and assert what comes back. Reflow keeps each result with the run, so your agent can compare it with an earlier run.

See MCP in a flow
reflow / MCP · billing serverRecorded output · fixture data
Captured billing flowLine 11 · selected step
  1. api.connect
  2. api.call tool="search_invoices" arguments="{\"query\":\"overdue\"}" expect="{\"textIncludes\":\"INV-1042\"}"
  3. api.call tool="get_invoice" arguments="{\"id\":\"INV-1042\"}" expect="{\"isError\":false,\"json\":[{\"pointer\":\"/status\",\"equals\":\"overdue\"},{\"pointer\":\"/amount_due\",\"type\":\"string\"}]}"
  4. api.call tool="void_invoice" arguments="{\"id\":\"INV-1042\"}" expect="{\"rpcError\":-32602}"
  5. api.disconnect
  6. # Run 2 repeats the same steps in a new session
  7. api.call tool="get_invoice" arguments="{\"id\":\"INV-1042\"}" expect="{\"isError\":false,\"json\":[{\"pointer\":\"/status\",\"equals\":\"overdue\"},{\"pointer\":\"/amount_due\",\"type\":\"string\"}]}"

Exact recorded RFL. ‘invoice’ names the retained result in this presentation; it is not a runtime checkpoint. Comment lines explain retention and comparison. Select a checkpoint line to inspect its output; preceding lines provide context.

Output · selected step
billing-fixtureStreamable HTTP · 2025-11-25
api.connect · HTTP 200
tools/list3 tools · 1 page
Senttools/list · params {}
  • get_invoicereadOnly trueschema 3797626b… · description b4f839da…
  • search_invoicesreadOnly trueschema 1dc63095… · description 27fafa01…
  • send_reminderdestructive false · idempotent false · readOnly falseschema 3797626b… · description 48814d33…
  • only 3 named toolspassed
  • noTrueAnnotations destructivepassed
Snapshot · invoice · beforeRun 1 · presentation label
search_invoicestools/call
Senttools/call · arguments {"query":"overdue"}
GotINV-1042 overdue 14 days INV-1043 overdue 3 days
  • textIncludes "INV-1042"passed
get_invoicetools/call
Senttools/call · arguments {"id":"INV-1042"}
Got{"id":"INV-1042","status":"overdue","amount_due":"120.00","currency":"USD"}
  • isError falsepassed
  • /status equals "overdue"passed
  • /amount_due type "string"passed
void_invoicetools/call · refusal expected
Senttools/call · arguments {"id":"INV-1042"}
Goterror -32602 · Unknown tool: void_invoice
  • rpcError -32602passed

api.disconnect · cleanup acknowledged (204)Run 1 · 7 of 7 expectations passed

Snapshot · invoice · afterRun 2 · presentation label
get_invoicesame steps, new session
Got{"id":"INV-1042","status":"overdue","amount_due":"0.00","currency":"USD"}
  • isError falsepassed
  • /status equals "overdue"passed
  • /amount_due type "string"passed

All recorded steps repeated, including the expected refusal and disconnect.Run 2 · 7 of 7 expectations passed

The retained invoice still says overdue. The amount due is now 0.00; passing expectations alone do not establish that this change is intended.

Delta · invoiceBefore · Run 1 → After · Run 2
stepBeforeAftercompared
tools/list 2 of 2 passed 2 of 2 passed match
search_invoices 1 of 1 passed 1 of 1 passed match
get_invoice 3 of 3 passed 3 of 3 passed differs
void_invoice 1 of 1 passed 1 of 1 passed match
Review point · get_invoiceretained text
Before · Run 1{"id":"INV-1042","status":"overdue","amount_due":"120.00","currency":"USD"}
After · Run 2{"id":"INV-1042","status":"overdue","amount_due":"0.00","currency":"USD"}

Every expectation passed in both runs, yet the retained get_invoice results differ. Highlighted: the field that changed.

Agent review · exampleINV-1042 is still overdue, but its amount due now reads 0.00. Was that change intended?

Recorded MCP plugin output from two runs against a synthetic billing server on localhost. “Sent” is the request the server received; replies, checks and comparisons are the plugin’s retained snapshots. Fixture data; layout illustrative. “invoice” is a presentation label for the retained get_invoice result, not a named runtime checkpoint.

Recorded snapshots for get_invoice
api.call tool="get_invoice" arguments="{\"id\":\"INV-1042\"}" expect="{\"isError\":false,\"json\":[{\"pointer\":\"/status\",\"equals\":\"overdue\"},{\"pointer\":\"/amount_due\",\"type\":\"string\"}]}"

// Run 1 · succeeded
{
  "contentTypes": [
    "text"
  ],
  "failures": [],
  "isError": false,
  "jsonAssertions": [
    {
      "index": 0,
      "operator": "equals",
      "passed": true,
      "pointer": "/status"
    },
    {
      "index": 1,
      "operator": "type",
      "passed": true,
      "pointer": "/amount_due"
    }
  ],
  "kind": "result",
  "redacted": false,
  "structuredContentSha256": null,
  "text": "{\"id\":\"INV-1042\",\"status\":\"overdue\",\"amount_due\":\"120.00\",\"currency\":\"USD\"}",
  "textTruncated": false,
  "tool": "get_invoice"
}

// Run 2 · succeeded
{
  "contentTypes": [
    "text"
  ],
  "failures": [],
  "isError": false,
  "jsonAssertions": [
    {
      "index": 0,
      "operator": "equals",
      "passed": true,
      "pointer": "/status"
    },
    {
      "index": 1,
      "operator": "type",
      "passed": true,
      "pointer": "/amount_due"
    }
  ],
  "kind": "result",
  "redacted": false,
  "structuredContentSha256": null,
  "text": "{\"id\":\"INV-1042\",\"status\":\"overdue\",\"amount_due\":\"0.00\",\"currency\":\"USD\"}",
  "textTruncated": false,
  "tool": "get_invoice"
}

// Comparison
MCP observations differ.

DockerUbuntu

Run your application’s tests, workers and build commands inside an Ubuntu container and keep their output for comparison. Your agent can spot a changed result even when every command succeeds.

See DockerUbuntu in a flow
DockerUbuntu · release checkRecorded plugin output
Captured release checkLine 8 · selected step
  1. # Before · main
  2. container.bash command="./bin/test"
  3. # After · PR · same commands, fresh container
  4. container.bash command="./bin/test"

Recorded plugin invocations shown in flow syntax. Checkpoint names label this presentation; the capture used the plugin host directly, not reflow apply. Select a checkpoint line to inspect its output; preceding lines provide context.

Output · selected step

Real plugin output, synthetic shop app. IDs shortened, setup flags abbreviated. Driven directly through the plugin host; checkpoint names below are presentation labels, not stored Reflow checkpoints. Full capture and fixture source

ContainerYour test setup
$ docker build -q -t shop-test:pr . sha256:b83b8cfb3206 $ docker run -d --network=none --name shop-test-pr shop-test:pr d2c3ea9cda2a

Connected shop-test-pr · /workspace

Snapshot · release-package · beforemain · presentation label
› container.bash command="./bin/test" shop 1.4.0 tests on Ubuntu 24.04.5 LTS ok - full refund returns the payment ok - half refund splits the payment ok - free order refunds nothing 3 passed › container.bash command="./bin/package" files="[\"dist/release.env\"]" packaged dist/shop-1.4.0.tar.gz dist/release.env: 4 settings
tests
exit 0stdout.txt 153 Bstderr.txt 0 B
release-package
exit 0stdout.txt 61 Bstderr.txt 0 Bfiles/dist/release.env 68 B
files/dist/release.envsha256 9f564b64
CURRENCY=USD
LOG_LEVEL=warn
RECEIPT_EMAILS=on
REFUND_WINDOW_DAYS=30
Snapshot · release-package · afterPR · presentation label
› container.bash command="./bin/test" shop 1.4.0 tests on Ubuntu 24.04.5 LTS ok - full refund returns the payment ok - half refund splits the payment ok - free order refunds nothing 3 passed › container.bash command="./bin/package" files="[\"dist/release.env\"]" packaged dist/shop-1.4.0.tar.gz dist/release.env: 4 settings
tests
exit 0stdout.txt 153 Bstderr.txt 0 B
release-package
exit 0stdout.txt 61 Bstderr.txt 0 Bfiles/dist/release.env 68 B
files/dist/release.envsha256 634287c8
CURRENCY=USD
LOG_LEVEL=warn
RECEIPT_EMAILS=on
REFUND_WINDOW_DAYS=14
Delta · release-packageBefore · main → After · PR
  • tests Unchanged, 3 passed
  • release-package State and evidence changed
files/dist/release.envsha256 9f564b64 → 634287c8
Before- REFUND_WINDOW_DAYS=30 After+ REFUND_WINDOW_DAYS=14

Exit code 0 → 0 · stdout.txt and stderr.txt unchanged

Both retained files
main
CURRENCY=USD
LOG_LEVEL=warn
RECEIPT_EMAILS=on
REFUND_WINDOW_DAYS=30
PR
CURRENCY=USD
LOG_LEVEL=warn
RECEIPT_EMAILS=on
REFUND_WINDOW_DAYS=14
Agent’s review note · example

“Both runs succeed, but the package now ships a 14-day refund window instead of 30. The PR comment says the shorter window is for local test fixtures. Should production keep 30?”

macOS VM

Development prototype · private source installation

Let your agent run native application journeys in a fresh macOS VM, inspect screenshots and accessibility snapshots, and compare the evidence with an earlier run. SwiftVM is used in Reflow QA through private source installation; it is not bundled with the public CLI.

Your agent drives the journey. Your desktop stays yours.

Explore the macOS VM prototype
SwiftVM · disposable macOS runRecorded run, composited
Recorded controller journalLine 8 · selected step

Controller actions from the 16 October recording, before Reflow guest_run integration. This trace follows that recording; the current guest_run flow is shown separately on the VM page. Select a checkpoint line to inspect its output; preceding lines provide context.

Recorded output · selected phase
144.5 sVerdictPassed, with nothing shown on the host desktop

Visual checkpoints · one recorded run

  1. Before input0
  2. No-input control0
  3. After host click1

Within-run delta: 0 → 1 after input; the no-input control stayed at 0. Counts are read from the VM screenshots, not a cross-run snapshot comparison.

  1. 0.0 s Clone Sealed template cloned and checked against its seal
  2. 9.9 s Boot VM started off-screen, no network
  3. 30.0 s Guest ready Guest agent connected to the host
  4. 34.7 s Tests Built in the guest; 3 of 3 tests passed
  5. 135.1 s App launched Sample counter app on the hidden display
  6. 138.3 s Host click Count read from screenshots: 0, 0, then 1
  7. 141.9 s Stopped Stop confirmed; clone removed at 143.3 s
  8. 144.5 s Verdict Passed, with nothing shown on the host desktop

Required checks from this run

  • passed host-window-hidden 133 samples, no window on screen
  • passed test-pass-result 3 of 3 Swift tests passed
  • passed gui-host-input-proof Count 0, 0 without input, 1 after the click
  • passed cleanup-confirmed Clone removed after a confirmed stop

All 19 required checks passed. Coverage was not requested.

Composited from one recorded prototype run (16 October 2026 UTC, one Apple Silicon host, macOS 27.0.1 guest). The macOS frames are captures from the VM’s own display, time-compressed from 144 seconds to 12. The dark intervals are not captures: no frames were kept during clone and boot, and the VM has stopped at the end. The crossfades, zoom and click ring are added. Times come from the run journal. Not a performance figure. This recording predates the source-installed vm.guest_run integration now used in Reflow QA; it does not show that Reflow flow.

macOS App

Development prototype · guest executor used in SwiftVM

Let your agent press native controls, wait for the expected state and retain named window captures with accessibility snapshots. The MacOSApp guest executor runs these journeys inside source-installed SwiftVM guests, so your agent can compare what changed without taking over your desktop.

See macOS App in a flow
reflow / MacOSApp · disk-usage appRecorded runs
Recorded macOS actionsLine 14 · selected step
  1. waitFor gigahog.plot · present
  2. expect gigahog.breadcrumb.item.0 · “reflow-demo”
  3. expect gigahog.mode.flame · present
  4. waitFor gigahog.plot · present
  5. press “Projects, 33.6 MB”
  6. waitFor “alpha, 25.2 MB” · present
  7. expect gigahog.breadcrumb.item.1 · present
  8. press gigahog.breadcrumb.item.0 · “reflow-demo”
  9. waitFor gigahog.breadcrumb.item.1 · absent
  10. # Presentation: compare base and injected-change runs

Recorded MacOSApp actions, grouped by named capture. Comparison comments describe this presentation, not runtime commands. Select a checkpoint line to inspect its output; preceding lines provide context.

Output · selected step
J01 launch-root · 4 actionsapp.run
Window capture “launched”: a flame view of the synthetic reflow-demo folder, 61 MB, split into Projects, Media, Old and Cache. Numbered outlines mark the disk gauge, the Flame button, the breadcrumb and the plot.
Checkpoint “launched” · window/launched-1.pngView capture
Inspect accessibilityax/launched.json
  1. AXWindow main
  2. 1AXUnknown “Disk 56 percent full”
  3. AXGroup “View” gigahog.mode
  4. 2AXButton “Flame” selected gigahog.mode.flame
  5. 3AXStaticText “reflow-demo” gigahog.breadcrumb.item.0
  6. 4AXGroup “Flame view of /private/tmp/reflow-gh-ff4b01362b0e28a7/data/reflow-demo, 61 MB. Largest: Projects 33.6 MB, Media 16.8 MB, Old 6.3 MB, Cache 4.2 MB.” gigahog.plot
  7. AXButton “Projects, 33.6 MB”

7 of 34 nodes, from another run of this build

Retained by this run
  • timeline.json
  • window/launched-1.png
J03 zoom-and-back · actions 1–5 of 8app.run
Window capture “zoomed”: the Projects folder, 33.6 MB, fills the view with alpha, 25.2 MB, and beta, 8.4 MB, below it. The breadcrumb reads reflow-demo / Projects.
Checkpoint “zoomed” · window/zoomed-1.pngView capture
Inspect accessibilityaction targets · result.json
  1. AXButton “Projects, 33.6 MB” pressed gigahog.plot.block./private/tmp/reflow-gh-18608611647edea9/data/reflow-demo/Projects
  2. AXButton “alpha, 25.2 MB” present gigahog.plot.block./private/tmp/reflow-gh-18608611647edea9/data/reflow-demo/Projects/alpha
  3. AXStaticText “Projects” present gigahog.breadcrumb.item.1
Retained by this run
  • timeline.json
  • ax/002-after.json
  • window/zoomed-1.png
J03 zoom-and-back · actions 6–8 of 8app.run
Window capture “back”: the flame view has returned to the whole reflow-demo folder, 61 MB.
Checkpoint “back” · window/back-1.pngView capture
Inspect accessibilityaction targets · result.json
  1. AXButton “reflow-demo” pressed gigahog.breadcrumb.item.0
  2. gigahog.breadcrumb.item.1 absent

The app quit through its own Quit command; cleanup confirmed.

Retained by this run
  • timeline.json
  • ax/002-after.json
  • ax/006-after.json
  • window/zoomed-1.png
  • window/back-1.png
J03 zoom-and-back · base build vs injected changecompare
The “zoomed” capture with the changed 24 by 47 pixel region outlined on the disk gauge needle.
window/zoomed-1.png24×47 px at 244,117
Run comparison6 artifacts
window/zoomed-1.png
Changed 0.27% of pixels, one 24×47 px region
window/back-1.png
Changed 0.22% of pixels, the same region
4 accessibility snapshots
Same scan times and temporary paths normalized
8 actions
Passed in both runs
Base build: the gauge needle leans right of centre.
Base build
Injected change: the needle is mirrored and leans left of centre.
Injected change
Changed pixels: both needle positions marked over a dimmed capture.
Changed pixels
Agent review · example interpretation

Every step passed. The gauge did not. The accessibility snapshots match, but the needle is drawn mirrored: the disk is more than half full and the needle now leans left. Was the gauge drawing meant to change?

Real MacOSApp window captures and accessibility snapshots from QA runs of Gigahog, a disk-usage app in development, on synthetic folders. Screens from separate runs of one build are shown in sequence; before each press, the run’s other root or zoomed capture stands in for the screen. The needle change was injected on purpose; its crops come from one round of that exercise and the comparison figures from a repeat, outlined here on another run’s capture. The guest executor is used inside source-installed SwiftVM guests; the standalone host provider is a separate development candidate. MacOSApp is not bundled with the public CLI.

Keep the results
with the change.

Review evidence deltas