How Maestro Handles Complex YAML Structures: From Parsing to Execution

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, 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.

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.

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:

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:

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.

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


# 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

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

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


# workspace.yaml

includeTags: ["smoke"]
excludeTags: ["flaky"]
flows:
  - login_flow.yaml
  - purchase_flow.yaml
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 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.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →