Configuration
Xenon takes its options from Appium's plugin settings. The quick way is a flag, --plugin-xenon-<kebab-case>: maxSessions becomes --plugin-xenon-max-sessions.
appium server --use-plugins=xenon --plugin-xenon-platform=android --plugin-xenon-max-sessions=4
An option you leave out takes its default. For anything beyond a quick try, put the options in an Appium config file under server.plugin.xenon.<key>, as in the example below, which lists only the options it changes. Options that hold an object or a list of objects, such as simulators, are easier to set in a config file. Defaults are shown as JSON.
A row such as autowait.enabled is a field inside the autowait object. Set it in a config file; it has no flag of its own.
A flag for a boolean option can only turn it on: --plugin-xenon-booted-simulators sets bootedSimulators to true, and Appium doesn't start with a value after it, such as --plugin-xenon-booted-simulators=false. To set one to false, such as enableSelfHealing, use a config file.
API keys and other secrets belong in environment variables, not in a config file. See Environment variables.
A config file
A config file needs only the options you change, and Appium fills in every other default. Save this as xenon.yaml and run it with appium server --config xenon.yaml.
server:
use-plugins: [xenon]
plugin:
xenon:
platform: android
maxSessions: 4
Platform & Discovery
| Option | Flag | Type | Default | Description |
|---|---|---|---|---|
platform | --plugin-xenon-platform | string | "both" | Which mobile platform(s) Xenon should discover and orchestrate. One of: ios, android, both. |
androidDeviceType | --plugin-xenon-android-device-type | string | "both" | Which Android device kinds to include: physical devices, emulators, or both. One of: both, real, simulated. |
iosDeviceType | --plugin-xenon-ios-device-type | string | "both" | Which iOS device kinds to include: physical devices, simulators, or both. One of: both, real, simulated. |
simulators | --plugin-xenon-simulators | array of object | [] | Allow-list of iOS simulators (by name + sdk) to expose. Empty array means expose all discoverable simulators. |
simulators[].name (required) | — | string | — | |
simulators[].sdk (required) | — | string | — | |
emulators | --plugin-xenon-emulators | array of object | [] | Android emulators (AVDs) to boot when the server starts, each as { "avdName": "Pixel_7", ... }. Any other field is a launch option passed to the emulator: args, env, language, country, launchTimeout, readyTimeout, retryTimes. Nothing is booted when platform is ios or androidDeviceType is real. It does not limit which emulators are discovered: every emulator that is running is found, as bootedEmulators allows. |
emulators[].avdName (required) | — | string | — | |
bootedSimulators | --plugin-xenon-booted-simulators | boolean | false | Only discover iOS simulators that are already booted. Recommended on machines with many installed simulators — avoids allocating WDA/MJPEG ports for shutdown sims (the WDA pool is 8100-8199, 100 ports). |
bootedEmulators | --plugin-xenon-booted-emulators | boolean | false | Only discover Android emulators that are already booted. |
adbRemote | --plugin-xenon-adb-remote | array of string | [] | List of remote ADB hosts in host:port form (e.g. '192.168.1.50:5037') to discover Android devices on other machines. |
removeDevicesFromDatabaseBeforeRunningThePlugin | --plugin-xenon-remove-devices-from-database-before-running-the-plugin | boolean | false | At startup, also forget what was set for this server's own phones (team, tags, maintenance, reservations), so each comes back as a new phone. Without it a phone keeps those whenever it reconnects, through restarts. A node forgets them for every phone it has; a hub keeps its nodes' phones and theirs either way. |
skipChromeDownload | --plugin-xenon-skip-chrome-download | boolean | true | Skip the automatic ChromeDriver download performed by uiautomator2. Leave true unless you specifically need Xenon to manage Chrome binaries. |
Networking
| Option | Flag | Type | Default | Description |
|---|---|---|---|---|
bindHostOrIp | --plugin-xenon-bind-host-or-ip | string | "auto" | Host/IP advertised in device URLs and the dashboard. Use auto (default), 0.0.0.0, 127.0.0.1, or localhost to pick the best LAN-reachable IPv4 (e.g. 192.168.x.x). Set an explicit address to override. |
remoteMachineProxyIP | --plugin-xenon-remote-machine-proxy-ip | string | — | Public host/URL that clients should use to reach this node when running behind a reverse proxy or NAT. |
proxy | --plugin-xenon-proxy | object | — | The proxy for every call this server makes to another Xenon server or a cloud provider: a hub's session creates, commands, device control, recordings and live-preview sockets for its nodes' phones, a node's calls to its hub, and Xenon's other internal calls. Hosts that NO_PROXY names, and loopback hosts, go direct. Without it these calls take HTTP_PROXY or HTTPS_PROXY. Webhooks, the AI providers and OpenTelemetry export don't use it. |
proxy.host | — | string | — | The proxy's host name or address. Without it the option is ignored, with a warning. |
proxy.port | — | integer | — | The proxy's port. |
proxy.protocol | — | string | — | How Xenon talks to the proxy itself; http when not given. One of: http, https. |
proxy.auth | — | object | — | The proxy's credentials, as { "username": ..., "password": ... }, sent to it as Proxy-Authorization. |
Session Control
| Option | Flag | Type | Default | Description |
|---|---|---|---|---|
maxSessions | --plugin-xenon-max-sessions | number | 8 | Maximum number of Appium sessions this server runs at once. A new session waits until fewer are running or being started. A live preview, a recording, or an SDK lease that has no session on it does not use a slot; a session started on a leased phone does, but is never held back itself. On a hub the count includes its nodes' phones. A value below 1 means no limit. |
deviceAvailabilityTimeoutMs | --plugin-xenon-device-availability-timeout-ms | number | 300000 | How long (ms) a session request waits for a free device before failing. |
deviceAvailabilityQueryIntervalMs | --plugin-xenon-device-availability-query-interval-ms | number | 1000 | How often (ms) the session queue polls for a free device while waiting. Runs only while a create is actively waiting for a busy/unsynced device, so a tighter interval cuts the wait without steady-state cost. |
newCommandTimeoutSec | --plugin-xenon-new-command-timeout-sec | number | 60 | Default Appium newCommandTimeout (seconds) applied when a client does not send one. Also drives the reconciler that releases devices idle past this threshold. |
sessionHeartbeatIntervalMs | --plugin-xenon-session-heartbeat-interval-ms | number | 30000 | How often (ms) each active session writes a heartbeat. The orphan sweeper uses ~3× this interval to detect abandoned sessions. |
Hub ↔ Node
| Option | Flag | Type | Default | Description |
|---|---|---|---|---|
hub | --plugin-xenon-hub | string | — | URL of the Xenon hub this instance should register with as a node (e.g. http://hub.example:4723). Omit to run as a standalone hub. |
sendNodeDevicesToHubIntervalMs | --plugin-xenon-send-node-devices-to-hub-interval-ms | number | 30000 | How often (ms) a node pushes its current device list to the hub. Only used when hub is set. |
checkStaleDevicesIntervalMs | --plugin-xenon-check-stale-devices-interval-ms | number | 30000 | How often (ms) the hub prunes devices from nodes that have stopped heartbeating. |
checkBlockedDevicesIntervalMs | --plugin-xenon-check-blocked-devices-interval-ms | number | 30000 | How often (ms) to re-evaluate manually-blocked devices and the session reconciler that frees orphaned busy devices. |
tlsRejectUnauthorized | --plugin-xenon-tls-reject-unauthorized | boolean | true | Whether to verify TLS certificates for internal outgoing requests. Default is true. Set to false only for dev/test. |
Dashboard & Auth
| Option | Flag | Type | Default | Description |
|---|---|---|---|---|
enableDashboard | --plugin-xenon-enable-dashboard | boolean | false | Keep a full record of each Appium session for the dashboard: its commands, screenshots, logs and performance on the Sessions page, and its heals in Selector Health. Self-healing learns selectors either way, while self-healing is on. Without it, a session on this server's own phones isn't listed at all, while a hub still lists the sessions it sends to its nodes' phones, with their result, why they failed and their video. A node's own setting records nothing for its hub's sessions. Video is recorded either way unless the session turns it off. The dashboard itself is always served at /xenon/, whatever this is set to. |
authDisabled | --plugin-xenon-auth-disabled | boolean | false | Disable API key authentication for all /xenon/api/* endpoints. Use only in local dev environments. |
Health & Lifecycle
| Option | Flag | Type | Default | Description |
|---|---|---|---|---|
healthCheckIntervalMs | --plugin-xenon-health-check-interval-ms | number | 300000 | Default interval (ms) between background device health checks. Overridden when healthCheckSchedule is set. Default 5 minutes — frequent enough to keep battery/thermal badges fresh without hammering devices. A value saved on the dashboard's Settings page replaces this one. |
healthCheckSchedule | --plugin-xenon-health-check-schedule | string | — | Cron expression for the device health-check job (e.g. '0 * * * *' for hourly). When set, takes precedence over healthCheckIntervalMs. A schedule saved on the dashboard's Settings page replaces this one. |
Data Retention
| Option | Flag | Type | Default | Description |
|---|---|---|---|---|
buildCleanupDays | --plugin-xenon-build-cleanup-days | number | 30 | Builds/sessions older than this many days are purged by the cleanup job. A value saved on the dashboard's Maintenance page replaces this one, and applies at the next cleanup run without a restart. |
buildCleanupMaxCount | --plugin-xenon-build-cleanup-max-count | number | 100 | Maximum number of builds to retain. Oldest-first eviction beyond this cap regardless of buildCleanupDays. A value saved on the dashboard's Maintenance page replaces this one. |
buildCleanupSchedule | --plugin-xenon-build-cleanup-schedule | string | "0 0 * * *" | Cron expression for the retention job. Default '0 0 * * *' runs at midnight. A schedule saved on the dashboard's Maintenance page replaces this one, and takes effect at once. |
deleteBuildAssets | --plugin-xenon-delete-build-assets | boolean | true | When true, the cleanup job also deletes session video recordings and screenshots from disk (not just DB rows). A value saved on the dashboard's Maintenance page replaces this one. |
Database
| Option | Flag | Type | Default | Description |
|---|---|---|---|---|
databaseProvider | --plugin-xenon-database-provider | string | — | Has no effect, and is accepted only so older configs still start. Xenon stores its data in SQLite, one database per server, hub or node (see databaseUrl). At startup the database itself decides how its tables are brought up to date: from its migration history if that history matches its tables, otherwise by matching them to this version's schema. One of: sqlite, postgresql. |
databaseUrl | --plugin-xenon-database-url | string | — | Where the SQLite database lives, as file:/path/to/xenon.db. Falls back to the DATABASE_URL env var, then to a file under ~/.cache/xenon. A PostgreSQL URL stops the server at startup. |
AI & Self-Healing
| Option | Flag | Type | Default | Description |
|---|---|---|---|---|
enableSelfHealing | --plugin-xenon-enable-self-healing | boolean | true | Enable the self-healing pipeline (etalon recovery → Native → Fuzzy XML → OCR → Visual AI → LLM) for failed findElement calls. A value saved with the AI self-healing switch on the dashboard's Settings page replaces this one, and applies from the next command without a restart. A session can limit the tiers it uses with xe:options.healingTiers ([] turns healing off for that session); it cannot turn healing on where this is off. |
aiProvider | --plugin-xenon-ai-provider | string | — | AI provider for the LLM healing tier and visual analysis. Also controlled by XENON_AI_PROVIDER. A provider chosen on the dashboard's AI engine page replaces this one, and applies from the next AI call without a restart. One of: gemini, openai, anthropic, ollama. |
aiModel | --plugin-xenon-ai-model | string | — | Override the default model for the selected aiProvider (e.g. 'gemini-1.5-pro', 'gpt-4o', 'claude-sonnet-4-6'). Falls back to XENON_AI_MODEL. A model saved through the dashboard's settings (POST /xenon/api/config) replaces this one. |
aiBaseUrl | --plugin-xenon-ai-base-url | string | — | Custom base URL for the AI provider (e.g. a local Ollama server or an OpenAI-compatible gateway). Falls back to XENON_AI_BASE_URL. A base URL saved through the dashboard's settings (POST /xenon/api/config) replaces this one. |
geminiApiKey | --plugin-xenon-gemini-api-key | string | — | Gemini API key. Prefer setting XENON_GEMINI_API_KEY (or GEMINI_API_KEY) via environment instead of committing it to a config file. |
openaiApiKey | --plugin-xenon-openai-api-key | string | — | OpenAI API key. Prefer setting XENON_OPENAI_API_KEY (or OPENAI_API_KEY) via environment. |
anthropicApiKey | --plugin-xenon-anthropic-api-key | string | — | Anthropic API key. Prefer setting XENON_ANTHROPIC_API_KEY (or ANTHROPIC_API_KEY) via environment. |
Streaming & Recording
| Option | Flag | Type | Default | Description |
|---|---|---|---|---|
maxConcurrentRecordings | --plugin-xenon-max-concurrent-recordings | integer | — | Server-wide hard cap on simultaneous free-form (non-session) screen recordings across all users. Automation session recording is exempt and not counted against this cap. Default 4. When unset, the XENON_MAX_CONCURRENT_RECORDINGS environment variable is used if it is a whole number of at least 1. |
recordingsAssetsPath | --plugin-xenon-recordings-assets-path | string | — | Directory for free-form recording artifacts. When unset, the XENON_RECORDINGS_ASSETS_PATH environment variable is used if it is set, else ~/.cache/xenon/assets/sessions/recordings. |
Autowait
| Option | Flag | Type | Default | Description |
|---|---|---|---|---|
autowait | --plugin-xenon-autowait | object | — | Implicit-wait behavior for findElement/findElements and pre-action enabled checks. |
autowait.enabled | — | boolean | false | Enable autowait globally. Sessions can override via the xenon: setAutowaitProperties execute script. |
autowait.timeoutMs | — | number | 10000 | How long (ms) to keep retrying findElement before throwing NoSuchElement. Self-healing still runs once this timeout expires. |
autowait.intervalBetweenAttemptsMs | — | number | 500 | Sleep (ms) between findElement retries while waiting for the element to appear. |
autowait.excludeEnabledCheck | — | array of string | [] | Action commands (click, setValue, clear) for which the pre-action elementEnabled check should be skipped. |
Network Interceptor
| Option | Flag | Type | Default | Description |
|---|---|---|---|---|
interceptor | --plugin-xenon-interceptor | object | — | Network capture (the interceptor) for sessions on this server's own Android phones: the default each session gets when its own interceptor capability doesn't say. A session's capability wins field by field. Fields: enabled, bufferSize, captureBodies. Android only. |
interceptor.enabled | — | boolean | false | Capture the network traffic of every Android session on this server that doesn't say otherwise. A session turns capture on or off for itself with its interceptor capability (xe:interceptor.enabled, xe:options.interceptor.enabled, the flat interceptorEnabled, ...), which wins over this. While capture runs, the phone's global HTTP proxy points at Xenon; it is put back when the session ends, however it ends. |
interceptor.bufferSize | — | number | 1000 | Maximum number of captured requests to retain in-memory per session before evicting oldest. The default for sessions whose interceptor capability doesn't set bufferSize. |
interceptor.captureBodies | — | boolean | true | Whether to capture request/response bodies. Disable for privacy or to reduce memory usage. The default for sessions whose interceptor capability doesn't set captureBodies. |
Misc
| Option | Flag | Type | Default | Description |
|---|---|---|---|---|
enableJsonLogging | --plugin-xenon-enable-json-logging | boolean | — | Emit structured JSON log lines instead of human-readable text. Recommended for shipping logs to a log aggregator. Off by default. When unset, the XENON_JSON_LOGGING environment variable decides (true turns it on); setting this to true or false overrides the variable. |
Advanced
| Option | Flag | Type | Default | Description |
|---|---|---|---|---|
cloud | --plugin-xenon-cloud | object | — | Cloud configuration object. |
cloud.cloudName (required) | — | string | — | Name of the cloud provider (e.g. browserstack, saucelabs, pcloudy, lambdatest) |
cloud.url (required) | — | string | — | URL of the cloud provider hub |
cloud.apiKey (required) | — | string | — | API Key for the cloud provider |
cloud.apiUrl | — | string | — | API URL for the cloud provider |
cloud.devices (required) | — | array of object | — | |
derivedDataPath | --plugin-xenon-derived-data-path | object | — | Map of derived data paths for iOS simulators/devices. |
recordingCleanupDays | --plugin-xenon-recording-cleanup-days | number | 30 | Live Devices recordings older than this many days are purged (DB row + files) by the cleanup job. Runs on buildCleanupSchedule. |
recordingCleanupMaxCount | --plugin-xenon-recording-cleanup-max-count | number | 100 | Maximum number of recordings to retain. Oldest-first eviction beyond this cap regardless of recordingCleanupDays. |
recordingFailedCleanupDays | --plugin-xenon-recording-failed-cleanup-days | number | 2 | Failed recordings hold no playable file, so they are purged on this much shorter window than recordingCleanupDays. |
streaming | --plugin-xenon-streaming | object | — | Live-streaming options. |
streaming.androidH264 | — | boolean | object | false | Android live preview: false/omitted = MJPEG; true = H.264 via scrcpy; { "source": "scrcpy" | "screenrecord" } to pick the capture source explicitly. Falls back to MJPEG when unsupported. |
sessionMetrics | --plugin-xenon-session-metrics | boolean | true | Record CPU and memory every 2 s for each session on this server's own phones (Android: the app under test and the device; iPhone: device CPU), shown in the session page's Performance panel. Needs the dashboard. |