mobster import maestro reads Maestro flows and writes Mobster checks that mobster test, mobster verify --check and the run_tests MCP tool run. It keeps each flow’s name and the order of its commands. It reads the flows as data with a YAML parser, never runs a flow, and never sends a flow’s text to a model as an instruction. (mobster test import maestro is the same command.)
No command is dropped without a line in the output. A command Mobster can’t run becomes a step marked TODO that Smart fills on the check’s first run, and the summary table counts it under “Needs a first run”. An assertion Mobster can’t judge is never turned into a step, because a model never decides a Mobster verdict: the flow is refused and counted under “Unsupported”.
A flow and its check
A Maestro flow,.maestro/checkout.yaml:
mobster import maestro .maestro writes to .mobster/checks/checkout.yaml:
${PROMO} was filled from the flow’s env. The two TODO steps are the commands Mobster doesn’t run. The expectations come last, because Mobster judges them on the accessibility tree once the steps end.
Import a folder of flows
From your project folder:PATH is a flow file or a folder. A folder is read for its top-level .yaml and .yml files; a flow in a subfolder is imported when another flow runs it with runFlow. Checks go to .mobster/checks, or to the folder you give with -o.
runFlow subflow.
Import twice and you get the same files: a check already on disk exactly as imported prints unchanged. A check that differs, such as one you edited, is kept and the output says so. Only --force replaces it.
Run them
mobster test --device NAME.
What converts
${NAME} is replaced from the flow’s env, the runFlow env and the appId. A name with no value is refused.
How text matches
Maestro matchestext as a pattern against the whole text and ignores case. The check keeps that: assertVisible: "get started" becomes /^get started$/i, so it matches the label GET STARTED. A selector that is already a pattern, such as "Order #[0-9]+", is anchored the same way. An id stays exact.
The match is on the element’s accessibility label. The Expo starter’s documentation link has the label Expo documentation, arrow.up.right.square, so assertVisible: "Expo documentation" fails and assertVisible: "Expo documentation.*" passes. The failure names the closest label it found.
Assertions in the middle of a flow
Mobster judges every expectation once, after the last step. A flow that asserts between actions would lose those assertions if it became one check. Mobster writes one check per group instead:flow-part-1 holds the steps up to the first assertion and that assertion, flow-part-2 holds the steps up to the second, and so on. Each part replays the steps from the start. No assertion is dropped.
Steps after the last assertion come after everything a check can judge. The output lists them, because no expectation covers them.
What is ignored
These options change timing or wording and are dropped, with no effect on what a check proves:label, duration, speed, timeout, retryTapIfNoChange, waitToSettleTimeoutMs, delay. optional: true on an assertion is refused, because an optional assertion judges nothing. On a tap it becomes a step that skips when the element isn’t there.
What needs a first run, and what is refused
Every row names the command and says what to do instead. A command that isn’t an assertion becomes aTODO step. Run the check once with Smart and read its report: Smart does what the step needs at that point, or nothing, and replay keeps the path it took. An assertion is refused, and so is a command with a value Mobster can’t read.
The rows that name an assertion (
assertTrue, assertWithAI, assertNoDefectsWithAI, assertScreenshot) refuse the flow. Every other row becomes a TODO step.
Check the result
Read the imported checks before you commit them. The import can’t tell whether a Maestro pattern matches the same label on iOS as it does in Maestro, and a flow written for Android may name text your iOS app doesn’t show. Runmobster test once and read the first report (Reports).