flow-execute
Turn an authored YAML flow into a single run that replays UI actions on a mobile simulator or emulator and reports each step's pass/fail verdict.
Instructions
Run a saved flow from the .argent/flows/ directory, or an explicit boundary-managed flow_path.
Use when a scenario is already authored as YAML and the whole of it should replay in one call with a
per-step verdict; reach for the individual gesture tools when nothing is authored yet, and for
run-sequence when the steps are an ad-hoc list rather than a stored flow.
Steps run in order: launch starts an app from scratch (terminate + relaunch) and waits until it is
ready (on iOS it also pins later element lookups to that app rather than auto-detecting the frontmost
one); tool calls dispatch through the registry (a raw tool step ends that iOS pin, so lookups
auto-detect again until the next launch, though a tool that cannot change the foreground app leaves the
launched id as a fallback for a timed-out auto-detect, and launch-app/restart-app leave the id they
started as that fallback instead); tap/long-press/type resolve a selector to an
element and act on it (tap: { on, times: 2 } double-taps; long-press: { on, duration } presses and
holds; tap/long-press alternatively take a raw normalized point — bare { x, y } or on: { x, y };
any selector may scope its matches geometrically, the CSS combinators read off frames: within: <selector>
(descendant — inside that container's frame), after: <selector> (CSS ~ — following it in reading
order), next: <selector> (CSS + — the nearest such follower, which unlike CSS reaches past a
non-matching neighbour rather than failing), plus any: true (CSS * — legal only WITH a scope and
never beside text/id/role). Scopes nest to disambiguate — within: { id: card, within: { id: list } }
reads "inside card inside list", each container's frame inside the next);
swipe performs one finger flick (swipe: left, or swipe: { from?, direction|to|by, momentum?, duration? } —
direction is the FINGER's travel, the opposite sense of scroll-to's content direction; by: { x?, y? } — signed
0–1 screen fractions, combined length at least 0.03 (a diagonal clears it where neither axis does); duration in ms,
default 300, minimum 150, maximum 10000; each bound is a parse error that rejects the file before any step runs;
momentum: false lands exactly where the finger lifts instead of flinging);
scroll-to scrolls (momentum-free) until a target is visible; pinch zooms
(pinch: { on?, scale } — scale > 1 in, < 1 out; screen center when on is omitted); rotate is the
two-finger rotation gesture (rotate: { on?, by } — degrees, + clockwise, within ±3000°; screen center
when on is omitted; distinct from the rotate tool, which changes device orientation); await waits
for a UI condition, and additionally takes the one condition that has no selector: idle: true waits
until the screen has content and stops moving in BOTH the UI tree and the rendered pixels (it never
fails a run — a screen that never settles passes carrying a warning, which is what makes it safe to
persist; the one idle outcome that does stop the run is an error for a tree source THIS step could not
read at all — a broken window rather than a verdict about the app, which leaves the run not-ok and skips
every later step; it says nothing about WHICH screen settled — a dropped tap leaves the source screen
perfectly idle — so pair it with the element check that names the destination); wait pauses for a fixed number of milliseconds; assert checks one now; snapshot
diffs a screenshot — or, with cropOn: <selector>, one element's cropped region — against a stored
baseline (a missing baseline fails the step — set updateBaselines to adopt the current screen; a
cropped element whose size drifted fails on dimensions); echo annotates; run executes another flow
inline — a YAML path resolved against the directory of the flow file that references it (co-located
runs only).
A selector-less gesture — a coordinate tap/long-press/swipe, or a pinch/rotate with no on — resolves
no frame out of the tree, so an unreadable tree source does NOT stop it the way it stops idle: it
settles best-effort, dispatches anyway, and the step PASSES carrying a warning that quotes the source's
own error. That green says the gesture was SENT, not that it landed. Restore the tree source (usually
relaunch the app so the instrumentation loads), or accept the warning where the app can serve no tree;
the first such gesture proves the outage and later ones spend that verdict without paying the settle
window again. A tree read that comes back, or a relaunch, retires that verdict — which only makes the
next gesture pay a fresh window, and it warns again if the source is still down.
A when: block (condition + steps:, no else) runs its steps only if the condition holds —
checked once with the short assert grace — for one-sided divergences like interstitials and coach
marks; a skipped block reports distinctly and failures inside an entered block are real failures.
A flow that begins with a launch step is a self-contained e2e flow; one that doesn't runs against the
device's current state. Device id is injected by the runner (flows store none) — pass device or
platform to pick one, else the single booted device is used. On Chromium a launch step's value is an
Electron app path ({ chromium: | { path, args } }) the runner boots (on the tool-server host) rather
than an installed app id it relaunches. With no explicit device, a run whose leading launch is
unambiguously chromium (platform: chromium, or a lone { chromium: … } target) boots that app and
starts there — following a leading run:, so a fragment that composes a chromium e2e flow boots too;
otherwise the first launch attaches to an already-running instance and never kills it. Every later
launch — a nested e2e flow's own, or a mid-flow relaunch — boots a fresh instance the run moves onto;
an instance the run already owns for that same app is killed first (its exit awaited) so the
replacement can't lose the race against its single-instance lock. Instances the runner still owns at
run end are torn down then. A launch declaring no id for the run's platform is an error, not a cue to
switch platforms. Every step hard-stops the flow on failure; later steps are reported as skipped.
Returns a structured report ({ flow, device, executionPrerequisite, ok, aborted?, passed, failed,
skipped, errored, steps }) — device is the device the run STARTED on; when launches moved it onto
runner-booted instances, each names its instance in that step's reason and marks the move — run moved off <id>, or retired <id> (same app relaunched) when the instance it left was the one killed —
a relaunch that retired an older owned instance names both.
If a fragment has an execution prerequisite and prerequisiteAcknowledged is not set to true, the tool returns a notice with the prerequisite instead of running. Pass exactly one flow source: name for a saved flow under project_root, or flow_path for an explicit YAML — both together, or neither, fails the call.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| name | No | Name of a saved flow to run from `.argent/flows` (e.g. "settings-explore"). Omit when flow_path is set. | |
| device | No | Device id to run against (iOS UDID, Android/Vega serial, Chromium id) — the id list-devices reports. Auto-detected when omitted, but only when exactly one booted device matches (optionally narrowed by `platform`); with several booted the run fails and lists them, so pass this explicitly whenever more than one device is up. | |
| platform | No | Restrict auto-detection to this platform when several devices are booted. `chromium` does more than filter: with no `device` it SELECTS the self-boot branch for an e2e flow - the runner boots an Electron instance from the `launch` step's chromium value and tears it down after the run (a single-key `launch: { chromium: … }` map selects it on its own, without this parameter). When it selects that branch it never falls back to device auto-detection (a fragment, or an e2e launch map with no `chromium` key, still does), and the launch value must be a real Electron app path on the tool-server host: a bare-string `launch:` - what the recorder writes - holds an installed-app bundle id, so passing `chromium` for one fails the whole run with `Electron boot: path does not exist`. Edit the launch to `{ chromium: <app path> }` first. | |
| flow_file | No | Path to the flow .yaml as readable by the tool-server. Internal — the argent client derives it from project_root and name automatically; leave unset. | |
| flow_path | No | Omit when name is set. Absolute path to a co-located flow .yaml on the client and tool server's shared filesystem. This must be supplied through the file-input boundary. For remote execution, pass name + project_root instead. | |
| project_root | Yes | Absolute path to the calling agent's project root — the cwd it is working in. With name, the saved flow is read from `.argent/flows/<name>.yaml` under this root; with flow_path, the flow, its run: siblings, and baselines all resolve beside the YAML instead, so pass the agent's cwd. | |
| updateBaselines | No | Write/refresh screenshot baselines for `snapshot` steps instead of diffing against them. | |
| prerequisiteAcknowledged | No | Set to true to confirm the execution prerequisite has been met. Required (LLM path) when a fragment defines an executionPrerequisite. |