# How Maestro Handles Complex YAML Structures: From Parsing to Execution

> Discover how Maestro expertly parses and executes complex YAML structures by converting them into Kotlin objects using Jackson YAML for robust mobile automation.

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

---

**Maestro handles complex YAML structures by treating flow files as a domain-specific language (DSL), parsing them through a layered pipeline that converts every syntactic construct into strongly-typed Kotlin objects using Jackson YAML before execution.**

Maestro, the mobile UI testing framework by mobile-dev-inc, processes complex YAML flow definitions through a sophisticated parsing architecture. Understanding how Maestro handles complex YAML structures reveals why it can support nested commands, conditional repeats, and workspace-wide orchestration while maintaining type safety and clear error diagnostics.

## The Entry Point: YamlCommandReader

The parsing journey begins in [`YamlCommandReader.kt`](https://github.com/mobile-dev-inc/Maestro/blob/main/YamlCommandReader.kt), which serves as the public API for flow parsing. This object reads raw YAML text and forwards it to the core parser while wrapping all exceptions in rich, inline error messages.

```kotlin
object YamlCommandReader {
    fun readCommands(flowPath: Path): List<MaestroCommand> = mapParsingErrors(flowPath) {
        val flow = flowPath.readText()
        MaestroFlowParser.parseFlow(flowPath, flow)
    }
    // …readSingleCommand, readConfig, getWatchFiles, … 
}

```

The `mapParsingErrors` function ensures that any parsing failure is converted into a `SyntaxError` with line numbers and caret indicators, utilizing the `drawTextBox` utility for markdown-styled error displays.

## Core Parsing Engine: MaestroFlowParser

`MaestroFlowParser` contains the **Jackson YAML ObjectMapper** configured with a custom deserializer. This is where Maestro handles complex YAML structures at the serialization layer, converting YAML nodes into Kotlin data classes.

```kotlin
private val MAPPER = ObjectMapper(YAMLFactory().apply {
    disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER)
}).apply {
    registerModule(KotlinModule.Builder().build())
    registerModule(SimpleModule().apply {
        addDeserializer(YamlFluentCommand::class.java, YamlCommandDeserializer)
    })
}

```

The `parseFlow` method first extracts the **config section** via `parseConfig`, then processes the **command list** through `parseCommands`. Both sections are represented by strongly-typed data classes: `YamlConfig` and `YamlFluentCommand`.

## Deserializing Complex Commands

### String vs Object Commands

The `YamlCommandDeserializer` distinguishes between simple string commands and complex object structures. When Maestro handles complex YAML structures like nested blocks or conditionals, this deserializer determines the appropriate Kotlin mapping.

* **String commands** (e.g., `"launchApp"`, `"tapOn"`) are mapped through the `stringCommands` map to `YamlFluentCommand` instances.
* **Object commands** (e.g., `repeat:` blocks) are reflectively matched to properties of `YamlFluentCommand` using the field name.

If a command is unrecognized, the parser generates a helpful suggestion via `suggestCommandMessage`, pointing the user to similar valid commands.

### Handling Nested Structures

When parsing `repeat` blocks or `runFlow` inclusions, Maestro represents these as dedicated data classes that can contain other commands recursively. The `YamlRepeatCommand` class demonstrates how Maestro handles complex YAML structures with nested command lists:

```kotlin
data class YamlRepeatCommand(
    val times: String? = null,
    val `while`: YamlCondition? = null,
    val commands: List<YamlFluentCommand>,
    val label: String? = null,
    val optional: Boolean = false,
)

```

* `times` accepts literal integers or variable references (e.g., `${repeatCount}`).
* `while` evaluates a `YamlCondition` against device state or variables.
* `commands` contains a **List of `YamlFluentCommand`**, enabling **deep nesting** (repeat-inside-repeat, repeat-inside-runFlow, etc.).

## Building the Execution Tree

`YamlFluentCommand.toCommands` transforms the YAML model into runtime `MaestroCommand` objects (e.g., `TapOnElementCommand`, `RunFlowCommand`, `RepeatCommand`). This is where the abstract syntax tree becomes executable logic.

When Maestro handles complex YAML structures with nested repeats, the conversion happens recursively:

```kotlin
private fun repeatCommand(repeat: YamlRepeatCommand, flowPath: Path, appId: String) = MaestroCommand(
    times = repeat.times,
    condition = repeat.`while`?.toCondition(),
    commands = repeat.commands,
    label = repeat.label,
    optional = repeat.optional,
)

```

The `commands` field is **recursively** transformed via `toCommands`, ensuring that nested repeats, sub-flows, and conditional blocks are fully expanded into the execution plan before the orchestration engine processes them.

## Workspace-Level Orchestration

Beyond single flow files, Maestro handles complex YAML structures at the workspace level through `WorkspaceConfig`. This enables multi-flow executions with tag filtering, custom execution ordering, and global environment variables.

```kotlin
data class WorkspaceConfig(
    val flows: StringList? = null,
    val includeTags: StringList? = null,
    val excludeTags: StringList? = null,
    val executionOrder: ExecutionOrder? = null,
    …
)

```

`YamlCommandReader.readWorkspaceConfig` parses the workspace descriptor, while `WorkspaceExecutionPlanner` computes the final execution plan by filtering flows based on their file-level `tags:` against the workspace `includeTags` and `excludeTags` lists.

## Error Handling and Diagnostics

When Maestro handles complex YAML structures that contain syntax errors, the parser provides deterministic feedback through `FlowParseException` and rich inline diagnostics.

* All parsing errors are wrapped as `FlowParseException` with source location metadata.
* `YamlCommandReader.errorMessage` generates **friendly inline snippets** with line numbers, caret positioning, and markdown-styled boxes via `drawTextBox`.
* The `checkSyntax` method allows IDE plugins and CI pipelines to validate flows without executing them, catching structural errors in `repeat` blocks or malformed `runFlow` references early.

## Practical Examples

### Simple Repeat Block

```yaml

# flow.yaml

appId: com.example.myapp
---
- repeat:
    times: 3
    commands:
      - tapOn: "Login"
      - inputText: "user@example.com"
      - tapOn: "Password"
      - inputText: "secret"
      - tapOn: "Submit"

```

Parsed by `YamlCommandReader.readCommands(Paths.get("flow.yaml"))`, the `repeat.times` field becomes `RepeatCommand.times = 3` at runtime.

### Conditional Repeat with While

```yaml
appId: com.example.myapp
---
- repeat:
    while: "${state == 'loggedOut'}"
    commands:
      - tapOn: "Login"
      - inputText: "user@example.com"
      - tapOn: "Password"
      - inputText: "secret"
      - tapOn: "Submit"

```

The `while` field deserializes into a `YamlCondition` object, compiled into a runtime `Condition` evaluated before each iteration.

### Nested Repeats and Sub-flows

```yaml
appId: com.example.myapp
---
- runFlow: subflow.yaml
  repeat:
    times: 2
    commands:
      - tapOn: "Continue"

```

`runFlow` resolves via `DependencyResolver`, while the outer `repeat` creates a `RepeatCommand` that recursively processes the sub-flow's commands twice.

### Workspace Execution with Tags

```yaml

# workspace.yaml

includeTags: ["smoke"]
excludeTags: ["flaky"]
flows:
  - login_flow.yaml
  - purchase_flow.yaml

```

```kotlin
val workspaceConfig = YamlCommandReader.readWorkspaceConfig(Paths.get("workspace.yaml"))
val planner = WorkspaceExecutionPlanner(
    workspaceRoot = Paths.get("."),
    includeTags = listOf("smoke"),
    excludeTags = listOf()
)
val plan = planner.plan(workspaceConfig)

```

Only flows tagged with `smoke` (and not `flaky`) are scheduled for execution.

## Summary

* Maestro handles complex YAML structures by treating flow files as a DSL, converting them into strongly-typed Kotlin objects via **Jackson YAML**.
* The **YamlCommandReader** serves as the entry point, wrapping all parsing errors in rich, inline diagnostics.
* **YamlCommandDeserializer** distinguishes between simple string commands and complex object structures like `repeat` blocks.
* **YamlRepeatCommand** enables deep nesting through recursive command lists, supporting `times` and `while` conditions.
* **WorkspaceConfig** and **WorkspaceExecutionPlanner** extend parsing to multi-flow workspaces with tag-based filtering.
* All runtime objects are immutable data classes that transform into executable **MaestroCommand** instances through `YamlFluentCommand.toCommands`.

## Frequently Asked Questions

### How does Maestro parse nested YAML commands?

Maestro parses nested YAML commands through recursive deserialization in `YamlCommandDeserializer`. When encountering a `repeat` block or `runFlow` inclusion, the parser instantiates `YamlRepeatCommand` or `YamlRunFlowCommand` objects that contain `List<YamlFluentCommand>` properties. The `YamlFluentCommand.toCommands` method then recursively transforms these nested structures into runtime `MaestroCommand` objects, preserving the hierarchical execution order.

### What library does Maestro use for YAML parsing?

Maestro uses **Jackson** with the **YAMLFactory** for parsing. Specifically, `MaestroFlowParser` configures an `ObjectMapper` with `YAMLFactory().apply { disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER) }`, registers the Kotlin module, and attaches a custom `YamlCommandDeserializer` to handle the DSL-specific command mapping. This combination provides type-safe deserialization of complex YAML structures into Kotlin data classes.

### How does Maestro handle syntax errors in flow files?

Maestro handles syntax errors through the `mapParsingErrors` wrapper in `YamlCommandReader`. All parsing exceptions are caught and converted into `FlowParseException` instances with rich metadata. The `errorMessage` function generates user-friendly diagnostics using `drawTextBox` to create markdown-styled error snippets with line numbers and caret indicators pointing to the exact syntax error. Additionally, the `checkSyntax` method allows validation without execution, useful for CI pipelines and IDE integrations.

### Can Maestro execute multiple flow files with tag filtering?

Yes, Maestro supports multi-flow execution through **WorkspaceConfig** and **WorkspaceExecutionPlanner**. By defining a [`workspace.yaml`](https://github.com/mobile-dev-inc/Maestro/blob/main/workspace.yaml) file with `includeTags` and `excludeTags` lists, users can filter which flows to execute based on file-level tags. The `WorkspaceExecutionPlanner.plan()` method computes the final execution set by matching flow tags against the workspace configuration, enabling selective test runs across complex project structures.