Skip to content

Flow commands

Every step in a flow is one command: - name (bare), - name: value (scalar) or - name: with a map of keys. Selector-taking commands accept a bare string (exact visible text) or a selector object — see Flow file & selectors. Durations are milliseconds. ${VAR} is substituted in string values — see Variables.

Command Summary web android ios
App & navigation
launchApp Launch the app (or open the URL)
stopApp Stop the app
installApp Install a build (apk / ipa)
openLink Open a URL / deep link
clearState Wipe the app’s stored data
grantPermissions Grant the app’s runtime permissions
setPermissions Allow / deny individual permissions
back Go back
vscodeCommand Run a VS Code command (VS Code)
Taps & gestures
tapOn Tap an element or a point
longPressOn Long-press an element or a point
doubleTapOn Double-tap an element or a point
scroll Scroll the screen
swipe Swipe by direction or between points
scrollUntilVisible Scroll until an element appears
dragAndDrop Drag one element onto another
check Tick a checkbox or switch
uncheck Untick a checkbox or switch
hover Move the pointer over an element
Text input & keys
inputText Type into the focused field
inputRandomEmail Type a random e-mail
inputRandomPersonName Type a random person name
inputRandomNumber Type a random number
inputRandomText Type random text
inputRandomCityName Type a random city name
inputRandomCountryName Type a random country name
inputRandomColorName Type a random color name
clearText Clear the focused field
eraseText Erase the last N characters
pressKey Press a key or a combo
hideKeyboard Dismiss the on-screen keyboard
setClipboard Set the flow’s clipboard register
pasteText Paste the register into the field
Forms & page (web)
selectOption Choose an option in a
uploadFile Attach files to a file input
assertUrl Assert the page address
Assertions & waits
assertVisible Assert an element is visible
assertNotVisible Assert an element is gone
waitForVisible Wait until an element is visible
extendedWaitUntil Wait until visible / not visible
waitForAnimationToEnd Wait for the screen to settle
assertTrue Assert over values or a JS expression
copyText Copy an element’s text into a variable
assertFile Assert a workspace file exists (VS Code)
assertScreenshot Visual check against a baseline
Screenshots & video
takeScreenshot Save a screenshot
startRecording Start screen recording
stopRecording Stop recording and save the video
Device state
setLocation Mock the GPS location
travel Move through GPS points
setOrientation Rotate the device
setAirplaneMode Turn airplane mode on / off
toggleAirplaneMode Toggle airplane mode
addMedia Add photos / videos to the gallery
clearKeychain Clear the iOS keychain
Control flow
runFlow Run a helper file or an inline block
repeat Repeat a block
retry Re-run a block on failure
AI
assertWithAI AI screen assertion
assertNoDefectsWithAI AI visual-defect check
extractTextWithAI AI text extraction into a variable
Scripting
evalScript Run inline JavaScript
runScript Run a JavaScript file
API & data
request HTTP request with extract / assert
sql SQL statement with extract / assert

VS Code extension targets behave like web in this table; desktop-app targets (--driver app) share the web DOM walk but lack most optional capabilities — see Testing desktop apps.

Launches the app under test — on web it navigates to the URL. The bare string form takes the app id / URL; the map form adds clearState (wipe stored data first), stopApp (force a cold start), permissions (a name: allow|deny|unset map, mobile) and arguments (launch arguments, mobile).

Keys: appId — string · required; clearState — bool; stopApp — bool; permissions — map; arguments — map.

commands:
- launchApp: com.example.app
- launchApp:
appId: com.example.app
clearState: true
permissions:
notifications: allow

Terminates the app: bare - stopApp uses the flow’s appId, - stopApp: <appId> stops another one. killApp is an alias.

Aliases: killApp

Keys: appId — string.

commands:
- stopApp
- stopApp: com.example.other

Installs a build from a path. grant: true grants runtime permissions right after the install.

Platforms: android, ios

Keys: path — string · required; grant — bool.

commands:
- installApp: build/app-debug.apk
- installApp:
path: build/app.ipa
grant: true

Opens a URL or deep link: browser navigation on web, am start -a VIEW on Android, simctl openurl on iOS.

Keys: url — string · required.

commands:
- openLink: https://example.com/reset?token=${TOKEN}

Resets stored state: localStorage + cookies on web, pm clear on Android, the simulator’s app data container on iOS (simulator only). Bare form targets the flow’s appId; pass an app id to target another app.

Keys: appId — string.

commands:
- clearState
- clearState: com.example.other

Grants the common runtime permissions (simctl privacy on iOS, pm grant on Android). Bare form uses the flow’s appId. Not supported on web — browsers have no permission model the driver can grant.

Platforms: android, ios

Keys: appId — string.

commands:
- grantPermissions

Sets permissions one by one: a name: allow|deny|unset map (all covers every permission), optionally under permissions: next to an appId. Names: camera, location, microphone, notifications, photos, contacts, calendar, bluetooth, motion, medialibrary, phone, reminders, siri, sms, storage.

Platforms: android, ios

Keys: appId — string; permissions — map.

commands:
- setPermissions:
camera: allow
location: deny
- setPermissions:
appId: com.example.other
permissions:
all: unset

Browser history back on web, the hardware Back key on Android. On iOS there is no system back — the step is a no-op there.

Platforms: web, android

commands:
- back

VS Code flows only. id — a bare workbench.action.closeAllEditors, or { id, args } — goes through the helper extension the driver bundles, which answers when the command resolves; { title: … } types the palette title instead and is done when the palette closes. A bare string is read as an id when it is shaped like one (dotted, no spaces) and as a palette title otherwise. args needs id: the palette takes no arguments, and asking for both is an error rather than a silent drop — as is id together with title, or any other key in the map. Works in before: / after: too; before: clean is five of these steps.

Platforms: web

Keys: id — string; title — string; args — list of strings.

Exclusive: one of id, title.

commands:
- vscodeCommand: workbench.action.closeAllEditors
- vscodeCommand:
title: "My Ext: Generate Config"
- vscodeCommand:
id: workbench.action.openSettings
args: ["editor.fontSize"]

Taps the first match of a selector; off-screen targets are scrolled into view first. { point: "x, y" } taps coordinates instead (pixels or %). repeat taps N times with delay ms between taps (default 100). timeout (ms, default 5000) overrides the element wait for this tap alone.

Keys: selector — selector; repeat — integer; delay — ms; timeout — ms; point — point "x, y".

commands:
- tapOn: "Log in"
- tapOn:
id: submit
- tapOn:
point: "50%, 90%"
- tapOn:
text: "+"
repeat: 3
delay: 200
- tapOn:
text: "Continue"
timeout: 30000

Presses and holds for 600 ms — a selector or { point: "x, y" }.

Keys: selector — selector; point — point "x, y".

commands:
- longPressOn: "Message"
- longPressOn:
point: "200, 400"

Two quick taps on a selector or { point: "x, y" }.

Keys: selector — selector; point — point "x, y".

commands:
- doubleTapOn: "Photo"

Scrolls one screen up or down (bare - scroll = down). Horizontal moves are swipe: left|right.

Keys: direction — enum (up | down); within — selector.

commands:
- scroll
- scroll: up

swipe: up|down|left|right, or { start, end } as "x, y" points in pixels or % of the screen.

Keys: direction — enum (up | down | left | right); start — point "x, y"; end — point "x, y".

Exclusive: direction not with start, end.

commands:
- swipe: left
- swipe:
start: "50%, 80%"
end: "50%, 20%"

Scrolls repeatedly until the selector is visible. Map form: direction (up|down|left|right, default down), timeout ms, speed 0–100, centerElement: true to finish with the element centred. Gives up after --scroll-attempts swipes (default 5).

Keys: selector — selector · required; direction — enum (up | down | left | right); timeout — ms; speed — integer; centerElement — bool; within — selector.

commands:
- scrollUntilVisible: "Terms of service"
- scrollUntilVisible:
id: footer
direction: down
centerElement: true

Locates both elements and drags between their centres. duration (ms, default 600) is slow enough for a drop target’s hover state to fire, which is what most drag-and-drop implementations wait for.

Keys: from — selector · required; to — selector · required; duration — ms; path — pointList.

commands:
- dragAndDrop:
from: "Card"
to: { id: done-column }

Reads the element’s checked state first and taps only when it isn’t already on, then confirms the tap took — a disabled or covered control fails the step instead of passing silently. Safe to re-run.

Keys: selector — selector · required.

commands:
- check: "Remember me"

The mirror of check: taps only when the element is on, then confirms it went off.

Keys: selector — selector · required.

commands:
- uncheck: { id: newsletter }

Moves the pointer without pressing — for menus, tooltips and controls that only appear under the cursor. Web and VS Code only; a touchscreen has no hover.

Platforms: web

Keys: selector — selector · required.

commands:
- hover: "Account"
- assertVisible: "Sign out"

Types text into the currently focused input. ${VAR} is substituted first.

Keys: text — string · required.

commands:
- tapOn: "Email"
- inputText: "${EMAIL}"

Types a generated e-mail address into the focused field.

Keys: length — integer.

commands:
- inputRandomEmail

Types a generated first + last name.

Keys: length — integer.

commands:
- inputRandomPersonName

Types a random number; length digits (default 8) — bare, : N or { length: N }.

Keys: length — integer.

commands:
- inputRandomNumber: 6

Types random letters; length characters (default 8) — bare, : N or { length: N }.

Keys: length — integer.

commands:
- inputRandomText:
length: 12

Types a city name from a built-in list.

Keys: length — integer.

commands:
- inputRandomCityName

Types a country name from a built-in list.

Keys: length — integer.

commands:
- inputRandomCountryName

Types a color name from a built-in list.

Keys: length — integer.

commands:
- inputRandomColorName

Empties the focused text field.

commands:
- tapOn: "Search"
- clearText

- eraseText: N deletes N characters before the caret; bare - eraseText clears the whole field.

Keys: count — integer.

commands:
- eraseText: 3

Keys: enter, home, back, backspace, tab, escape, volume up, volume down, lock, power (case-insensitive). Combos like cmd+shift+p (mods ctrl/control, alt/option, shift, cmd/meta/win/super) work on web and VS Code targets.

Keys: key — enum (enter | home | back | backspace | tab | escape | arrow up | arrow down | arrow left | arrow right | volume up | volume down | lock | power) · required.

commands:
- pressKey: enter
- pressKey: cmd+shift+p

Dismisses the software keyboard (blur on web, best-effort on iOS).

commands:
- hideKeyboard

Stores text in the flow’s copy register — the same one copyText fills and ${copiedText} reads. It is a flow-level register, not the OS clipboard.

Keys: text — string · required.

commands:
- setClipboard: "hello"
- pasteText

Types the copy register (copyText / setClipboard, else a copiedText variable) into the focused field.

commands:
- copyText: "Order #"
- tapOn: "Search"
- pasteText

Picks an option by its visible text (option), by the underlying value (optionValue) or by position (optionIndex) — exactly one of the three. Fires input and change, so frameworks see the edit.

Platforms: web

Keys: selector — selector · required; option — string; optionValue — string; optionIndex — integer.

Exclusive: one of option, optionValue, optionIndex.

commands:
- selectOption:
id: country
option: "Germany"

A file dialog is browser chrome and cannot be clicked, so the files are handed to the input directly. files takes a path or a list of paths, resolved like every other file a flow names; a missing file fails the step.

Platforms: web

Keys: selector — selector · required; files — list of strings · required.

commands:
- uploadFile:
id: avatar
files: fixtures/face.png

The bare form matches exactly; the map form takes one of equals, contains or regex, plus timeout (ms). Polls until it matches, so a click that navigates has time to land.

Platforms: web

Keys: equals — string; contains — string; regex — string; timeout — ms.

Exclusive: one of equals, contains, regex.

commands:
- assertUrl: { contains: "/checkout" }

The engine settles before every command, so most flows need no explicit wait at all. On web a page counts as settled when four signals are quiet at once — the document has finished loading, no request is in flight (streams that stay open by design, and anything older than 5 s, do not count), the DOM has not changed for 150 ms, and the address has not changed for 150 ms — and two reads of the screen come back the same, ignoring drift of up to 2 px so an animation’s tail is not mistaken for movement. On mobile and native the platform’s own idle signal stands in for the first three. Settling is capped by --settle-timeout (2000 ms); a step’s own timeout: raises that cap for that step. A screen whose tree is slower to read than that budget still gets read twice — the cap stretches to what the reads themselves cost, so a slow page reads slow, not unsettled.

A run that could not settle in time still goes on, and says so. assertNotVisible, waitForNotVisible and when: notVisible only pass on a settled screen: an element missing from a page that is still drawing itself has not gone away, it has not arrived — so they fail with the reason instead of passing on a half-drawn screen. Everything else (tapOn, assertVisible, inputText, …) acts anyway at the deadline and the step carries acted on a page that had not settled (Network(2)), plus settled: false and unsettledReason in the report. The reasons are Loading, Network(n), Mutating, Navigating and Timeout.

Waits up to timeout ms (default 5000, --wait-timeout) for the selector to be visible, then fails the step if it isn’t.

Keys: selector — selector · required; timeout — ms.

commands:
- assertVisible: "Welcome"
- assertVisible:
id: toast
timeout: 10000

Polls until the selector is absent on a settled screen (up to timeout ms, default 5000); fails if it is still visible, and fails with the reason if the page never settled — an element missing from a page still drawing itself is not proof it went away.

Keys: selector — selector · required; timeout — ms.

commands:
- assertNotVisible: "Loading…"

Same as assertVisible; reads better when a step is a wait, not a check.

Keys: selector — selector · required; timeout — ms.

commands:
- waitForVisible:
text: "Dashboard"
timeout: 15000

Alias form: { visible: <selector> } or { notVisible: <selector> } plus timeout ms.

Keys: visible — selector; notVisible — selector; timeout — ms.

commands:
- extendedWaitUntil:
notVisible: "Spinner"
timeout: 20000

Waits until the screen stops changing; timeout ms caps the wait (default --settle-timeout, 2000).

Keys: timeout — ms.

commands:
- waitForAnimationToEnd
- waitForAnimationToEnd:
timeout: 5000

{ equals: [a, b] } / { contains: [haystack, needle] } over ${}-substituted strings, or a bare string evaluated as JavaScript in the flow’s script context (assertTrue: "${count} > 1").

Keys: expression — string; equals — list of strings; contains — list of strings.

Exclusive: one of expression, equals, contains.

commands:
- copyText:
id: total
into: total
- assertTrue:
equals: ["${total}", "42"]
- assertTrue: "Number(total) > 40"

Reads the element’s text into into (default copiedText), available as ${name} and as a JS global. Alias copyTextFrom.

Aliases: copyTextFrom

Keys: selector — selector · required; into — string.

commands:
- copyText:
id: order-number
into: order

For VS Code extension tests: asserts that path (relative to the open workspace) exists and, with contains, includes the text.

Platforms: web

Keys: path — string · required; contains — string.

commands:
- assertFile:
path: out/report.md
contains: "PASS"

Compares the screen with <name>.png; the first run creates the baseline. tolerance is the allowed percentage of differing pixels (default 0.5); a failing actual is saved as <name>.actual.png.

Keys: name — string · required; tolerance — number.

commands:
- assertScreenshot: checkout
- assertScreenshot:
name: screens/home
tolerance: 1.5

Writes <name>.png (default screenshot.png) — relative to the current directory in the CLI, next to the run’s artifacts in the desktop.

Keys: name — string.

commands:
- takeScreenshot: after-login

Starts recording (native on Android/iOS, screencast on web); stopRecording writes <name>.mp4 (default recording.mp4). Whole-run video is simpler with tapstep test --video.

Keys: name — string.

commands:
- startRecording: checkout
- tapOn: "Pay"
- stopRecording

Ends the recording started by startRecording and saves the file.

commands:
- stopRecording

Overrides the device location with { lat, lon } (geolocation override on web).

Keys: lat — number · required; lon — number · required.

commands:
- setLocation:
lat: 43.238
lon: 76.889

Sets the location to each "lat, lon" point in turn; with speed (m/s) the pause between points follows the distance, otherwise 1 s.

Keys: points — list of strings · required; speed — number.

commands:
- travel:
points:
- "43.238, 76.889"
- "43.240, 76.900"
speed: 15

portrait, landscapeLeft (landscape), landscapeRight, upsideDown (case-insensitive).

Keys: orientation — enum (portrait | landscapeLeft | landscapeRight | upsideDown) · required.

commands:
- setOrientation: landscape

enabled or disabled (true/false also work). Android only.

Platforms: android

Keys: mode — enum (enabled | disabled) · required.

commands:
- setAirplaneMode: enabled

Flips airplane mode. Android only.

Platforms: android

commands:
- toggleAirplaneMode

Pushes one path or a list of paths into the device’s media library.

Platforms: android, ios

Keys: paths — list of strings · required.

commands:
- addMedia:
- fixtures/photo.jpg
- fixtures/clip.mp4

Wipes the simulator’s keychain. iOS only.

Platforms: ios

commands:
- clearKeychain

runFlow: path.helper.yaml inlines a helper (must end in .helper.yaml, resolved relative to the calling file). The map form takes file or inline commands, an optional when ({ visible | notVisible: <selector> } or { platform: android | [web, ios] }) and env — variables scoped to the block (shadowed names are restored afterwards; new ones persist). Helper params: are passed via env.

Keys: file — string; when — condition; env — map; commands — commands.

commands:
- runFlow: helpers/login.helper.yaml
- runFlow:
file: helpers/login.helper.yaml
env:
USER: qa@example.com
- runFlow:
when:
visible: "Accept cookies"
commands:
- tapOn: "Accept cookies"

times: N or while: <condition> ({ visible | notVisible: <selector> }), plus commands. A while loop stops after 1000 iterations.

Keys: times — integer; while — condition; commands — commands · required.

commands:
- repeat:
times: 3
commands:
- tapOn: "+"
- repeat:
while:
visible: "Load more"
commands:
- tapOn: "Load more"

Runs commands; on failure retries up to maxRetries times (default 1). A pass after retries is reported with a note.

Keys: maxRetries — integer; commands — commands · required.

commands:
- retry:
maxRetries: 2
commands:
- tapOn: "Refresh"
- assertVisible: "Updated"

Asks the configured model whether the assertion holds for the current screen. The bare form is optional: true — a failed check is a warning; { assertion, optional: false } fails the flow. Needs an AI connection: ai: in config.yaml or TAPSTEP_AI_* — see AI agents.

Keys: assertion — string · required; optional — bool.

commands:
- assertWithAI: "The cart shows exactly two items"
- assertWithAI:
assertion: "No overlapping text on the screen"
optional: false

Asks the model for obvious visual defects (clipped text, overlaps, broken layout). Optional by default; { optional: false } fails the flow.

Keys: optional — bool.

commands:
- assertNoDefectsWithAI

Asks the model to read something off the screen into into (default aiText); with pattern (regex) the answer must match — one retry, then the step fails.

Keys: query — string · required; into — string; pattern — string.

commands:
- extractTextWithAI:
query: "the 6-digit confirmation code"
into: code
pattern: "\\d{6}"
- inputText: "${code}"

Evaluates JS in the flow’s script context (one QuickJS context per run). Flow variables are globals; write output.name = … to publish ${output.name}; console.log is captured into the report. See Scripts.

Keys: script — string · required.

commands:
- evalScript: "output.total = Number(price) * 2"
- assertVisible: "${output.total}"

Runs a .js file (relative to the flow) in the same context; env adds variables for the script.

Keys: file — string · required; env — map.

commands:
- runScript:
file: scripts/prepare.js
env:
SEED: "42"

Calls an HTTP endpoint from the flow (http is an alias). - request: <url> is a GET; the map form takes method, headers, query, json or body, auth ({ bearer } / { basic: { user, pass } }), extract (name → JSONPath into ${vars}), assert or retryUntil (status as a code or "2xx", json equalities / { contains }, headers, schema file or inline), timeout ms (default 30000). retryUntil polls every second. ${} is substituted in the request, not in expected values. With device: none a flow is pure API — see API testing.

Aliases: http

Keys: url — string · required; method — enum (GET | POST | PUT | PATCH | DELETE | HEAD | OPTIONS); headers — map; query — map; json — map; body — string; auth — map; extract — map; assert — map; retryUntil — map; timeout — ms.

Exclusive: one of json, body; one of assert, retryUntil; inside auth: one of bearer, basic.

commands:
- request:
method: POST
url: "${API}/login"
json:
email: "${EMAIL}"
password: "${PASSWORD}"
extract:
token: $.token
assert:
status: 200
- request:
url: "${API}/me"
auth:
bearer: "${token}"
retryUntil:
json:
$.status: active
timeout: 20000

Runs one statement against Postgres, MySQL or SQLite — seed a fixture before the UI, check what the UI wrote afterwards, clean up in after. db is the connection string (postgres://…, mysql://…, sqlite:<path>), usually ${DATABASE_URL} from .env; the flow header’s db: is the default for every step that omits it. Every step opens its own connection, so sqlite::memory: starts empty again in the next step — point sqlite: at a file. Exactly one of query (rows come back) and exec (affected count comes back) is required. params is a list of scalars — strings, numbers, booleans, null — bound positionally to the statement’s own placeholders ($1, $2 on Postgres, ? on MySQL and SQLite), so a value is never spliced into SQL text. A ${var} param carries an extracted value and binds as text, so on Postgres cast the placeholder when the column is not text (where id = $1::int). readOnly: true runs inside a transaction that is rolled back: a check that cannot change data. extract takes a JSONPath over the result — the rows array for query ($[0].id), { "affected": N } for exec — and assert takes rows, minRows, none, first (columns on the first row), contains (some row matches every column) and affected. timeout ms (default 30000) covers connecting and the statement. After the step ${sql.rows} or ${sql.affected} is available. The DSN is a secret: reports and logs show scheme://user:***@host/db. Writes are allowed — in CI, point db: at a read-only database user for check-only suites. See API testing.

Keys: db — string; query — string; exec — string; params — list of strings; timeout — ms; readOnly — bool; extract — map; assert — map.

Exclusive: one of query, exec.

commands:
- sql:
exec: "insert into coupons (code, percent) values ($1, $2)"
params: ["QA10", 10]
assert:
affected: 1
- sql:
query: "select status, total from orders where coupon = $1"
params: ["QA10"]
readOnly: true
extract:
total: $[0].total
assert:
rows: 1
first:
status: paid