How to Reuse or Import Maestro Flows: Complete Guide to Modular Test Automation
Yes, Maestro flows can be reused and imported using the runFlow command, which loads external YAML files or inline command blocks and executes them within the current orchestration session without breaking the test context.
The mobile-dev-inc/Maestro repository provides a powerful YAML-based DSL for mobile UI automation. Understanding how to reuse or import Maestro flows enables you to build maintainable test suites that eliminate duplication and support modular architecture through composition.
The runFlow Command Architecture
Maestro implements flow reuse through the runFlow command, a first-class DSL element that functions as a dynamic loader for sub-flows. When the interpreter encounters this command, it parses the referenced content into a list of MaestroCommand objects and executes them sequentially as part of the current session.
Parsing and Validation in YamlFluentCommand
The core implementation resides in maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlFluentCommand.kt, specifically within the runFlowCommand() method (lines 511-545). This function validates the command structure, resolves the file path (or processes inline commands), merges environment variables, and constructs a RunFlowCommand object. The method handles both runFlow.file references and runFlow.commands blocks, converting YAML definitions into executable command objects.
Execution in the Orchestra Engine
The actual execution occurs in maestro-orchestra/src/main/java/maestro/orchestra/Orchestra.kt via the runFlow() method (lines 164-170). This method receives the list of parsed commands, runs them sequentially through the orchestration engine, and returns a boolean success flag. Because the sub-flow executes within the existing session, all state—including the current screen and application context—persists across the boundary between parent and child flows.
Flow Reuse Patterns and Implementation Details
File-Based Flow Import with Hot Reloading
Flows can import external YAML files using relative paths. The runFlow command resolves these paths and establishes file watchers on imported flows, enabling automatic reload during development. This implementation detail appears in YamlFluentCommand.kt (lines 646-654), where each runFlow invocation configures its own watch files to detect changes in the dependency graph.
Inline Command Blocks
For scenarios requiring encapsulated logic without separate files, runFlow accepts an inline commands list. The parser converts these entries into MaestroCommand objects directly within runFlowCommand(), treating them equivalently to file-based definitions while maintaining lexical scope.
Parameterized Sub-flows Using Environment Variables
Sub-flows accept parameters through the env map, which runFlowCommand merges into the child flow's environment using .withEnv(runFlow.env). This mechanism allows parent flows to inject dynamic values—such as credentials or configuration flags—into reusable components without modifying the underlying YAML files.
Conditional Execution with Runtime Evaluation
The when clause enables conditional flow import. During parsing, runFlow.when?.toCondition() (lines 536-538) compiles the condition into a Condition object. At runtime, the orchestration engine evaluates this condition and only executes the sub-flow if the predicate returns true, allowing platform-specific or state-dependent reuse patterns.
Practical Code Examples
Importing an External Flow File
# parent.yaml
- tapOn:
text: "Open Settings"
- runFlow:
file: "subflow.yaml"
label: "Settings Navigation"
# subflow.yaml
- tapOn:
text: "Settings"
- scroll:
direction: down
distance: 400
The file parameter resolves relative to the parent YAML location, and the orchestration engine executes subflow.yaml's commands as if they were inline.
Using Inline Commands
- runFlow:
commands:
- tapOn:
text: "Profile"
- swipe:
direction: up
distance: 300
env:
USER_MODE: "demo"
This pattern encapsulates transient logic without creating separate files, while still supporting environment variable injection.
Conditional Flow Import
- runFlow:
file: "androidOnly.yaml"
when: ${platform == "android"}
The JavaScript expression in when is evaluated at runtime, and the flow only imports when the condition evaluates to true.
Passing Dynamic Variables
- runFlow:
file: "loginFlow.yaml"
env:
USERNAME: ${username}
PASSWORD: ${password}
The env map merges with the sub-flow's environment, allowing parameterized reuse of authentication or setup routines across multiple test scenarios.
Summary
- Use
runFlowto import external YAML files or define inline command blocks within the current orchestration session. - Reference implementation resides in
YamlFluentCommand.kt(parsing) andOrchestra.kt(execution), with specific logic for command validation, environment merging, and sequential execution. - Support nesting arbitrarily deep, with each level maintaining its own file watchers for development hot-reloading.
- Apply conditions using the
whenclause to enable platform-specific or state-dependent flow selection. - Inject parameters via the
envmap to create reusable, parameterized test components.
Frequently Asked Questions
Can Maestro flows be nested multiple levels deep?
Yes, Maestro supports arbitrary nesting of runFlow commands. Each nested invocation maintains its own context while preserving the parent session state. The file watcher implementation in YamlFluentCommand.kt (lines 646-654) ensures that changes to any level of the dependency tree trigger appropriate reloads during development.
How do environment variables behave when importing flows?
Environment variables flow downward through the env parameter. When you specify env in a runFlow invocation, the runFlowCommand() method merges these values into the sub-flow's environment using .withEnv(). The child flow can access these variables using standard interpolation syntax, but changes within the child do not propagate back to the parent scope.
Does runFlow support conditional execution based on platform?
Yes, the when clause enables conditional import. The parser compiles the condition into a Condition object via runFlow.when?.toCondition() (lines 536-538), and the orchestration engine evaluates this at runtime. This allows you to import Android-specific or iOS-specific flows conditionally using JavaScript expressions like ${platform == "android"}.
What happens to the test session when a sub-flow fails?
Because runFlow executes within the existing orchestration session via Orchestra.runFlow() (lines 164-170), a failure in the sub-flow propagates to the parent flow immediately. The method returns a boolean success flag, and the test runner handles the failure according to the parent flow's error configuration—either stopping execution or continuing based on the optional parameter or retry logic defined in the parent.
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 →