Best Practices for Writing Maestro YAML Flows: A Complete Guide
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.
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:
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:
- 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:
- 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:
- 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:
# 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 theenv: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 withassertVisible:,waitForAnimationToEnd, orrepeat:withdelay:to handle flaky UI deterministically. - Reference variables using
${VAR_NAME}syntax declared in the top-levelenv:block or passed through flow invocations, leveraging theMaestroContextpropagation.
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, 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. 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →