Appium has been the backbone of mobile test automation for over a decade. It’s survived iOS rewrites, Android fragmentation, and countless framework wars. Maestro is newer, simpler, and faster to adopt—but it’s missing patterns that Appium learned the hard way. For the full side-by-side breakdown, see our Maestro vs Appium comparison.

Here’s what Maestro could borrow from Appium’s playbook.

Lesson 1: Explicit Waits Are Not the Enemy

Maestro’s philosophy: “No need to pepper your tests with sleep() calls.”

Appium’s philosophy: “Give developers control, but make smart defaults easy.”

Appium’s FluentWait:

java
// Full control when you need it
Wait<WebDriver> wait = new FluentWait<>(driver)
    .withTimeout(Duration.ofSeconds(30))
    .pollingEvery(Duration.ofMillis(500))
    .ignoring(NoSuchElementException.class)
    .ignoring(StaleElementReferenceException.class);

WebElement element = wait.until(
    ExpectedConditions.elementToBeClickable(By.id("submit"))
);

What you can configure:

Parameter Appium Maestro
Total timeout ✅ Custom ❌ 17s hardcoded
Polling interval ✅ Custom ❌ Tight loop
Exceptions to ignore ✅ Custom list ❌ None
Custom conditions ✅ Lambda/function ❌ None
Per-element timeout ✅ Yes ❌ No

Why this matters:

java
// Appium: Different waits for different scenarios
WebDriverWait fastWait = new WebDriverWait(driver, Duration.ofSeconds(3));
WebDriverWait slowWait = new WebDriverWait(driver, Duration.ofSeconds(60));

// Quick check for optional element
try {
    fastWait.until(ExpectedConditions.visibilityOf(skipButton));
    skipButton.click();
} catch (TimeoutException e) {
    // Not there, move on quickly
}

// Patient wait for slow API response
slowWait.until(ExpectedConditions.visibilityOf(searchResults));

Maestro can’t do this. Every element gets the same 17-second timeout (see the source code analysis). Fast checks waste time. Slow operations fail.


Lesson 2: Timeouts Should Be Configurable at Every Level

Appium provides timeout configuration at multiple layers:

Global (Capabilities):

java
capabilities.setCapability("newCommandTimeout", 300);
capabilities.setCapability("implicitWait", 10000);

Session-level:

java
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(30));
driver.manage().timeouts().scriptTimeout(Duration.ofSeconds(60));

Per-command:

java
new WebDriverWait(driver, Duration.ofSeconds(45))
    .until(ExpectedConditions.elementToBeClickable(slowButton));

Maestro’s approach:

yaml
# That's it. No configuration.
- tapOn: "Button"

What Maestro could add:

yaml
# Global config (doesn't exist)
config:
  timeouts:
    element: 30000
    animation: 5000
    script: 60000

# Per-command override (doesn't exist)
- tapOn:
    text: "Slow Button"
    timeout: 45000

Lesson 3: The Plugin Architecture

Appium’s killer feature isn’t the core—it’s the extensibility.

Appium’s plugin system:

bash
# Install community plugins
appium plugin install images
appium plugin install execute-driver
appium plugin install relaxed-caps

# Run with plugins
appium --use-plugins=images,execute-driver

What plugins enable:

Plugin Capability
images Visual comparison, find by image
execute-driver Run batched commands
relaxed-caps Flexible capability matching
gestures Custom gesture support
Community plugins Anything the community builds

Maestro’s extensibility:

yaml
# runScript for JavaScript
- runScript:
    script: |
      // Limited to what's exposed
      output.result = "value";

# That's the extent of it

No plugin system. No community extensions. No way to add custom commands without forking the project.

What Maestro could add:

yaml
# Hypothetical plugin system
plugins:
  - maestro-plugin-visual
  - maestro-plugin-accessibility
  - maestro-plugin-performance

# Use plugin commands
- visual.compareScreenshot:
    baseline: "login-screen.png"
    threshold: 0.02

- accessibility.audit:
    level: "AA"

Lesson 4: Retry Logic That Actually Works

Appium doesn’t have built-in retries, but it gives you the primitives to build them properly.

Appium pattern for retries:

java
public void tapWithRetry(By locator, int maxAttempts) {
    int attempts = 0;
    while (attempts < maxAttempts) {
        try {
            WebElement element = wait.until(
                ExpectedConditions.elementToBeClickable(locator)
            );
            element.click();

            // Verify the tap worked
            if (verifyNavigation()) {
                return;
            }
        } catch (Exception e) {
            attempts++;
            if (attempts >= maxAttempts) throw e;
            sleep(1000 * attempts); // Exponential backoff
        }
    }
}

What you control:

  • Number of retries
  • Delay between retries
  • Backoff strategy
  • Success verification
  • Which exceptions to retry

Maestro’s retry:

kotlin
// From source code
private const val MAX_RETRIES_ALLOWED = 3

// Even if you write retry: 10, you get 3

Hardcoded. No backoff. No custom verification. No exception filtering.

What Maestro could add:

yaml
- tapOn:
    text: "Submit"
    retry:
      attempts: 5
      delay: 1000
      backoff: exponential
      verify:
        visible: "Success"

Lesson 5: Multi-Platform Abstraction Done Right

Appium’s architecture separates “what to do” from “how to do it.”

Appium’s driver architecture:

┌─────────────────────────────────────┐
│         Test Code (Java/Python)     │
├─────────────────────────────────────┤
│         WebDriver Protocol          │
├──────────┬──────────┬───────────────┤
│ XCUITest │ UiAuto2  │ Espresso      │
│ Driver   │ Driver   │ Driver        │
├──────────┼──────────┼───────────────┤
│   iOS    │ Android  │ Android       │
│ Devices  │ Devices  │ (in-process)  │
└──────────┴──────────┴───────────────┘

Benefits:

  • Same test code, different drivers
  • Platform-specific optimizations in drivers
  • Community can build new drivers
  • Swap drivers without changing tests

Maestro’s architecture:

┌─────────────────────────────────────┐
│           YAML Test Files           │
├─────────────────────────────────────┤
│         Maestro Monolith            │
│  (iOS + Android in one codebase)    │
├──────────┬──────────────────────────┤
│   iOS    │         Android          │
└──────────┴──────────────────────────┘

Everything is tightly coupled. Platform differences are handled with conditionals in the same codebase. You can’t optimize one without touching the other.

Why this matters:

When Maestro needs to add Flutter support, Windows support, or new iOS APIs, it’s a monolithic change. Appium’s Flutter driver is a separate project that slots into the ecosystem.


Lesson 6: Session Management

Appium treats sessions as first-class citizens.

Appium session control:

java
// Create session with specific state
DesiredCapabilities caps = new DesiredCapabilities();
caps.setCapability("noReset", true);      // Keep app data
caps.setCapability("fullReset", false);   // Don't reinstall
caps.setCapability("app", "/path/to/specific/build.ipa");

// Multiple sessions simultaneously
WebDriver driver1 = new IOSDriver(url, caps1);
WebDriver driver2 = new IOSDriver(url, caps2);

// Clean up explicitly
driver1.quit();
driver2.quit();

What you control:

Capability Effect
noReset Keep app state between tests
fullReset Complete reinstall
app Specific build to test
udid Specific device
autoLaunch Control app launch
eventTimings Performance data

Maestro’s approach:

yaml
appId: com.company.app
# App is launched fresh every time
# No control over reset behavior
# No parallel sessions from same test

What Maestro could add:

yaml
appId: com.company.app
session:
  noReset: true
  keepData: true
  parallelCapable: true

# Or session-per-flow
- launchApp:
    session: "user-logged-in"
    inherit: "base-session"

Lesson 7: Detailed Logging and Debugging

When tests fail, you need information. Appium provides layers of debugging.

Appium’s logging:

bash
# Server logs with timestamps
appium --log-level debug --log appium.log

# Session-specific logs
caps.setCapability("showIOSLog", true);
caps.setCapability("showXcodeLog", true);

What you get:

[HTTP] --> POST /session
[MJSONWP] Calling AppiumDriver.createSession()
[XCUITest] Launching WebDriverAgent...
[XCUITest] WebDriverAgent successfully started
[HTTP] <-- POST /session 200 3842 ms
[HTTP] --> POST /session/xxx/element
[XCUITest] Matched element: XCUIElementTypeButton[@name="Login"]
[HTTP] <-- POST /session/xxx/element 200 127 ms

Every command, every response, every timing.

Maestro’s logging:

[INFO] Tapping on "Login"
[INFO] Tap COMPLETED

Minimal. When something fails, you’re often guessing why.

What Maestro could add:

yaml
# Debug mode with full information
maestro test flow.yaml --debug --log-level verbose

# Per-command logging
- tapOn:
    text: "Login"
    debug: true  # Capture screenshots, hierarchy, timing

Lesson 8: The Ecosystem Matters

Appium’s power isn’t just the tool—it’s the ecosystem.

Appium ecosystem:

Category Examples
Language bindings Java, Python, Ruby, C#, JavaScript
Cloud providers BrowserStack, Sauce Labs, LambdaTest, HeadSpin
Reporting Allure, ExtentReports, ReportPortal
CI integrations Jenkins, GitHub Actions, CircleCI, Azure
IDE support IntelliJ, VS Code, Eclipse
Parallel execution Selenium Grid, custom farms

Maestro ecosystem:

Category Options
Language YAML only
Cloud providers Maestro Cloud (proprietary)
Reporting Built-in only
CI integrations CLI-based (works anywhere)
IDE support VS Code extension
Parallel execution Limited

Maestro’s simplicity is a feature, but the limited ecosystem is a constraint. Enterprise teams often need (see Maestro Test Reports: JUnit, HTML, Allure):

  • Custom reporting formats
  • Integration with existing test infrastructure
  • Language flexibility for complex logic
  • Multi-provider deployment options

What Maestro Gets Right

This isn’t all criticism. Maestro genuinely innovated:

YAML syntax:

yaml
# Readable by anyone
- launchApp
- tapOn: "Login"
- inputText: "[email protected]"
- tapOn: "Submit"
- assertVisible: "Welcome"

vs Appium:

java
driver.findElement(By.accessibilityId("Login")).click();
driver.findElement(By.accessibilityId("email"))
    .sendKeys("[email protected]");
driver.findElement(By.accessibilityId("Submit")).click();
new WebDriverWait(driver, Duration.ofSeconds(10))
    .until(ExpectedConditions.visibilityOfElementLocated(
        By.accessibilityId("Welcome")));

Built-in flakiness handling (when defaults work)

Fast local development (no server startup)

Low barrier to entry (no programming required)


The Best of Both Worlds

Maestro’s syntax + Appium’s reliability = what teams actually need.

Our approach:

We’re building an open-source engine that:

  1. Parses Maestro YAML — keep the beautiful syntax
  2. Runs on Appium — proven, configurable infrastructure
  3. Adds the escape hatches — when defaults fail, adjust them
yaml
# Maestro syntax you know
- tapOn: "Login"

# With Appium's configurability
config:
  timeouts:
    element: 30000
    animation: 5000
  retries:
    enabled: true
    attempts: 5
    backoff: exponential

The simplicity of Maestro for 80% of cases. The power of Appium for the other 20%.

Watch this space.


Series Conclusion

This four-part series examined Maestro’s “built-in flakiness handling” from every angle:

  1. Part 1: Code Deep-Dive — What the source code actually does
  2. Part 2: 15 GitHub Issues — Real users reporting real problems
  3. Part 3: When It’s Not Enough — Scenarios where defaults fail
  4. Part 4: Lessons from Appium — What a decade of mobile testing teaches us

Maestro is a great tool that made mobile testing accessible to more teams. But “built-in flakiness handling” isn’t magic—it’s trade-offs. When those trade-offs don’t fit your needs, you need options.

We’re building those options. Open source. Community driven. Best of both worlds.


Maestro Deep Dive Series