# Best Practices for Writing Maestro YAML Flows: A Complete Guide

> Master Maestro YAML flows by structuring with metadata, modularizing with runFlow, injecting variables via env, and using smart waits like assertVisible for robust mobile UI tests.

- Repository: [Maestro/Maestro](https://github.com/mobile-dev-inc/Maestro)
- Tags: best-practices
- Published: 2026-03-20

---

**Structure Maestro YAML flows with metadata separated from steps by a `---` delimiter, use `runFlow:` to modularize reusable sequences, leverage `env:` for variable injection, and rely on smart waiting mechanisms like `assertVisible:` instead of hardcoded sleeps to build maintainable and deterministic mobile UI tests.**

The mobile-dev-inc/Maestro framework uses a human-readable YAML DSL to describe UI interactions, assertions, and flow control. Because this DSL is interpreted at runtime by the Maestro execution engine, the way you author flows directly impacts readability, maintainability, and test stability. The following guidelines are distilled from the core library implementation and the repository's extensive test-flow library located in `maestro-test/src/test/resources/`.

## Structure Your Flow Files for Readability

### Separate Metadata from Steps

Every Maestro flow should begin with configuration metadata, followed by a `---` delimiter before the steps list. Place the `appId` (or `bundleId` for iOS) and any environment variables in the top-level `env:` block. This separation makes the purpose and target of the flow immediately obvious to reviewers and follows the convention shown in the official README.

```yaml
appId: com.example.shop
env:
  USERNAME: test_user
---
- launchApp
- assertVisible: "Login"

```

### Use Descriptive Command Keys

Maestro commands are self-documenting when you use the command name as the key and provide human-readable selectors. Avoid inline comments that duplicate command intent; the YAML itself should convey the action. For example, prefer `tapOn: "Save"` over cryptic selectors with explanatory comments.

## Leverage Variables and Environment Configuration

### Declare Variables in the env Block

Store reusable values in the top-level `env:` block or pass them via the `env:` map in `runFlow:` calls. Reference variables using the `${VAR_NAME}` syntax. This prevents duplication and enables data-driven testing across different environments.

Example from [`maestro-test/src/test/resources/060_pass_env_to_env.yaml`](https://github.com/mobile-dev-inc/Maestro/blob/main/maestro-test/src/test/resources/060_pass_env_to_env.yaml):

```yaml
env:
  PRODUCT_ID: "sku-1234"
---
- inputText:
    text: ${PRODUCT_ID}
    selector: "#search"

```

## Implement Conditional Logic and Control Flow

### Use when Clauses for Conditional Execution

Guard steps or entire sub-flows using the `when:` clause, which accepts JavaScript expressions evaluated by the embedded JS engine (Rhino or GraalJS). This enables running the same flow on multiple device configurations without code duplication, as implemented in the conditional execution logic.

Example from [`maestro-test/src/test/resources/065_when_true.yaml`](https://github.com/mobile-dev-inc/Maestro/blob/main/maestro-test/src/test/resources/065_when_true.yaml):

```yaml
- runFlow:
    when:
      "${device.osVersion >= 13}": true
    file: enable_new_feature.yaml

```

## Handle Flaky UI with Retries and Smart Waiting

### Prefer Assertions Over Sleeps

Before each UI interaction, the Maestro engine calls `waitForStableState`, which polls the UI hierarchy until it is idle or a timeout is reached. Place an `assertVisible:` (or other assertion) before interacting with an element to ensure deterministic execution without arbitrary delays.

### Use repeat and delay for Known Flakiness

When UI elements are inherently unstable, use `repeat:` with `delay:` to retry interactions a fixed number of times rather than arbitrary `sleep:` commands. The `retryTapIfNoChange:` parameter can be used when a UI change is expected but not guaranteed.

Example from [`maestro-test/src/test/resources/100_tapOn_multiple_times.yaml`](https://github.com/mobile-dev-inc/Maestro/blob/main/maestro-test/src/test/resources/100_tapOn_multiple_times.yaml):

```yaml
- repeat:
    times: 5
    delay: 0.5
    commands:
      - tapOn:
          text: "Refresh"
          retryTapIfNoChange: true

```

## Modularize Large Flows with Sub-Flows

### Extract Reusable Sequences

Large flows become difficult to maintain. Extract repetitive sequences into separate files and invoke them with `runFlow:`. Keep sub-flow files focused on a single logical action sequence. The `MaestroContext` is passed down the call stack, which is why sub-flows can read variables from parent flows.

Example from [`maestro-test/src/test/resources/064_subflow.yaml`](https://github.com/mobile-dev-inc/Maestro/blob/main/maestro-test/src/test/resources/064_subflow.yaml):

```yaml
- runFlow:
    file: add_to_cart.yaml
    env:
      PRODUCT_ID: "sku-1234"

```

## Complete Working Example

Combine these best practices for writing Maestro YAML flows into a production-ready structure:

```yaml

# purchase_android.yaml

appId: com.example.shop
env:
  USERNAME: test_user
  PASSWORD: secret
---
- launchApp
- assertVisible: "Login"
- inputText:
    text: ${USERNAME}
    selector: "#username"
- inputText:
    text: ${PASSWORD}
    selector: "#password"
- tapOn:
    text: "Sign In"
- runFlow:
    file: add_to_cart.yaml
    env:
      PRODUCT_ID: "sku-1234"
- runFlow:
    when:
      "${device.osVersion >= 13}": true
    file: enable_new_feature.yaml
- assertVisible: "Checkout"
- tapOn: "Checkout"

```

## Summary

- Separate metadata (`appId`, `env`) from steps using a `---` delimiter at the top of Maestro YAML files for immediate clarity.
- Use `runFlow:` to invoke modular sub-flows and pass variables via the `env:` map to avoid duplication and enable composition.
- Implement conditional logic with `when:` clauses containing JavaScript expressions evaluated by the embedded JS engine for device-specific execution paths.
- Replace hardcoded `sleep:` commands with `assertVisible:`, `waitForAnimationToEnd`, or `repeat:` with `delay:` to handle flaky UI deterministically.
- Reference variables using `${VAR_NAME}` syntax declared in the top-level `env:` block or passed through flow invocations, leveraging the `MaestroContext` propagation.

## Frequently Asked Questions

### How do I share variables between parent and child flows in Maestro?

Variables declared in a parent flow's `env:` block are accessible in sub-flows invoked via `runFlow:`. According to the Maestro source code, the execution engine injects a `MaestroContext` that holds environment variables and passes it down the call stack, allowing child flows to read `${VAR_NAME}` references from parent scopes without explicit redeclaration.

### What is the purpose of the `---` delimiter in Maestro YAML files?

The `---` delimiter separates the metadata configuration (including `appId` and `env` variables) from the list of test steps. This structure, as shown in the README examples and test resources like [`maestro-test/src/test/resources/064_subflow.yaml`](https://github.com/mobile-dev-inc/Maestro/blob/main/maestro-test/src/test/resources/064_subflow.yaml), improves readability and clearly distinguishes configuration from executable commands.

### When should I use `repeat:` versus `sleep:` in Maestro flows?

Use `repeat:` with `delay:` when you need to retry an interaction that may fail due to flaky UI elements, as demonstrated in [`maestro-test/src/test/resources/100_tapOn_multiple_times.yaml`](https://github.com/mobile-dev-inc/Maestro/blob/main/maestro-test/src/test/resources/100_tapOn_multiple_times.yaml). Reserve `sleep:` only for waiting on non-UI side effects, since Maestro's built-in `waitForStableState` polling and `assertVisible:` commands provide more reliable synchronization with UI state changes.

### Can I execute JavaScript logic inside Maestro YAML flows?

Yes. Maestro embeds a JavaScript engine (Rhino or GraalJS) within the `MaestroContext`. The `when:` clause on commands or `runFlow:` invocations evaluates JavaScript expressions, enabling complex boolean logic like `${device.os === 'android' && ${ENV_VAR} > 0}` while keeping the YAML syntax concise and readable.