How to Customize YAML Flow Parsing in Maestro: A Complete Developer’s Guide
Maestro does not provide a runtime plugin mechanism for YAML parsing; customizing flow syntax requires modifying the open-source maestro-orchestra source code and recompiling the library.
Maestro is a popular open-source mobile UI testing framework that interprets YAML files to automate user interactions. While the framework ships with dozens of built-in commands, teams with specialized testing needs often want to customize YAML flow parsing in Maestro to support proprietary operations or simplify repetitive test patterns. Because the command vocabulary is statically compiled into the parser, extending Maestro requires a deep dive into its Jackson-based deserialization architecture.
How Maestro Parses YAML Flow Files
The entry point for all YAML flow parsing is MaestroFlowParser located in maestro-orchestra/src/main/java/maestro/orchestra/yaml/MaestroFlowParser.kt. This class constructs a Jackson ObjectMapper and registers a custom YamlCommandDeserializer that inspects each YAML token to determine which command class to instantiate.
The Two Command Registries
During deserialization, the parser categorizes every potential command into one of two hard-coded lookup tables defined at compile time:
-
stringCommands: Defined inMaestroFlowParser.kt(lines 59‑84), this map handles scalar YAML tokens—commands that can be expressed as simple strings without parameters, such aslaunchAppwhen used without arguments. -
objectCommands: Automatically derived from the primary constructor ofYamlFluentCommand(lines 54‑57), this collection handles mapping tokens—commands that require a map of key-value pairs, such astapOnwith selector options.
When the deserializer encounters a YAML scalar, it invokes parseStringCommand against the stringCommands map. When it encounters an object (a YAML mapping), it invokes parseObjectCommand against the objectCommands map. If the command name is absent from both collections, the parser throws a ParseException with an "Invalid Command" error and halts execution.
Extending Maestro with Custom Commands
Because both registries are hard-coded at compile time, you cannot add commands via configuration files or environment variables. You must extend the source code following these five steps:
Step 1: Define the YAML DTO
Create a new data class in the maestro.orchestra.yaml package to represent your command’s parameters. For example, to create a showToast command:
package maestro.orchestra.yaml
import com.fasterxml.jackson.annotation.JsonProperty
data class YamlShowToast(
@JsonProperty("message") val message: String,
@JsonProperty("duration") val duration: Long? = null,
)
Step 2: Extend the Central Command DTO
Add your new DTO as a nullable property to YamlFluentCommand.kt. This automatically registers it in the objectCommands collection because Jackson introspects the primary constructor:
// Inside YamlFluentCommand.kt data class definition
val showToast: YamlShowToast? = null,
Step 3: Register String Commands (Optional)
If your command requires no arguments and should work as a scalar string (e.g., - restartApp), manually add an entry to the stringCommands map in MaestroFlowParser.kt:
"restartApp" to { location -> YamlFluentCommand(
_location = location,
launchApp = YamlLaunchApp(
appId = null,
clearState = true // Forces a full restart
)
)}
Object commands with parameters skip this step; they are automatically available once added to YamlFluentCommand.
Step 4: Implement Command Conversion
Navigate to the _toCommands method inside YamlFluentCommand.kt (the large when block) and add a branch that converts your YAML DTO into a concrete MaestroCommand:
showToast != null -> listOf(
MaestroCommand(
ShowToastCommand(
message = showToast.message,
duration = showToast.duration
)
)
)
You must also create the corresponding command execution class (e.g., ShowToastCommand.kt) in the maestro.orchestra.command package to define the runtime behavior.
Step 5: Rebuild the Library
Compile your changes to generate the updated CLI and libraries:
./gradlew :maestro-orchestra:assemble
After rebuilding, your custom syntax is immediately available in flow files:
appId: com.example.myapp
---
- showToast:
message: "Custom command active"
duration: 3000
Critical Source Files for Customization
Understanding the following files is essential when you customize YAML flow parsing in Maestro:
-
maestro-orchestra/src/main/java/maestro/orchestra/yaml/MaestroFlowParser.kt: Contains the Jackson configuration, thestringCommandsmap, and theYamlCommandDeserializerimplementation. This is where command name resolution happens. -
maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlFluentCommand.kt: The central DTO that aggregates all possible YAML commands. Its constructor defines theobjectCommandslookup table, and its_toCommandsmethod handles the conversion from YAML representation to executableMaestroCommandobjects. -
maestro-orchestra/src/main/kotlin/maestro/orchestra/yaml/Yaml*.kt: Package containing individual command DTOs (e.g.,YamlTapOnElement.kt,YamlLaunchApp.kt). Create new files here following the existing naming convention. -
maestro-orchestra/src/main/java/maestro/orchestra/command/: Directory containing concrete command implementations that perform actual device interactions. Any new YAML command requires a corresponding class here to handle execution logic.
Summary
- Maestro’s YAML parser is static: Command vocabularies are hard-coded in
MaestroFlowParser.ktandYamlFluentCommand.ktat compile time. - No runtime plugins: You cannot add commands via external configuration; source modification and recompilation are mandatory.
- Two registration paths: Use
stringCommandsfor scalar shortcuts and extendYamlFluentCommand’s constructor for object-based commands with parameters. - Conversion is manual: Every new YAML DTO requires an explicit branch in
YamlFluentCommand._toCommandsto map it to an executable command class. - Rebuild required: Changes take effect only after recompiling the
maestro-orchestramodule or the entire project.
Frequently Asked Questions
Can I add custom Maestro commands without modifying the source code?
No. As implemented in mobile-dev-inc/Maestro, the stringCommands and objectCommands lookup tables are immutable after compilation. There is no configuration file, environment variable, or dynamic classloader mechanism that allows runtime command registration. You must fork the repository, implement your changes, and rebuild the binary.
What is the difference between stringCommands and objectCommands in Maestro?
stringCommands handles YAML scalars—simple command names without parameters (e.g., - back). It is explicitly defined as a map in MaestroFlowParser.kt. objectCommands handles YAML mappings—commands with key-value configurations (e.g., - tapOn: { id: "button" }). It is implicitly built from the primary constructor properties of YamlFluentCommand via Jackson’s introspection.
Where should I implement the actual execution logic for my custom command?
The YAML parsing layer only converts text into data objects. To implement behavior, create a new class in maestro-orchestra/src/main/java/maestro/orchestra/command/ (e.g., ShowToastCommand.kt) that implements the MaestroCommand interface. Then reference this class in the _toCommands method of YamlFluentCommand.kt to bridge the parser with the executor.
Will my custom YAML commands break when I update Maestro?
Yes, if you simply upgrade the CLI binary without re-applying your patches. Because you are modifying core parser files like MaestroFlowParser.kt and YamlFluentCommand.kt, you must maintain a fork or patch set. When updating, you need to rebase your custom command implementations onto the latest upstream version and resolve any merge conflicts in the command registries or conversion logic.
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 →