Revyl
Use Revyl to run agent-device on hosted iOS simulators and Android emulators.
You need a Revyl API key and the @agent-device/revyl plugin. Revyl issues keys to its customers; a
partner integration gets one through Revyl’s partner program.
Revyl is not a WebDriver grid. agent-device rents a device from Revyl’s execution API and drives it
through the same device-proxy actions Revyl’s own clients use, so there is no Appium session, no
lt:options capability set, and no Appium log to read afterwards.
Install the plugin
agent-device plugins add @agent-device/revylThe plugin is an optional npm package installed under AGENT_DEVICE_HOME. After you add or update
it, close open sessions and run agent-device daemon stop with the state directory you use; the
next device command loads the plugin.
Set credentials and connect
export REVYL_API_KEY=...agent-device connect revyl \ --platform ios \ --device "iPhone 16" \ --provider-os-version 18.0 \ --provider-app https://apps.example.com/MyApp-simulator.zipconnect authenticates against the Revyl backend with one cheap request and saves the profile. It
does not rent a device: Revyl picks and boots a device when the first device command allocates the
lease, usually open. --device and --provider-os-version are optional; without them Revyl picks
from its own pool and reports a device agent-device then uses. A model or OS version Revyl has no
device for is reported when the session starts, not by connect.
--provider-app is required and must be a public HTTPS URL. Revyl downloads and installs the build
when it starts the device, so a local path or a provider-specific app reference has no meaning
there. Pass the zipped .app for iOS the way Revyl expects it, and the .apk URL for Android.
open takes the app’s installed bundle identifier or package name, not the file name in that URL.
agent-device open com.example.appagent-device snapshot -iagent-device click 'label="Continue"'agent-device closeagent-device disconnectclose ends the app on the device. disconnect, and any daemon shutdown, ends the Revyl session, so
the device is returned. Revyl also releases an idle session on its own after its idle timeout.
What works on a Revyl session
A Revyl session runs the commands that reach a device through Revyl’s device actions:
snapshotandscreenshot. iOS reads the simulator’s accessibility tree and Android reads its uiautomator dump, sosnapshot -i,--json, and label and selector targeting work the same way they do on a local device.click,press,double tap,longpress, andfillby point or by snapshot reference, andscrollby direction.scrollsends one drag of the planned finger path, so--pixelsand--amountmean the same distance they mean on a local device. Revyl’s drag takes endpoints only, soscroll --duration-msrefuses with that reason rather than running the drag at a speed nobody chose;fill --delay-msis paced by typing one character per pause.open(the session’s app),openwith a deep link or URL, andclosefor the app the session opened.home, andbackon Android. iOS has no system back button; click the app’s own back control.
The plugin refuses every other command with a reason that names the substitute. It does not report a refused command as a success:
typerefuses and points atfill, because Revyl enters text at a point rather than into the field the platform happens to focus.swipeandgesturerefuse. Usescroll, which plans the path itself and sends one drag.keyboard,clipboard,settings,orientation,alert,fold,telephony, andtv-remoteare not supported.install,apps,push,logs,network dump,artifacts, andrecordingsare not supported by this version of the plugin. Use--provider-appto put the build on the device when the session starts.port reverseis unavailable. Publish your app server at a public HTTPS URL instead of pointing Metro atlocalhost.
devices lists only the device your own session rented.
Run it in CI
The plugin reads only REVYL_API_KEY and the optional REVYL_API_URL, so one secret is enough:
- run: npm install -g agent-device && agent-device plugins add @agent-device/revyl- run: agent-device connect revyl --platform android --provider-app "$APP_URL"- run: agent-device open com.example.app && agent-device snapshot -i- run: agent-device disconnect if: always()Keep the disconnect: it is what ends the rented device when a run fails before close.
A running daemon keeps the REVYL_API_KEY it started with. If your shell holds a different one, the
first command that allocates a device refuses before it rents one. Run agent-device daemon stop
with the same --state-dir, then rerun the command. See
Change credentials while the daemon runs.
To use a staging or private Revyl backend, set REVYL_API_URL to its origin, for example
https://backend.example.com. It must be HTTPS; plain HTTP is accepted only for a loopback address,
which is what a local fake of the API is for.
Use it from JavaScript
Install the plugin package as a dependency of your host and pass it to a host you start yourself.
startAgentDevice() runs the daemon in your process and registers the plugin objects you hand it, so
no plugins add step and no REVYL_API_KEY in the environment are needed:
import { createAgentDeviceClient, startAgentDevice } from 'agent-device';import revyl from '@agent-device/revyl';
const host = await startAgentDevice({ stateDir: './.my-app/agent-device-state', plugins: [revyl({ apiKey: 'your-api-key', apiUrl: 'https://backend.example.com' })],});
const scope = { tenant: 'revyl', runId: process.env.GITHUB_RUN_ID ?? 'local-run', leaseBackend: 'ios-instance', leaseProvider: 'revyl',} as const;
const lease = await host.client.leases.allocate({ ...scope, platform: 'ios', device: 'iPhone 16', providerOsVersion: '18.0', providerApp: 'https://apps.example.com/MyApp-simulator.zip',});const client = createAgentDeviceClient({ ...host.clientConfig, ...scope, leaseId: lease.leaseId });
try { await client.apps.open({ app: 'com.example.app' }); await client.capture.snapshot({ interactiveOnly: true }); await client.interactions.click({ selector: 'label="Continue"' });} finally { // Releasing the lease ends the Revyl session, so a failure cannot leave a device rented. // The inner finally closes the host even when the release itself fails; otherwise the embedded // daemon stays alive and nothing retries the release. try { await host.client.leases.release({ ...scope, leaseId: lease.leaseId }); } finally { await host.close(); }}apiUrl is optional and only needed for a staging or private Revyl backend. Leaving both out makes
the plugin read REVYL_API_KEY from the environment, which is also the only path a CLI or MCP session
takes when the daemon loads the plugin from the plugin store.
Troubleshoot
UNAUTHORIZEDfromconnect:REVYL_API_KEYis not a key that backend accepts, orREVYL_API_URLpoints at a different environment than the key was issued for.- A start that never becomes ready reports
provider-not-readyorprovider-start-failedand stops the run it started, so a failedopendoes not leave a rented device. Retry, then check that Revyl has a device of the requested model and OS version. - A device that Revyl released while your session was open reports
provider-session-gone. Runcloseandopenagain to rent a fresh one.