Actions
An action is a saved, parameterised flow through your app — login, navigating to a screen, completing a multi-step form. The plugin records them automatically when /test-feature verification passes; you replay them in seconds via /run-action, and the agent uses them as prologues when it needs to reach a known state before doing new work.
| What | A saved, replayable Maestro flow with a metadata header and ${KEY} placeholders. |
| Where | .rn-agent/actions/<name>.yaml. The plugin’s home in your project is .rn-agent/. |
| Create one | Run /rn-dev-agent:test-feature <description>. On clean verification, the verified walk is saved as an action. |
| Run one | List with /rn-dev-agent:list-learned-actions; replay with /rn-dev-agent:run-action <name>. The agent also picks an action automatically when it needs to reach a known state. |
| Self-repair | If a testID changes, the plugin patches the action against the live UI and retries. Small UI drift is absorbed; broken product logic is not. |
| Why | Known flows replay in seconds instead of being rediscovered interactively. Repeated setup work like login becomes one fast step. |
Why we have actions — the LLM/pragmatic hybrid
Section titled “Why we have actions — the LLM/pragmatic hybrid”LLM agents are great at understanding intent and improvising on novel screens. They are slow and stochastic at re-deriving things they’ve already seen. A login flow that took fourteen minutes to walk interactively the first time will take fourteen minutes again the next time, every time, if we don’t record it.
Pure-script approaches (Maestro, Detox, Appium) are the opposite: fast and deterministic on the happy path, but they don’t adapt. A renamed testID breaks the script; a human re-records.
Actions sit deliberately in the middle. They are emitted by the agent, not authored by humans — the verification walk that proves the feature works is the same artefact that gets replayed next time. The agent is in charge of when to use an action versus when to discover something new.
The composition pattern
Section titled “The composition pattern”The agent never replays an entire job from a script — that would defeat the point of having an LLM in the loop. Each task is composed of two regimes:
- Pragmatic reusable actions for the predictable parts — login, “navigate to settings → security”, “create a draft task with X title”, switching locale, dismissing the subscription gate, getting back to logged-in home.
- LLM-driven discovery for the part that is actually new — verifying a specific UI state, exercising a new edge case, debugging a regression, walking a freshly built feature.
A worked example. You ask: “tap the cart badge.”
- The agent reads the navigation state. The app is on
LoginScreen, but the cart badge lives onHomeScreen. - It scans saved actions and finds
user-login, whose recorded outcome is “logged-in home.” - It runs
user-login(~4 seconds), arrives atHomeScreen. - Then it discovers the cart badge interactively and taps it.
Measured impact
Section titled “Measured impact”A 3-step task-creation wizard took 13 min 55 s as an interactive agent walk on first run; the same wizard replayed as an action runs in ~4 seconds — a ~210× speed-up. Across 35 stories in the test app, average end-to-end time dropped from ~12 min to ~4 min once the corresponding actions existed. The latency win is most of the point, but the deeper one is determinism: replayed prologues take a fixed number of turns, so the LLM doesn’t waste context re-orienting before getting to the actually-novel work.
How this compares with the alternatives
Section titled “How this compares with the alternatives”| Failure mode | Pure script (Detox, Maestro) | Pure LLM (no actions) | This plugin |
|---|---|---|---|
testID renamed in app | Breaks; human re-records | Re-discovers slowly each run | cdp_repair_action patches the YAML via fuzzy match against the live snapshot, retries, logs the diff |
| Button moved / restyled | Breaks | Adapts but spends turns | Repair handles it; if structure changed, escalates |
| Product logic changed | Passes anyway, masking the bug | Probabilistically catches it | Refuses to auto-patch a logical break; surfaces the failure to you |
| Net-new behaviour to verify | n/a — can’t author for unknown flows | Re-derives every session | Discovers interactively, then the verified walk auto-saves as a new action |
| Cost over time | Linear (every drift needs a human) | Quadratic-ish (every session re-pays full walk) | Sub-linear (drift auto-absorbed, new flows compound the library) |
Said another way: actions are the memory of the LLM loop. Every successful verification adds one. Every drift gets quietly absorbed. Every truly broken flow escalates.
Tool surface
Section titled “Tool surface”The hybrid is implemented across four MCP tools (one conceptual family, “Actions”) and two slash commands.
| Tool | Role |
|---|---|
cdp_record_test_save_as_action | Convert a recorded interactive walk into a first-class .rn-agent/actions/<id>.yaml with metadata header and sidecar state file. Auto-promotes to status: active after the first clean replay. |
cdp_run_action | Replay an action by id with params. Orchestrates maestro_run + optional cdp_repair_action retry. Persists a RunRecord with autoRepair telemetry (passed / failed / refused / skipped, phase timings) so MTTR analysis can see which flows are stable. |
cdp_repair_action | When a run fails with SELECTOR_NOT_FOUND, fuzzy-match the stale selector against the live snapshot, patch the YAML, retry. Refuses on human-edited files (mtime check), >3 repairs/24h, or snapshot infrastructure failure. |
cdp_record_test_* (start / stop / generate / annotate / save / load / list) | The recorder upstream of actions — captures device taps + CDP state assertions during interactive walks, before they get promoted to actions. |
| Command | Role |
|---|---|
/rn-dev-agent:list-learned-actions | Read-only inventory — feedback memories + flows + skeletons + plugin commands. Shared script (scripts/learned-actions.mjs) is the single source of truth, also called by rn-tester / rn-debugger agents before they walk anything manually. |
/rn-dev-agent:run-action | Side-effecting execution — looks up the action via the same script, gates safety checks (mutates flag, appId match, ${VAR} coverage), then calls cdp_run_action. |
The artifact-first protocol
Section titled “The artifact-first protocol”Both the rn-tester and rn-debugger agents are instructed (via feedback_execute_artifacts_before_manual.md) to scan saved actions before composing any new device_* primitives. Manual primitives are the fallback, not the default — that’s the lever that keeps the LLM from paying full-walk latency on flows it has already verified once.
In practice this means a session opens with /list-learned-actions (or its programmatic equivalent), routes through /run-action when a match exists, and only drops to interactive device_press / device_fill / cdp_interact when no action covers the intent.
Where actions live
Section titled “Where actions live”The plugin’s home in your project is .rn-agent/. Actions live in the actions/ subdirectory; sibling folders hold supporting state.
.rn-agent/├── actions/ ← saved actions (commit)│ └── *.yaml├── state/ ← run history, repair history (gitignore)├── recordings/ ← raw captures from cdp_record_test (gitignore)├── skeleton.yaml ← UI semantic-name → testID map (commit)├── nav-graph.yaml ← persisted navigation graph (commit, optional)├── fixtures/ ← seed data for replay (commit)├── proposals/ ← repair proposals queued for review (commit)└── README.md/rn-dev-agent:setup scaffolds the entire directory on first onboarding; /doctor reports on its health.
The plugin’s entire footprint is .rn-agent/. It does not read or write anywhere else in your project.
Creating an action
Section titled “Creating an action”Run /rn-dev-agent:test-feature <feature description>. The plugin walks the feature on the live simulator, verifies UI rendering and internal state, then saves the verified walk as .rn-agent/actions/<feature-slug>.yaml. Each action carries a small metadata header at the top — its intent, what it tags, whether it mutates data, its lifecycle status.
# id: wizard-create-task# intent: Create a task via the 3-step wizard# tags: [task, wizard, create]# mutates: true# status: activeappId: com.rndevagent.testapp---# Maestro YAML body...You can hand-edit actions, but consider that self-repair refuses to touch hand-edited files (mtime check) — to keep the plugin’s automatic upkeep working, prefer rerecording over hand-editing.
Listing and running
Section titled “Listing and running”/rn-dev-agent:list-learned-actions shows what’s saved in this project (with an optional keyword filter):
/rn-dev-agent:list-learned-actions task/rn-dev-agent:run-action replays one by name:
/rn-dev-agent:run-action wizard-create-task -e TITLE="Buy milk" -e PRIORITY=high-e KEY=VALUE fills ${KEY} placeholders inside the action. --platform ios|android targets a specific device when multiple are booted. --dry-run prints the resolved replay command without executing it.
Self-repair, in plain words
Section titled “Self-repair, in plain words”When a testID gets renamed in your app and the action references the old name, replay fails with SELECTOR_NOT_FOUND. The plugin doesn’t give up: it looks at the live UI, finds the most likely new testID via fuzzy matching, patches the YAML, and retries. The repair gets logged to the action’s sidecar file so you can see what changed.
Engine version pinning
Section titled “Engine version pinning”Action replay runs on a pinned maestro-runner version (currently 1.0.9). The pin lives in
scripts/cdp-bridge/src/domain/engine-pin.ts together with the engine’s known quirks;
cdp_status → replayEngine and /doctor report drift, so every session log records which
engine ran. A locally-newer or older install still works but warns once — untested upstream
changes are how silent replay regressions arrive. Bumping the pin follows the upgrade ritual
documented in that module: install the candidate with --version, replay the committed action
corpus on both platforms, reconcile the quirks list, then update the manifest and installer
together (a sync test keeps the two copies equal). Opt into hard enforcement with
RN_ENGINE_PIN_STRICT=1 (refuses proven drift/checksum mismatch at replay time).
On WDA-blind runtimes (iOS 26 bridgeless, or after a transport-blind failure on the same
device), cdp_run_action probes the component tree first and — when the action’s anchor is
visible — replays through the CDP/JS fallback directly instead of paying the doomed ~40s WDA
attempt (RunRecord.blindProbe records the routing; disable with RN_BLIND_PROBE=0).
What actions are NOT
Section titled “What actions are NOT”- Not a magic auto-tester. Self-repair handles small UI drift; it doesn’t fix broken features.
- Not a replacement for hand-written E2E tests. If you maintain a
.maestro/suite for CI, the plugin doesn’t touch it. Actions and your team’s E2E suite live separately. - Not isolated from your project. Actions live in
.rn-agent/actions/, alongside the skeleton and other plugin-managed files. Commit them — they’re as much a part of your project as__tests__/.
See also
Section titled “See also”/rn-dev-agent:test-feature— the command that records actions/rn-dev-agent:list-learned-actions— list saved actions/rn-dev-agent:run-action— replay an action by name/rn-dev-agent:setup— scaffolds.rn-agent/and the dev-bridge- Architecture — where actions sit in the three-layer model