How to Debug YAML Flows in Maestro: Capturing Debug Bundles and Artifacts

Enable the --debug-output flag when running maestro test to automatically capture UI hierarchies, screenshots, and JSON execution logs that pinpoint exactly why any command fails.

Maestro (mobile-dev-inc/Maestro) is an open-source mobile UI testing framework that executes flows written in YAML. When a flow fails, the framework produces a debug bundle containing full execution metadata, allowing you to inspect the exact UI state and command history without guessing what went wrong.

Enabling Debug Output

The CLI provides two flags to control debug artifact generation. Both are parsed in [TestCommand.kt](https://github.com/mobile-dev-inc/Maestro/blob/main/maestro-cli/src/main/java/maestro/cli/command/TestCommand.kt) (lines 146-160).

Flag Purpose Source Location
--debug-output <path> Specifies the output directory (defaults to <home>/maestro/debug). TestCommand.kt lines 146-151
--flatten-debug-output Flattens the directory structure—writes all files directly to the output folder instead of nested subdirectories. Ideal for CI artifact collection. TestCommand.kt lines 158-160

Run your flow with debug capture enabled:

maestro test my_flow.yaml --debug-output ./debug

This creates a directory structure at ./debug/my_flow/ containing debug.json, a screenshots/ folder, and hierarchy.xml (if a UI inspection failure occurred).

What Gets Recorded

The debug bundle aggregates four distinct artifact types produced by coordinated components across maestro-cli and maestro-orchestra.

Debug JSON (debug.json) A complete execution log written after the flow finishes (success or failure). It contains ordered command metadata including status, evaluated expressions, and error messages. Generated by TestDebugReporter.saveFlow.

Screenshots PNG files captured before and after each command depending on its status (pending, completed, failed, or warned). Produced by ScreenshotUtils.takeDebugScreenshot.

UI Hierarchy An XML (or JSON) dump of the complete view hierarchy captured at the moment a UI-dependent command fails—such as when an element selector returns no matches. Implemented in [Orchestra.kt](https://github.com/mobile-dev-inc/Maestro/blob/main/maestro-orchestra/src/main/java/maestro/orchestra/Orchestra.kt#L425-L435).

Command Metadata In-memory state accumulated during execution within the FlowDebugOutput data class. This container tracks every command's lifecycle and any exceptions thrown. Defined in [TestDebugReporter.kt](https://github.com/mobile-dev-inc/Maestro/blob/main/maestro-cli/src/main/java/maestro/cli/report/TestDebugReporter.kt#L230-L242):

data class FlowDebugOutput(
    val commands: MutableMap<Command, CommandDebugMetadata> = mutableMapOf(),
    val screenshots: MutableList<ScreenshotMetadata> = mutableListOf(),
    var exception: Exception? = null
)

How the Debug Bundle Is Built

The framework constructs the debug bundle through a five-stage pipeline involving the CLI, test runner, and orchestra modules.

  1. CLI Bootstrapping — TestCommand.call() installs the debug reporter with your specified path.

    TestDebugReporter.install(
        debugOutputPathAsString = debugOutput,
        flattenDebugOutput = flattenDebugOutput,
        printToConsole = parent?.verbose == true,
    )

    Source: [TestCommand.kt](https://github.com/mobile-dev-inc/Maestro/blob/main/maestro-cli/src/main/java/maestro/cli/command/TestCommand.kt) lines 46-51.

  2. Flow Initialization — TestRunner.runFlow() instantiates a fresh FlowDebugOutput container for each flow execution.

    val debugOutput = FlowDebugOutput()

    Source: [TestRunner.kt](https://github.com/mobile-dev-inc/Maestro/blob/main/maestro-cli/src/main/java/maestro/cli/runner/TestRunner.kt) lines 56-58.

  3. Command Execution — MaestroCommandRunner runs each command and immediately records screenshots to the debugOutput object, updating status from PENDING to final state.

    ScreenshotUtils.takeDebugScreenshotByCommand(maestro, debugOutput, CommandStatus.PENDING)
    // ... execution ...
    debugOutput.commands[command] = CommandDebugMetadata(...)

    Source: MaestroCommandRunner.kt lines 101-113, 125-128.

  4. Failure Handling — When a command fails, Orchestra captures the view hierarchy and enriches the error message with debugging guidance.

    val hierarchyRoot = maestro.viewHierarchy().root
    debugMessage = "Element with $description not found. Check the UI hierarchy in debug artifacts ..."

    Source: [Orchestra.kt](https://github.com/mobile-dev-inc/Maestro/blob/main/maestro-orchestra/src/main/java/maestro/orchestra/Orchestra.kt) lines 425-436.

  5. Persistence — After completion, TestDebugReporter.saveFlow writes the JSON file and creates the directory structure.

    Files.createDirectories(debugOutput)
    TestDebugReporter.saveFlow(flowName, debugOutput, debugOutputPath)

    Source: [TestDebugReporter.kt](https://github.com/mobile-dev-inc/Maestro/blob/main/maestro-cli/src/main/java/maestro/cli/report/TestDebugReporter.kt) lines 183-194.

Inspecting the Debug Artifacts

Analyze the generated files to diagnose failures without reproducing the issue manually.

debug.json Open this file in any JSON viewer to see an ordered list of commands with their statuses, evaluated arguments, duration in milliseconds, and error messages. Each command entry includes a debugMessage field that often contains the specific selector or expression that failed.

screenshots/ Browse PNG files named by command index (e.g., 0_failed.png, 1_completed.png). Compare the screenshot taken before the command executed with the one taken after to visualize UI transitions and timing issues.

hierarchy.xml Import this file into Android Studio's Layout Inspector or view it in any XML editor to see the complete UI tree at the exact moment of failure. Use this to verify that element selectors in your YAML match actual view attributes.

Quick Inspection Workflow


# Run the flow

maestro test ./flows/checkout.yaml --debug-output ./debug

# Navigate to the generated bundle

cd ./debug/checkout

# View the UI hierarchy (macOS example)

open hierarchy.xml

# Examine the failing command's screenshot

open screenshots/3_failed.png

# Query the JSON log for failed commands

cat debug.json | jq '.commands[] | select(.status == "FAILED")'

Common Debugging Scenarios

Element Not Found Errors When tapOn or assertVisible fails, open hierarchy.xml to verify the element exists and check the debugMessage in debug.json for the exact selector that failed. The message explicitly references the UI hierarchy artifact for verification.

Timing and Flakiness Check the durationMs field in debug.json for each command. If a command fails after a very short duration (e.g., < 500ms), the UI likely hadn't settled. Add explicit waitFor commands or increase retry counts in your YAML.

Screenshot Assertion Mismatches When using assertScreenshot, compare the expected baseline with the actual screenshot saved in screenshots/. The debug bundle also stores a diff image highlighting pixel differences when the assertion fails.

Unexpected Crashes The exception field in debug.json contains the full stack trace of any unhandled exception, while the commands map shows exactly which instruction was executing when the crash occurred.

Summary

  • Enable debug mode with --debug-output <path> to automatically capture comprehensive failure data.
  • Key artifacts include debug.json (execution log), screenshots/ (visual state), and hierarchy.xml (UI tree).
  • Source coordination involves TestCommand, TestDebugReporter, ScreenshotUtils, and Orchestra working together to build the bundle.
  • Inspect hierarchies to validate selectors, check screenshots to verify visual state, and query debug.json for precise error messages and timing data.
  • Use --flatten-debug-output in CI pipelines to simplify artifact archival.

Frequently Asked Questions

Where does Maestro store debug output by default?

If you omit the --debug-output flag, Maestro writes debug bundles to <home>/maestro/debug/<flow-name>/. You can override this path by providing a specific directory to the flag, such as --debug-output ./ci-artifacts.

Can I view the debug bundle on CI without downloading files?

Yes. Use --flatten-debug-output to ensure all files sit in a single directory, then configure your CI system (GitHub Actions, GitLab CI, etc.) to upload that folder as a pipeline artifact. Most CI UIs allow browsing PNG screenshots and JSON files directly in the browser.

Why is hierarchy.xml missing from my debug bundle?

The UI hierarchy is only captured when a command that inspects the UI fails—typically an element lookup like tapOn or assertVisible. If your flow fails due to a JavaScript error, network timeout, or assertion unrelated to UI elements, the hierarchy file may not be generated because the view state was never queried.

How do I correlate a screenshot filename with a specific command in my YAML?

Screenshots are named using the command's zero-based index in the flow execution sequence followed by its final status (e.g., 2_failed.png means the third command failed). Match this index against the commands array in debug.json, which preserves the execution order and includes the command name and arguments for each step.

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 →