# How Maestro Parses YAML Flows: From File to MaestroCommand

> Discover how Maestro parses YAML flow files into executable MaestroCommand objects using Jackson YAML mapping and its custom Kotlin parser. Learn the technical details.

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

---

**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`](https://github.com/mobile-dev-inc/Maestro/blob/main/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`](https://github.com/mobile-dev-inc/Maestro/blob/main/MaestroFlowParser.kt):

```kotlin
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:

1. **Parse the config** (`parseConfig`)
2. **Parse the command list** (`parseCommands`)
3. **Convert to runtime commands** (`toCommands`)

The method signature and core logic look like this:

```kotlin
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 `launchApp` or `stopApp`
- **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:

- `launchApp`
- `stopApp`
- `clearState`
- `hideKeyboard`

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:

```kotlin
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:

```kotlin
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:

```kotlin
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`](https://github.com/mobile-dev-inc/Maestro/blob/main/maestro-orchestra/src/main/java/maestro/orchestra/yaml/MaestroFlowParser.kt)**: Core parser containing mapper configuration, entry points (`parseFlow`, `parseCommand`), error handling, and the `YamlCommandDeserializer` implementation.

- **[`maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlFluentCommand.kt`](https://github.com/mobile-dev-inc/Maestro/blob/main/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`](https://github.com/mobile-dev-inc/Maestro/blob/main/maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlConfig.kt)**: Data class representing the flow's config section, including `appId` and environment variables.

- **[`maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlCommandReader.kt`](https://github.com/mobile-dev-inc/Maestro/blob/main/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.parseFlow`** method orchestrates a three-stage pipeline: config parsing, command list parsing, and conversion to `MaestroCommand`.
- **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 `wrapException` provides 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`](https://github.com/mobile-dev-inc/Maestro/blob/main/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.