Skip to Content
Mobile Automation
Sign in
  • Home
    • - Appium Foundations
    • - Framework Series
  • About
  • Blog
  • Tags
  • Contact
  • X

Maestro Beyond the Basics: From One Flow to a Real Test Suite

Turn a few Maestro flows into a real test suite

Featured Post For Members Maestro October 12, 2026
Rows of smartphones with lit screens on retail display stands
Photo by Jason Leung on Unsplash
On this page
Unlock full content

In Part 1, you learned to pick selectors that find the right element and you moved the login steps into a reusable subflow, flows/common/login.yaml with two tests that call it.

That's a good start, but real apps throw more at you. Some screens only appear sometimes. Some steps need repeating. Some tests leave data behind when they fail. And once you have more than a few tests, running them one at a time becomes slow and tedious.

This part turns your flows into a real test suite. By the end, you'll know how to:

  • Handle screens that don't always look the same
  • Repeat actions without repeating YAML
  • Run setup and cleanup steps that always execute, even when a test fails
  • Call your own backend from inside a flow
  • Organize your flows before they turn into a messy folder of YAML files

Conditionals: Handling Screens That Don't Always Look the Same

Some screens don't show up every time. A permission dialog might appear on first launch and never again. A promotional banner might show up depending on region or account state. assertVisible on something that's conditionally there fails your test for the wrong reason. Maestro's when clause handles this declaratively instead of forcing you into a try/catch.

- runFlow:
    when:
      platform: Android
      visible: "Allow Notifications"
    commands:
      - tapOn: "Allow"

You've already used one of these. Article 1's cross-platform login flow used when: platform: to tap the LOGIN button differently on Android and iOS — that's a conditional, just one based on the platform rather than on what's on screen.

You can run steps based on four kinds of conditions:

  • visible — only if an element is on screen.
  • notVisible — only if an element is not on screen.
  • platform — only on Android, iOS or Web.
  • true — only if a JavaScript expression is true. Use this when a simple on-screen check isn't enough.

If you list more than one condition, all of them must be true. In the example above, Maestro taps "Allow" only if the test is running on Android and the "Allow Notifications" dialog is showing.

💡 Note: the "Allow Notifications" dialog above is a generic example, not a screen from the demo app. In your own app, check the exact dialog text and which platform triggers it in Maestro Studio (or with maestro hierarchy) before wiring it into a flow; system dialog wording varies between OS versions.
💡 Shortcut for a single tap: if all you need is to tap one thing if it's there, you can skip runFlow and when and add optional: true to the command instead:
- tapOn:
    text: "Dismiss"
    optional: true
    label: "Dismiss popup if it exists"

The difference: when skips the step entirely if the condition isn't met. optional: true still tries the step, but if the element isn't there, the test carries on instead of failing. For one tap, optional: true is shorter. For several steps, stick with runFlow and when.

Loops: Repeating Actions Without Repeating YAML

repeat runs a block of commands multiple times, either a fixed number of times or until a condition is met.

# Fixed number of iterations
- repeat:
    times: 5
    commands:
      - tapOn: "Add Item"
      - tapOn: "Save"

# Runs until a condition is met
- repeat:
    while:
      notVisible: "Your inbox is empty"
    commands:
      - tapOn: "Delete Message"
      - tapOn: "Confirm"

# Both together — a safety net against an infinite loop
- repeat:
    times: 10
    while:
      visible: "Update available"
    commands:
      - tapOn: "Dismiss"
⚠️ Important: a loop with only while keeps going until its condition changes. If the condition never changes — because of a bug in the app, or a selector that matches the wrong element — the loop never stops, and your test hangs. Adding times sets a maximum. In the last example above, the loop stops as soon as "Update available" disappears, or after 10 rounds, whichever comes first.

while also accepts a JavaScript expression through the true condition type for loops driven by something more than an element check:

- evalScript: ${output.attempt = 0}
- repeat:
    while:
      true: ${output.attempt < 3}
    commands:
      - tapOn: "Refresh Data"
      - evalScript: ${output.attempt++}

Lifecycle Hooks: Reliable Setup and Teardown

Article 1 introduced clearState: true as the way to start a flow from a clean slate. onFlowStart and onFlowComplete are the more general version of the same idea: guaranteed setup before the flow's real steps run, and guaranteed cleanup after — success or failure.

# flows/tests/logged_in_alert.yaml
appId: ${APP_ID}

onFlowStart:
  - launchApp:
      clearState: true
  - runFlow:
      file: ../common/login.yaml
      env:
        EMAIL: "test@mobile-automation.io"
        PASSWORD: "Password1234"

onFlowComplete:
  - stopApp

---
- assertVisible: "You are logged in!"
- tapOn: "OK"
- assertNotVisible: "You are logged in!"

Here's what each part does:

  • onFlowStart opens the app fresh and logs in. That's setup not what you're testing.
  • The main steps (below the —) check only what this test is about: the "You are logged in!" alert appears, and tapping OK closes it.
  • onFlowComplete closes the app when the test ends — whether it passed or failed. In a real app, this is where you'd log out or delete any test data the flow created.

Both hooks can run any command, including runFlow. In practice, most teams put their setup and cleanup steps in a subflow and call it from the hook.

What happens when something fails:

  • If onFlowStart fails, the test is marked as failed and the main steps are skipped. onFlowComplete still runs, so cleanup still happens.
  • If onFlowComplete fails, the test is marked as failed, even if the main steps passed.
👍 Always put cleanup in onFlowComplete, not at the end of your test. When a step fails, Maestro stops the test right there, so any cleanup steps after it never run. The demo app doesn't save any data, so you won't run into this here. In a real app, though, picture a test that creates a new account and then fails before the step that deletes it. The account stays behind, and the next test that signs up with the same email fails too. onFlowComplete runs no matter what, so the cleanup always happens.

Calling Your Backend From a Flow

Maestro's JavaScript commands (runScript and evalScript) can do more than help you find elements on screen — they can also send requests straight to your app's API. That lets you set up test data before the test starts instead of tapping through the app to create it.

The demo app has no API of its own, so this example uses JSONPlaceholder, a free public API built for testing. The script gets a fake user from it and the flow logs in to the demo app with that user's email. Create a scripts folder inside flows, then add the script:

// flows/scripts/get_test_user.js
const response = http.get('https://jsonplaceholder.typicode.com/users/1');
const user = json(response.body);

output.testUser = {
    name: user.name,
    email: user.email
};
# flows/tests/api_login.yaml
appId: ${APP_ID}
---
- runScript: ../scripts/get_test_user.js
- launchApp:
    clearState: true
- runFlow:
    file: ../common/login.yaml
    env:
      EMAIL: ${output.testUser.email}
      PASSWORD: "Password1234"
- assertVisible: "You are logged in!"
# Android
maestro --platform android test -e APP_ID=com.wdiodemoapp flows/tests/api_login.yaml
# iOS
maestro --platform ios test -e APP_ID=org.wdiodemoapp flows/tests/api_login.yaml

When you run it, you'll see Sincere@april.biz typed into the email field — that's the email JSONPlaceholder returns for user 1 — and the test passes.

💡 On a real app, you'd usually go one step further: use http.post to create a brand-new test user through your own API then log in with it. Every run starts with fresh data, and no test depends on an account someone else might have changed. Pair it with onFlowComplete to delete the user when the test ends.

The example used http.get. Maestro also has http.post, http.put, http.delete and http.request for any other method. In the script, json() turns the API's response into an object you can read from — that's how user.email works — and anything you save to output can be used back in the flow as ${output.yourKey}. For all the options, see Maestro's HTTP requests docs.

💡 Tip: need different test data on every run? Maestro has a built-in faker object that makes up realistic values for you — names, emails, phone numbers. For example, ${faker.internet().emailAddress()} gives you a new email each time. That's handy for sign-up tests where reusing the same email fails with "already registered" on the second run.
⚠️ Know the limits: Maestro's JavaScript is more limited than JavaScript on your computer. Scripts can call APIs, but they can't read files on your computer (like a CSV of test users) or use extra libraries from npm, so there's no way to load a database driver either. If your setup needs any of those, write it as a separate script and run it before your Maestro tests. The next article shows where that step fits in a CI pipeline.

Organizing a Growing Flow Library

Your flows folder now holds three kinds of files: tests you run (tests/), reusable steps (common/) and scripts (scripts/). Running tests one file at a time stops being practical. You want one command that runs all your tests. But not every file in flows is a test: common/login.yaml is a building block that tests call with an email and password. Run on its own, it has nothing to type and would fail. Maestro needs to know which files are tests and which aren't.

That's what a config.yaml file is for. Here's the problem it solves: when you give Maestro a folder to run, it only looks at the files directly inside that folder. It doesn't look inside subfolders. Your tests are in flows/tests/ — one level down — so if you run maestro test flows/ without a config, Maestro doesn't see any tests and stops with this message: Top-level directories do not contain any Flows

Create flows/config.yaml:

# flows/config.yaml
flows:
  - tests/**

This tells Maestro: "run every flow inside tests/." common/ isn't listed, so Maestro skips it — which is what you want, because common/login.yaml isn't a test on its own. It only works when a test calls it with an email and password.

💡 In these patterns, * means "the files directly in this folder," and ** means "this folder and every folder inside it."

Now point maestro test at the whole folder instead of a single file:

# Android
maestro --platform android test -e APP_ID=com.wdiodemoapp flows/
# iOS
maestro --platform ios test -e APP_ID=org.wdiodemoapp flows/

Maestro runs all four tests you've built in this article — successful_login, failed_login, logged_in_alert, and api_login — and finishes with a summary line for each one.

Tags: running a subset of your suite

config.yaml tells Maestro which files are tests. But you won't always want to run all of them. Sometimes you just want a quick check that the most important things still work — often called a smoke test.

That's what tags are for. A tag is a label you stick on a test like smoke or regression. You can then tell Maestro to run only the tests with a certain label. Add tags to the top of a flow, above the - - -:

# flows/tests/successful_login.yaml
appId: ${APP_ID}
tags:
  - smoke
  - regression
---
- launchApp:
    clearState: true
# ... rest of the flow

Then filter on the command line:

# Only flows tagged smoke
maestro --platform android test -e APP_ID=com.wdiodemoapp --include-tags=smoke flows/

# Everything except flows tagged network
maestro --platform android test -e APP_ID=com.wdiodemoapp --include-tags=smoke flows/

You can also combine tags:

  • List more than one tag to run tests that have any of them. For example, --include-tags=smoke,regression runs a test if it's tagged smoke, regression, or both.
  • Use --include-tags and --exclude-tags together to pick a group of tests, then leave some out. For example, --include-tags=regression --exclude-tags=network runs all the regression tests, but skips any that are also tagged network.

Here's how that works with the four tests you've built. Say you tag them like this:

Test Tags
successful_login smoke, regression
failed_login regression
logged_in_alert regression
api_login regression, network

api_login gets the network tag because it depends on an API over the internet. If that API is down the test fails even when your app is fine — so sometimes you'll want to skip it. Now run:

maestro --platform android test -e APP_ID=com.wdiodemoapp --include-tags=regression --exclude-tags=network flows/

All four tests are tagged regression but api_login is also tagged network, so Maestro skips it:

[Passed] failed_login (22s)
[Passed] logged_in_alert (22s)
[Passed] successful_login (20s)

3/3 Flows Passed in 1m 4s

In practice, this is how teams keep test runs fast. A few quick smoke tests run every time someone changes the code so problems show up within minutes. The full regression suite takes longer so it runs once a night. The next article shows how to use tags in CI — an automated pipeline that runs your tests for you whenever the code changes.

Right now you have four tests in one folder. Once you have forty, you'll want a plan for where each one goes. Maestro's docs suggest two ways to organize them — by what the user is trying to do or by feature:

User Journey — organized around what a user is trying to accomplish, start to finish. The demo app doesn't have goals like that, so imagine a shopping app instead:

flows/
├── config.yaml
├── tests/
│   ├── new_users/
│   │   ├── register.yaml
│   │   └── first_purchase.yaml
│   └── existing_users/
│       ├── login.yaml
│       └── checkout.yaml
└── common/

Feature Test — one folder per screen or feature. Here, the four tests you've built all live in login/ next to tests you might add for the app's other screens:

flows/
├── config.yaml
├── tests/
│   ├── login/
│   │   ├── successful_login.yaml
│   │   ├── failed_login.yaml
│   │   ├── logged_in_alert.yaml
│   │   └── api_login.yaml
│   ├── forms/
│   │   └── fill_form.yaml
│   ├── swipe/
│   │   └── swipe_cards.yaml
│   └── drag/
│       └── drag_and_drop.yaml
├── common/
│   └── login.yaml
└── scripts/
    └── get_test_user.js
💡 If you know the Page Object Model, this might look similar — both are organized by screen. The difference: page objects organize the code that uses a screen while these folders organize the tests. In Maestro, the closest thing to a page object is a subflow in common/, like login.yaml: the steps for using a screen are written once there and your tests call it.

Which one should you pick? Ask yourself: do people open your app to get one specific thing done?

  • Yes → User Journey. Think of a food delivery app: people open it to order dinner then close it. The most important thing to test is that they can get from "I'm hungry" to "order placed," so each test follows that whole path.
  • No → Feature Test. Think of a social media app: people scroll, like post and message, with no single finish line. So you test each feature on its own.

Maestro's docs recommend the same split: User Journey for shopping, banking and food delivery apps, Feature Test for social media, content and entertainment apps.

For the demo app, the answer is no. It's a set of separate screens — Login, Forms, Swipe, Drag — with nothing to "finish." That's why the Feature Test example above uses it.

Adding a new folder is easy. Want tests for the Forms screen? Create flows/tests/forms/ and put them there. You don't need to touch config.yaml — tests/** already picks up every folder inside tests/.

👍 You don't have to pick just one. Many apps mix both. What matters is deciding early before you have forty test files and no idea where anything is.

Quick-reference table

Flow control

Command Purpose Example
when: { visible } Run commands only if an element is visible visible: "Allow"
when: { platform } Run commands only on a given platform platform: Android
when: { true } Run commands only if a JS expression is true true: ${output.x < 3}
repeat: { times } Repeat a fixed number of times times: 5
repeat: { while } Repeat until a condition is false while: { notVisible: "..." }
onFlowStart / onFlowComplete Guaranteed setup / teardown Declared above the --- separator
tags + --include-tags / --exclude-tags Run a subset of your suite tags: [smoke] in the header
optional: true Let one step fail without failing the test tapOn: { text: "Dismiss", optional: true }
config.yaml → flows: Tell Maestro which files are tests flows: [tests/**]

What's Next?

Across both parts, you've gone from one flow to a real test suite: selectors that find the right element, a login subflow every test reuses, conditions and loops, setup and cleanup that always run, an API call from inside a flow and a flows folder you can run — in full or by tag — with one command.

None of it matters if the only place these flows run is your own laptop. The next article, Maestro in CI/CD, wires everything you've built into a pipeline that runs on every push. It also looks at Maestro Cloud, a paid hosted option for when your suite outgrows what a single CI runner can handle.

Download the code

Everything from both parts — the login subflow, all four tests, the API script, and config.yaml — is available as a free download.

🔒 Members-only (free). Subscribe for free to download the code for both parts. No payment, just your email. It also unlocks every other companion download on the site.

This post is for subscribers only

Become a member now and have access to all posts, enjoy exclusive content, and stay updated with constant updates.

Become a member

Already have an account? Sign in

  • Share on X
  • Share on Facebook
  • Share on LinkedIn
  • Share on Pinterest
  • Email

Written by

Mayvin Ramasawmy

Related Posts

How to Write Maestro Flows That Don't Break or Repeat Themselves
Featured Post

How to Write Maestro Flows That Don't Break or Repeat Themselves

Maestro October 5, 2026
Skip Appium's Setup: A Passing Mobile Test in Minutes (Android & iOS)
Featured Post

Skip Appium's Setup: A Passing Mobile Test in Minutes (Android & iOS)

Maestro September 28, 2026
Parallel Mobile Testing, Part 2: Scale Across Devices and Turn It On
Featured Post

Parallel Mobile Testing, Part 2: Scale Across Devices and Turn It On

Framework August 25, 2026

Join the Mobile Automation Community

Receive new Appium and Maestro tutorials, framework design tips, and mobile testing best practices.

Please check your inbox and click the confirmation link.
Subscribe 1 Subscribe 2 Subscribe 3 Subscribe 1 Clone Subscribe 2 Clone Subscribe 3 Clone Subscribe 1 Clone
Subscribe 4 Subscribe 5 Subscribe 6 Subscribe 4 Clone Subscribe 5 Clone Subscribe 6 Clone Subscribe 4 Clone

Tags

Best Practices Appium Java Framework
© 2026 Mobile Automation
Mobile Automation
  • Home
    • - Appium Foundations
    • - Framework Series
  • About
  • Blog
  • Tags
  • Contact
  • X
Sign in