Skip to content

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.

WhatA 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 oneRun /rn-dev-agent:test-feature <description>. On clean verification, the verified walk is saved as an action.
Run oneList 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-repairIf a testID changes, the plugin patches the action against the live UI and retries. Small UI drift is absorbed; broken product logic is not.
WhyKnown 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 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:

  1. 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.
  2. 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 on HomeScreen.
  • It scans saved actions and finds user-login, whose recorded outcome is “logged-in home.”
  • It runs user-login (~4 seconds), arrives at HomeScreen.
  • Then it discovers the cart badge interactively and taps it.

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.

Failure modePure script (Detox, Maestro)Pure LLM (no actions)This plugin
testID renamed in appBreaks; human re-recordsRe-discovers slowly each runcdp_repair_action patches the YAML via fuzzy match against the live snapshot, retries, logs the diff
Button moved / restyledBreaksAdapts but spends turnsRepair handles it; if structure changed, escalates
Product logic changedPasses anyway, masking the bugProbabilistically catches itRefuses to auto-patch a logical break; surfaces the failure to you
Net-new behaviour to verifyn/a — can’t author for unknown flowsRe-derives every sessionDiscovers interactively, then the verified walk auto-saves as a new action
Cost over timeLinear (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.

The hybrid is implemented across four MCP tools (one conceptual family, “Actions”) and two slash commands.

ToolRole
cdp_record_test_save_as_actionConvert 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_actionReplay 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_actionWhen 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.
CommandRole
/rn-dev-agent:list-learned-actionsRead-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-actionSide-effecting execution — looks up the action via the same script, gates safety checks (mutates flag, appId match, ${VAR} coverage), then calls cdp_run_action.

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.

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.

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: active
appId: 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.

/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.

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.

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_statusreplayEngine 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).

  • 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__/.