How Maestro Parses YAML Flows: From File to MaestroCommand
Maestro converts YAML flow files into executable MaestroCommand objects through a structured pipeline using Jackson YAML mapping and a custom MaestroFlowParser class in Kotlin.
When you write a mobile UI test in Maestro, the framework must transform your human-readable YAML into runtime instructions. According to the mobile-dev-inc/Maestro source code, this transformation happens in maestro-orchestra/src/main/java/maestro/orchestra/yaml/MaestroFlowParser.kt through a series of discrete parsing stages that handle configuration, command lists, and error recovery.
The Jackson YAML Mapper Configuration
Before parsing begins, Maestro initializes a singleton ObjectMapper configured with YAMLFactory. This mapper specifically disables the --- start marker requirement and registers a custom deserializer for YamlFluentCommand.
The initialization happens in the MAPPER companion object at lines 52-59 of MaestroFlowParser.kt:
private val MAPPER: ObjectMapper = ObjectMapper(YAMLFactory()).apply {
registerModule(
SimpleModule().addDeserializer(
YamlFluentCommand::class.java,
YamlCommandDeserializer()
)
)
// Disable YAML start marker requirement
configure(JsonParser.Feature.ALLOW_YAML_COMMENTS, true)
}
This configuration allows Maestro to parse both simple string commands and complex object commands within the same YAML structure.
The Parsing Pipeline: From File to Commands
The entry point parseFlow at lines 61-73 orchestrates the entire transformation. It receives a file Path and the raw YAML string, then executes a three-stage pipeline:
- Parse the config (
parseConfig) - Parse the command list (
parseCommands) - Convert to runtime commands (
toCommands)
The method signature and core logic look like this:
fun parseFlow(flowPath: Path, yaml: String): List<MaestroCommand> {
val parser = MAPPER.createParser(yaml)
// Stage 1: Config parsing
val config = parseConfig(parser)
// Stage 2: Command list parsing
val yamlCommands = parseCommands(parser, flowPath)
// Stage 3: Conversion and environment application
return yamlCommands
.flatMap { it.toCommands(config) }
.withEnv(config.env)
}
Step 1: Parsing the Config Section
The parseConfig method (lines 95-104) expects the first JSON token to be START_OBJECT. If the YAML does not begin with an object (the appId and other settings), it immediately throws a ParseException with a clear error message.
This validation ensures that every flow file begins with proper configuration metadata before proceeding to the command array.
Step 2: Parsing the Command List
After the config section, parseCommands (lines 69-92) validates that the next token is START_ARRAY. The parser iterates over this array, deserializing each element as a YamlFluentCommand using the custom YamlCommandDeserializer.
This stage catches structural errors such as missing separators or malformed array entries, wrapping them in FlowParseException objects that include file location and context.
Step 3: Custom Deserialization Logic
The YamlCommandDeserializer (embedded at lines 106-122) distinguishes between two command shapes:
- String commands: Simple entries like
launchApporstopApp - Object commands: Complex entries like
tapOn: { text: "OK", optional: true }
For string commands, the deserializer looks up the command in a predefined stringCommands map. For object commands, it reads the field name, finds the matching constructor parameter, and uses Jackson to bind the nested options to the appropriate data class (such as YamlTapOn or YamlInputText).
Command Factories and String Commands
The stringCommands map (lines 59-89) holds lambdas that create YamlFluentCommand instances for every command that can be expressed without additional options. This includes common actions like:
launchAppstopAppclearStatehideKeyboard
When the deserializer encounters a string command, it retrieves the corresponding lambda from this map, which returns a YamlFluentCommand with default-filled option objects. Object-style commands bypass this map and use the generic object-parsing path instead.
Error Handling and Diagnostics
All parsing entry points wrap their logic in try/catch blocks that feed into the wrapException method (lines 67-94). When parsing fails, this analyzer examines the root cause and constructs a user-friendly FlowParseException containing:
- The exact location in the file
- The offending file path
- A concise title and markdown-formatted message
- Optional documentation links for common mistakes
For misspelled commands, the parser uses Levenshtein distance calculations in suggestCommandMessage (lines 19-28) to recommend similar valid commands. Additionally, the checkSyntax family of methods provides lightweight validation that the Studio UI and CLI can invoke without executing the flow.
Practical Usage Examples
Parse a Complete Flow from File
This is the standard CLI usage pattern:
import maestro.orchestra.yaml.MaestroFlowParser
import java.nio.file.Paths
val flowPath = Paths.get("myFlow.yaml")
val yaml = flowPath.readText()
val commands = MaestroFlowParser.parseFlow(flowPath, yaml)
The parseFlow method returns a List<MaestroCommand> ready for the execution engine.
Parse a Single Command Inline
Used by the "Run Command" endpoint or REPL interfaces:
val singleCommand = "- tapOn: { text: \"Submit\" }"
val commands = MaestroFlowParser.parseCommand(
flowPath = Paths.get("/tmp/inline.yaml"),
appId = "com.example.app",
command = singleCommand
)
Validate Syntax Without Execution
For IDE integration or pre-commit hooks:
try {
MaestroFlowParser.checkSyntax(yamlContent)
println("YAML syntax is valid")
} catch (e: FlowParseException) {
println("Syntax error at ${e.location}: ${e.message}")
}
The checkSyntax method selects the appropriate validator (checkFlowSyntax or checkCommandSyntax) based on the JSON tree shape.
Key Source Files
Understanding Maestro's YAML parsing requires familiarity with these specific files in the mobile-dev-inc/Maestro repository:
-
maestro-orchestra/src/main/java/maestro/orchestra/yaml/MaestroFlowParser.kt: Core parser containing mapper configuration, entry points (parseFlow,parseCommand), error handling, and theYamlCommandDeserializerimplementation. -
maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlFluentCommand.kt: Generated Kotlin data classes modeling each supported command and its options; used by the deserializer for type-safe binding. -
maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlConfig.kt: Data class representing the flow's config section, includingappIdand environment variables. -
maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlCommandReader.kt: Thin wrapper utilities used by test frameworks and Maestro Studio to invoke the parser.
Summary
- Maestro uses a Jackson YAML mapper with custom configuration to parse flow files without requiring
---start markers. - The
MaestroFlowParser.parseFlowmethod orchestrates a three-stage pipeline: config parsing, command list parsing, and conversion toMaestroCommand. - String commands (like
launchApp) resolve through a factory map, while object commands use Jackson's data binding to populate command-specific data classes. - Comprehensive error handling via
wrapExceptionprovides file locations, suggestions for misspelled commands, and markdown-formatted error messages. - Syntax validation methods allow tools to check YAML correctness without executing tests, supporting IDE integrations and CI pipelines.
Frequently Asked Questions
How does Maestro handle YAML syntax errors in flow files?
Maestro catches parsing exceptions in the wrapException method and converts them into FlowParseException objects. These exceptions contain the exact file location, the path to the offending file, and a markdown-formatted message. For misspelled command names, the parser uses Levenshtein distance calculations to suggest the closest valid command.
What is the difference between string commands and object commands in Maestro YAML?
String commands are simple YAML strings like launchApp that require no parameters; Maestro resolves these through the stringCommands factory map to create commands with default options. Object commands use YAML mapping syntax like tapOn: { text: "OK" }; the custom YamlCommandDeserializer reads the field name and uses Jackson to bind the nested properties to type-specific data classes such as YamlTapOn.
Can I validate a Maestro YAML file without running the test?
Yes. The MaestroFlowParser class provides checkSyntax and related methods (checkFlowSyntax, checkCommandSyntax) that parse and validate YAML structure without creating executable commands. These methods throw FlowParseException on validation failures, making them suitable for IDE linting, pre-commit hooks, and Studio UI feedback loops.
Where does the conversion from YAML to runtime commands happen?
The conversion occurs in maestro-orchestra/src/main/java/maestro/orchestra/yaml/MaestroFlowParser.kt. Specifically, the toCommands extension method transforms the list of YamlFluentCommand objects (produced by Jackson deserialization) into MaestroCommand instances that the Maestro execution engine can run. This happens after parseConfig and parseCommands complete their work.
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 →