AI-powered mobile automation agent for Android and iOS. Tell it what to do in plain English — it figures out what to tap, type, and swipe.
|
|
npm install -g appclaw
Create a .env file in your working directory:
cp .env.example .env
git clone https://github.com/AppiumTestDistribution/appclaw.git
cd appclaw
npm install
cp .env.example .env
Edit .env based on your preferred mode:
Vision mode (recommended)
Screenshot-first mode using Stark (df-vision + Gemini) for element location. Requires a Gemini API key.
LLM_PROVIDER=gemini
LLM_API_KEY=your-gemini-api-key
LLM_MODEL=gemini-3.1-flash-lite
AGENT_MODE=vision
DOM mode
Uses XML page source to find elements by accessibility ID, xpath, etc. No vision needed — works with any LLM provider.
LLM_PROVIDER=gemini # or anthropic, openai, groq, ollama
LLM_API_KEY=your-api-key
AGENT_MODE=dom
AppClaw supports both Android and iOS (simulators + real devices). On macOS, you'll get an interactive prompt to choose. For CI or to skip prompts, use flags:
# Android (default — no flags needed)
appclaw "Open Settings"
# iOS Simulator (auto-selects the booted simulator)
appclaw --platform ios --device-type simulator "Open Settings"
# iOS Simulator — pick by name
appclaw --platform ios --device-type simulator --device "iPhone 17 Pro" "Open Settings"
# iOS Real Device — pick by UDID
appclaw --platform ios --device-type real --udid 00008120-XXXX "Open Settings"
# Env vars work too (great for .env or CI)
PLATFORM=ios DEVICE_TYPE=simulator appclaw "Open Settings"
Tip: If only one simulator is booted, it's auto-selected — no
--udidneeded.
Run against a remote Appium grid — BrowserStack, Sauce Labs, LambdaTest, or any
self-hosted grid — instead of a local device. Set CLOUD_PROVIDER and the
CLOUD_* creds; AppClaw builds the hub URL, skips local device discovery, and
routes the session to the grid. appclaw init can prompt for these and write
them to .env.example.
# .env — BrowserStack
CLOUD_PROVIDER=browserstack
CLOUD_USERNAME=your-user
CLOUD_ACCESS_KEY=your-key
CLOUD_DEVICE_NAME=Google Pixel 8
CLOUD_OS_VERSION=14
CLOUD_APP=bs://<hashed-app-id> # optional; app to install
# Sauce Labs (region defaults to us-west-1; override with CLOUD_REGION)
CLOUD_PROVIDER=saucelabs
CLOUD_USERNAME=your-user
CLOUD_ACCESS_KEY=your-key
CLOUD_DEVICE_NAME=iPhone 14
CLOUD_OS_VERSION=16
# Self-hosted grid — bring your own hub URL (auth may be embedded)
CLOUD_PROVIDER=custom
CLOUD_SERVER_URL=https://user:key@grid.internal:4444/wd/hub
CLOUD_DEVICE_NAME=Pixel_7
CLOUD_OS_VERSION=14
AppClaw sets only the essentials (platformName, appium:automationName,
device, OS, app). Provider-specific options — bstack:options,
sauce:options, lt:options, real-vs-emulator flags, video, network logs — go
in a capabilities file passed via --caps (or CAPABILITIES_FILE / the SDK
capabilitiesFile option), which is merged on top and wins:
// caps.json → appclaw --flow login.yaml --caps caps.json
{
"bstack:options": { "projectName": "AppClaw", "buildName": "smoke", "networkLogs": true }
}
CLOUD_BUILD_NAME / CLOUD_PROJECT_NAME are convenience shortcuts mapped into
the active provider's namespace, so common dashboard labels don't need a caps file.
# Interactive mode (prompts for platform + goal)
appclaw
# Pass goal directly
appclaw "Open Settings"
appclaw "Search for cats on YouTube"
appclaw "Turn on WiFi"
appclaw "Send hello on WhatsApp to Mom"
# Or with npx (no global install)
npx appclaw "Open Settings"
When running from a local clone, use npm start instead:
npm start
npm start "Open Settings"
Export a replayable test — pass --export and the agent's trajectory is written as a runnable vitest spec when the goal completes:
# Default path: $EXPORT_DIR/<goal-slug>.test.ts (defaults to .appclaw/exports/)
appclaw --export "Open YouTube and search for Appium 3.0"
# Bare filename → EXPORT_DIR/<name>
appclaw --export youtube.test.ts "Open YouTube and search for Appium 3.0"
# Path with directory hint → used verbatim
appclaw --export tests/e2e/youtube.test.ts "Open YouTube"
# Override the directory for one run
appclaw --export-dir tests/recorded --export youtube.test.ts "Open YouTube"
# Or set it persistently in .env
echo 'EXPORT_DIR=tests/recorded' >> .env
The export drops the wrong-direction branch when verification rejected a done, preserves the auto-launched app as the first step, and translates internal agent tools back to natural language so the generated test reads like English. See the SDK section below for what the test file looks like.
Run declarative automation steps from a YAML file — fast, repeatable, zero LLM cost:
appclaw --flow examples/flows/google-search.yaml
Flows support both structured and natural language syntax:
Structured:
appId: com.android.settings
name: Turn on WiFi
---
- launchApp
- wait: 2
- tap: 'Connections'
- tap: 'Wi-Fi'
- done: 'Wi-Fi turned on'
Natural language:
name: YouTube search
---
- open YouTube app
- click on search icon
- type "Appium 3.0" in the search bar
- perform search
- scroll down until "TestMu AI" is visible
- verify video from TestMu AI is visible
- done
Supported natural language patterns include: open <app>, click/tap <element>, type "text", scroll up/down, swipe left/right, scroll down until "X" is visible, wait N seconds, go back, press home, verify/assert <element> is visible, press enter, and done. Questions like "whats on the screen?" or "how many items are there?" are answered via vision without executing any action.
Run the same flow on N devices simultaneously, or distribute a suite of flows across N workers:
Same flow, N devices — add parallel: N to the flow's metadata:
name: youtube_parallel
platform: android
parallel: 2
---
- open YouTube app
- search for "Appium 3.0"
- assert "TestMu AI" is visible
- done
appclaw --flow youtube.yaml # spins up 2 devices, runs flow on both concurrently
Suite: different flows, N workers — a suite YAML lists flows and a worker count:
name: youtube_suite
platform: android
parallel: 2
flows:
- flows/login.yaml
- flows/search.yaml
- flows/playback.yaml
appclaw --flow youtube-suite.yaml # 2 devices pull from queue until all 3 flows finish
The VS Code extension shows a live multi-device grid — each device card updates in real time with a per-device step log, progress bar, and pass/fail result. Failed flows can be re-run with Re-run Failed from the summary notification.
Build YAML flows interactively on a real device — type commands and watch them execute live:
appclaw --playground
# iOS simulator
appclaw --playground --platform ios --device-type simulator
# Specific device
appclaw --playground --platform ios --device-type simulator --device "iPhone 17 Pro"
Features:
/help, /steps, /export, /clear, /device, /disconnectExport formats — /export dispatches by extension:
> /export my-flow.yaml # YAML flow (default behaviour)
> /export tests/youtube.test.ts # SDK vitest spec
> /export youtube.test.ts # bare filename → EXPORT_DIR/youtube.test.ts
Bare filenames for SDK tests land in EXPORT_DIR (default .appclaw/exports); paths with a directory hint are used verbatim. YAML files stay in the current directory regardless.
Drive a device programmatically from a vitest / jest / mocha test. The SDK exposes the same natural-language layer as the playground, so steps you build interactively can be lifted into a test file as-is.
import { AppClaw, AppClawAssertionError } from 'appclaw';
import { describe, it } from 'vitest';
import 'dotenv/config';
describe('YouTube smoke', () => {
it('searches and verifies a result', async () => {
const app = new AppClaw({
provider: 'gemini',
apiKey: process.env.LLM_API_KEY,
platform: 'android',
agentMode: 'vision',
video: true, // record screen, embed in report
mcpDebug: false, // silence appium-mcp stderr noise
});
await app.run('open YouTube app');
await app.run('tap search icon');
await app.run('type "Appium 3.0"');
await app.run('tap first result');
// Throws AppClawAssertionError on failure — vitest marks the test red.
// The error message includes the original claim, the LLM's reason (in
// vision mode), and a snapshot of visible texts (in DOM mode).
await app.verify('the video by TestMu AI is visible');
await app.teardown(); // closes the appium session, writes the report
}, 120_000);
});
Three execution surfaces:
| Method | Use when |
|---|---|
app.run(instruction, options?) |
One natural-language step. Returns { success, action, message } — does NOT throw on failure. options overrides wait/scroll config for this call only. |
app.verify(claim) |
Assertion. Throws AppClawAssertionError so the test framework records a failure. |
app.runFlow(path) |
Execute a YAML flow file end-to-end. |
app.runGoal(goal, opts?) |
Hand the goal to the LLM agent (same as CLI agent mode). Supports exportPath (see below). |
Recording goal runs as replayable tests — pass exportPath to runGoal() and the agent's trajectory is written as a runnable vitest spec when it finishes:
await app.runGoal('open YouTube, search for Appium 3.0, play first result', {
exportPath: 'tests/e2e/youtube.test.ts',
});
The exported file replays via app.run(...) calls — no LLM cost, no agent loop. The exporter:
launch_app step the preprocessor handled (so the launch isn't missing from the replay)done decision — the recovery path the agent took after a verification failure is the only one preservedfind_and_click, find_and_type, etc.) back into natural language so the test reads like EnglishRead the four caveats embedded in the generated file header before running it in CI — replays don't inherit the agent's safety net.
Per-command overrides — app.run() takes an optional second argument that wins over the instance defaults for that one step. Useful for a slow screen that needs a longer wait, or a tight list that needs a shorter scroll:
// Wait up to 20s for this specific (slow-loading) screen
await app.run('click on Dashboard', { waitTimeout: 20000 });
// Scroll a short distance, up to 5 times, to find an item
await app.run('scroll down until Karma is visible', { scrollMode: 'short', scrollTimes: 5 });
// A single full-screen swipe
await app.run('swipe up', { scrollMode: 'full' });
RunOptions field |
Purpose |
|---|---|
waitTimeout |
Implicit-wait timeout (ms) for this command's target element. Overrides instance value. |
waitInterval |
Poll cadence (ms) for this command's implicit wait. |
scrollMode |
Scroll/swipe distance: short (~30%) / medium (~60%) / full (~90%) of the screen. |
scrollTimes |
Repeat count (plain swipe) or max scroll attempts (scroll … until …). |
scrollMode / scrollTimes can also be set on the constructor as instance-wide defaults.
Constructor options (all optional — env vars fall through):
| Option | Default | Purpose |
|---|---|---|
$ claude mcp add AppClaw \
-- python -m otcore.mcp_server <graph>